Skip to content

Commit

Permalink
Add an acceptance criteria.
Browse files Browse the repository at this point in the history
When using the heuristic to optimise the objective function it was doing
so in ignorance of the constraints.

This add the ability to pass a `acceptance_criteria` to both heuristics
which gives a bound on some measure. In practice it's used in
`schedule.heuristic` when an objective function is passed. In this case
it passes the violation counts as an `acceptance_criteria` to ensure
that as each heuristic goes through the search space optimising the
objective function it will only accept a solution if this does not add
any constraint violations.

TLDR:

The modified tests show a good example of this. The objective function
optimisation was giving a schedule that was in fact invalid. This has
now been fixed.
  • Loading branch information
drvinceknight committed Aug 9, 2017
1 parent eecb0a2 commit 07bada3
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 5 deletions.
13 changes: 12 additions & 1 deletion src/conference_scheduler/heuristics/hill_climber.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
def hill_climber(objective_function,
initial_array,
lower_bound=-float('inf'),
acceptance_criteria=None,
max_iterations=10 ** 3):
"""
Implement a basic hill climbing algorithm.
Expand All @@ -12,9 +13,17 @@ def hill_climber(objective_function,
1. Maximum number of iterations;
2. A known lower bound, a none is passed then this is not used.
If acceptance_criteria (a callable) is not None then this is used to obtain
an upper bound on some other measure (different to the objective function).
In practice this is used when optimising the objective function to ensure
that we don't accept a solution that improves the objective function but tht
adds more constraint violations.
"""

X = initial_array
if acceptance_criteria is not None:
acceptance_bound = acceptance_criteria(X)

iterations = 0
current_energy = objective_function(X)
Expand All @@ -25,7 +34,9 @@ def hill_climber(objective_function,
candidate = element_from_neighbourhood(X)
candidate_energy = objective_function(candidate)

if candidate_energy < current_energy:
if (candidate_energy < current_energy and
(acceptance_criteria is None or
acceptance_criteria(candidate) <= acceptance_bound)):

X = candidate
current_energy = candidate_energy
Expand Down
8 changes: 7 additions & 1 deletion src/conference_scheduler/heuristics/simulated_annealing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ def simulated_annealing(objective_function,
initial_array,
initial_temperature=10 ** 4,
cooldown_rate=0.7,
acceptance_criteria=None,
lower_bound=-float('inf'),
max_iterations=10 ** 3):
"""
Expand All @@ -21,6 +22,8 @@ def simulated_annealing(objective_function,
"""

X = initial_array
if acceptance_criteria is not None:
acceptance_bound = acceptance_criteria(X)
best_X = X

iterations = 0
Expand All @@ -36,7 +39,10 @@ def simulated_annealing(objective_function,

delta = candidate_energy - current_energy

if candidate_energy < best_energy:
if (candidate_energy < best_energy and
(acceptance_criteria is None or
acceptance_criteria(candidate) <= acceptance_bound)):

best_energy = candidate_energy
best_X = candidate

Expand Down
1 change: 1 addition & 0 deletions src/conference_scheduler/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def func(array):

X = algorithm(initial_array=X,
objective_function=func,
acceptance_criteria=count_violations,
**objective_function_algorithm_kwargs)

return list(zip(*np.nonzero(X)))
Expand Down
26 changes: 25 additions & 1 deletion tests/heuristics/test_hill_climber.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ def objective_function(array):
max_iterations=1)

assert objective_function(X) == 1
# TODO Check warning is raised


def test_hill_climber_for_objective_function(slots, events):
Expand All @@ -89,3 +88,28 @@ def objective_function(array):
max_iterations=10)

assert objective_function(X) == -440


def test_hill_climbing_for_obj_function_with_criteria(slots, events):

def objective_function(array):
return of.capacity_demand_difference(slots, events, array)

def acceptance_criteria(array):
return sum(array[:, 3])

array = np.array([
[1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 0, 0, 0]
])
assert acceptance_criteria(array) == 0
assert objective_function(array) == -400

np.random.seed(0)
X = hill_climber(initial_array=array,
objective_function=objective_function,
acceptance_criteria=acceptance_criteria,
max_iterations=100)

assert objective_function(X) == -400
25 changes: 25 additions & 0 deletions tests/heuristics/test_simulated_annealing.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,31 @@ def objective_function(array):
assert objective_function(X) == -440


def test_simulated_annealing_for_obj_function_with_criteria(slots, events):

def objective_function(array):
return of.capacity_demand_difference(slots, events, array)

def acceptance_criteria(array):
return sum(array[:, 3])

array = np.array([
[1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 0, 0, 0]
])
assert acceptance_criteria(array) == 0
assert objective_function(array) == -400

np.random.seed(0)
X = simulated_annealing(initial_array=array,
objective_function=objective_function,
acceptance_criteria=acceptance_criteria,
max_iterations=100)

assert objective_function(X) == -400


def test_simulated_annealing_for_objective_function_starting_temp(
slots, events):

Expand Down
4 changes: 2 additions & 2 deletions tests/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ def test_heuristic_solution_with_simulated_annealing(events, slots):
algorithm=heu.simulated_annealing,
objective_function=of.capacity_demand_difference)

assert solution == [(0, 2), (1, 3), (2, 1)]
assert solution == [(0, 3), (1, 0), (2, 5)]

solution = scheduler.heuristic(
events=events,
Expand All @@ -350,4 +350,4 @@ def test_heuristic_solution_with_simulated_annealing(events, slots):
objective_function_algorithm_kwargs={"max_iterations": 2},
objective_function=of.capacity_demand_difference)

assert solution == [(0, 2), (1, 3), (2, 6)]
assert solution == [(0, 2), (1, 0), (2, 6)]

0 comments on commit 07bada3

Please sign in to comment.