job-shop-lib 1.3.0__py3-none-any.whl → 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,9 +25,8 @@ def no_setup_time_calculator(
25
25
  ) -> int:
26
26
  """Default start time calculator that implements the standard behavior.
27
27
 
28
- The start time is the maximum of the next available time for the
29
- machine and the next available time for the job to which the
30
- operation belongs.
28
+ The start time is the maximum of the machine's next available time, the
29
+ job's next available time, and the operation's release date.
31
30
 
32
31
  Args:
33
32
  dispatcher:
@@ -44,6 +43,7 @@ def no_setup_time_calculator(
44
43
  return max(
45
44
  dispatcher.machine_next_available_time[machine_id],
46
45
  dispatcher.job_next_available_time[operation.job_id],
46
+ operation.release_date,
47
47
  )
48
48
 
49
49
 
@@ -552,7 +552,9 @@ class Dispatcher:
552
552
  for machine_id in operation.machines
553
553
  )
554
554
  job_start_time = self._job_next_available_time[operation.job_id]
555
- return max(machine_earliest_start_time, job_start_time)
555
+ return max(
556
+ machine_earliest_start_time, job_start_time, operation.release_date
557
+ )
556
558
 
557
559
  def remaining_duration(
558
560
  self, scheduled_operation: ScheduledOperation
@@ -632,7 +634,11 @@ class Dispatcher:
632
634
  def is_ongoing(self, scheduled_operation: ScheduledOperation) -> bool:
633
635
  """Checks if the given operation is currently being processed."""
634
636
  current_time = self.current_time()
635
- return scheduled_operation.start_time <= current_time
637
+ return (
638
+ scheduled_operation.start_time
639
+ <= current_time
640
+ < scheduled_operation.end_time
641
+ )
636
642
 
637
643
  def next_operation(self, job_id: int) -> Operation:
638
644
  """Returns the next operation to be scheduled for the given job.
@@ -88,6 +88,8 @@ def filter_non_immediate_operations(
88
88
  min_start_time = dispatcher.min_start_time(operations)
89
89
  immediate_operations: list[Operation] = []
90
90
  for operation in operations:
91
+ if operation.release_date > min_start_time:
92
+ continue
91
93
  start_time = dispatcher.earliest_start_time(operation)
92
94
  if start_time == min_start_time:
93
95
  immediate_operations.append(operation)
@@ -14,6 +14,7 @@ dispatcher.
14
14
  PositionInJobObserver
15
15
  RemainingOperationsObserver
16
16
  IsCompletedObserver
17
+ DatesObserver
17
18
  FeatureObserverType
18
19
  feature_observer_factory
19
20
  FeatureObserverConfig
@@ -41,6 +42,7 @@ from ._is_scheduled_observer import IsScheduledObserver
41
42
  from ._position_in_job_observer import PositionInJobObserver
42
43
  from ._remaining_operations_observer import RemainingOperationsObserver
43
44
  from ._is_completed_observer import IsCompletedObserver
45
+ from ._dates_observer import DatesObserver
44
46
  from ._factory import (
45
47
  FeatureObserverType,
46
48
  feature_observer_factory,
@@ -60,6 +62,7 @@ __all__ = [
60
62
  "PositionInJobObserver",
61
63
  "RemainingOperationsObserver",
62
64
  "IsCompletedObserver",
65
+ "DatesObserver",
63
66
  "FeatureObserverType",
64
67
  "feature_observer_factory",
65
68
  "FeatureObserverConfig",
@@ -0,0 +1,162 @@
1
+ """Home of the `DatesObserver` class."""
2
+
3
+ from typing import Literal
4
+
5
+ import numpy as np
6
+
7
+ from job_shop_lib import ScheduledOperation, JobShopInstance
8
+ from job_shop_lib.dispatching import Dispatcher
9
+ from job_shop_lib.dispatching.feature_observers import (
10
+ FeatureObserver,
11
+ FeatureType,
12
+ )
13
+
14
+ Attribute = Literal["release_date", "deadline", "due_date"]
15
+
16
+
17
+ class DatesObserver(FeatureObserver):
18
+ """Observes time-related attributes of operations.
19
+
20
+ This observer tracks attributes like release date, deadline, and
21
+ due date for each operation in the job shop instance. The attributes to
22
+ be observed can be specified during initialization.
23
+
24
+ The values are stored in a numpy array of shape ``(num_operations,
25
+ num_attributes)``, where ``num_attributes`` is the number of attributes
26
+ being observed.
27
+
28
+ All attributes are updated based on the current time of the dispatcher so
29
+ that they reflect the relative time remaining until the event occurs:
30
+ (attribute - current_time). This means that the values will be negative if
31
+ the event is in the past.
32
+
33
+ Args:
34
+ dispatcher:
35
+ The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
36
+ subscribe:
37
+ If ``True``, the observer is subscribed to the dispatcher upon
38
+ initialization. Otherwise, the observer must be subscribed later
39
+ or manually updated.
40
+ attributes_to_observe:
41
+ A list of attributes to observe. If ``None``, all available time
42
+ attributes (release_date, deadline, due_date) will be
43
+ observed, provided they exist in the instance.
44
+ """
45
+
46
+ _supported_feature_types = [FeatureType.OPERATIONS]
47
+
48
+ __slots__ = {
49
+ "attributes_to_observe": "List of attributes to observe.",
50
+ "_attribute_map": "Maps attributes to their column index.",
51
+ }
52
+
53
+ def __init__(
54
+ self,
55
+ dispatcher: Dispatcher,
56
+ *,
57
+ subscribe: bool = True,
58
+ attributes_to_observe: list[Attribute] | None = None,
59
+ feature_types: FeatureType | list[FeatureType] | None = None,
60
+ ):
61
+ self.attributes_to_observe = self._determine_attributes_to_observe(
62
+ dispatcher.instance, attributes_to_observe
63
+ )
64
+ self._attribute_map = {
65
+ attr: i for i, attr in enumerate(self.attributes_to_observe)
66
+ }
67
+ self._previous_current_time = 0
68
+ super().__init__(
69
+ dispatcher,
70
+ subscribe=subscribe,
71
+ feature_types=feature_types,
72
+ )
73
+
74
+ @property
75
+ def feature_sizes(self) -> dict[FeatureType, int]:
76
+ return {
77
+ FeatureType.OPERATIONS: len(self.attributes_to_observe),
78
+ }
79
+
80
+ @property
81
+ def attribute_map(self) -> dict[Attribute, int]:
82
+ """Maps attributes to their column index in the features array."""
83
+ return self._attribute_map
84
+
85
+ def initialize_features(self):
86
+ """Initializes the features for the operations.
87
+
88
+ This method sets up the features for the operations based on the
89
+ attributes to observe. It creates a numpy array with the shape
90
+ (num_operations, num_attributes) and fills it with the corresponding
91
+ values from the job shop instance. Note that the matrix may contain
92
+ ``np.nan`` values for operations that do not have deadlines or
93
+ due dates.
94
+
95
+ .. seealso::
96
+ - :meth:`job_shop_lib.JobShopInstance.release_dates_matrix_array`
97
+ - :meth:`job_shop_lib.JobShopInstance.deadlines_matrix_array`
98
+ - :meth:`job_shop_lib.JobShopInstance.due_dates_matrix_array`
99
+ """
100
+ self.features = {
101
+ FeatureType.OPERATIONS: np.zeros(
102
+ (
103
+ self.dispatcher.instance.num_operations,
104
+ len(self.attributes_to_observe),
105
+ ),
106
+ dtype=np.float32,
107
+ )
108
+ }
109
+ self._previous_current_time = self.dispatcher.current_time()
110
+ release_dates_matrix = (
111
+ self.dispatcher.instance.release_dates_matrix_array
112
+ )
113
+ valid_operations_mask = ~np.isnan(release_dates_matrix.flatten())
114
+
115
+ for attr, col_idx in self._attribute_map.items():
116
+ matrix = getattr(self.dispatcher.instance, f"{attr}s_matrix_array")
117
+ values = np.array(matrix).flatten()
118
+ valid_values = values[valid_operations_mask]
119
+ self.features[FeatureType.OPERATIONS][:, col_idx] = valid_values
120
+
121
+ def update(self, scheduled_operation: ScheduledOperation):
122
+ """Updates the features based on the scheduled operation.
123
+
124
+ This method updates the features by subtracting the current time from
125
+ the initial release date, deadline, and due date attributes of the
126
+ operations.
127
+
128
+ Args:
129
+ scheduled_operation:
130
+ The scheduled operation that has just been processed. It is
131
+ not used in this observer, but is required by the
132
+ :meth:`FeatureObserver.update` method.
133
+ """
134
+ current_time = self.dispatcher.current_time()
135
+ elapsed_time = current_time - self._previous_current_time
136
+ self._previous_current_time = current_time
137
+ cols = [
138
+ self._attribute_map[attr]
139
+ for attr in self.attributes_to_observe
140
+ ]
141
+ self.features[FeatureType.OPERATIONS][:, cols] -= elapsed_time
142
+
143
+ def _determine_attributes_to_observe(
144
+ self,
145
+ instance: JobShopInstance,
146
+ attributes_to_observe: list[Attribute] | None,
147
+ ) -> list[Attribute]:
148
+ if attributes_to_observe:
149
+ return attributes_to_observe
150
+
151
+ default_attributes: list[Attribute] = []
152
+ if instance.has_release_dates:
153
+ default_attributes.append("release_date")
154
+ if instance.has_deadlines:
155
+ default_attributes.append("deadline")
156
+ if instance.has_due_dates:
157
+ default_attributes.append("due_date")
158
+ return default_attributes
159
+
160
+ def reset(self):
161
+ """Calls :meth:`initialize_features`"""
162
+ self.initialize_features()
@@ -77,12 +77,13 @@ class DurationObserver(FeatureObserver):
77
77
  self.features[FeatureType.JOBS][job_id, 0] = job_duration
78
78
 
79
79
  def _update_operation_durations(
80
- self, scheduled_operation: ScheduledOperation
80
+ self, unused_scheduled_operation: ScheduledOperation
81
81
  ):
82
- operation_id = scheduled_operation.operation.operation_id
83
- self.features[FeatureType.OPERATIONS][operation_id, 0] = (
84
- self.dispatcher.remaining_duration(scheduled_operation)
85
- )
82
+ for scheduled_operation in self.dispatcher.ongoing_operations():
83
+ operation_id = scheduled_operation.operation.operation_id
84
+ self.features[FeatureType.OPERATIONS][operation_id, 0] = (
85
+ self.dispatcher.remaining_duration(scheduled_operation)
86
+ )
86
87
 
87
88
  def _update_machine_durations(
88
89
  self, scheduled_operation: ScheduledOperation
@@ -18,6 +18,7 @@ Dispatching rules:
18
18
  :nosignatures:
19
19
 
20
20
  shortest_processing_time_rule
21
+ largest_processing_time_rule
21
22
  first_come_first_served_rule
22
23
  most_work_remaining_rule
23
24
  most_operations_remaining_rule
@@ -32,6 +33,7 @@ Dispatching rule scorers:
32
33
  :nosignatures:
33
34
 
34
35
  shortest_processing_time_score
36
+ largest_processing_time_score
35
37
  first_come_first_served_score
36
38
  MostWorkRemainingScorer
37
39
  most_operations_remaining_score
@@ -45,6 +47,8 @@ from ._dispatching_rules_functions import (
45
47
  most_work_remaining_rule,
46
48
  most_operations_remaining_rule,
47
49
  random_operation_rule,
50
+ largest_processing_time_score,
51
+ largest_processing_time_rule,
48
52
  score_based_rule,
49
53
  score_based_rule_with_tie_breaker,
50
54
  shortest_processing_time_score,
@@ -78,6 +82,8 @@ __all__ = [
78
82
  "most_work_remaining_rule",
79
83
  "most_operations_remaining_rule",
80
84
  "random_operation_rule",
85
+ "largest_processing_time_score",
86
+ "largest_processing_time_rule",
81
87
  "score_based_rule",
82
88
  "score_based_rule_with_tie_breaker",
83
89
  "observer_based_most_work_remaining_rule",
@@ -17,6 +17,7 @@ from job_shop_lib.dispatching.rules import (
17
17
  most_operations_remaining_rule,
18
18
  random_operation_rule,
19
19
  most_work_remaining_rule,
20
+ largest_processing_time_rule,
20
21
  )
21
22
 
22
23
 
@@ -24,6 +25,7 @@ class DispatchingRuleType(str, Enum):
24
25
  """Enumeration of dispatching rules for the job shop scheduling problem."""
25
26
 
26
27
  SHORTEST_PROCESSING_TIME = "shortest_processing_time"
28
+ LARGEST_PROCESSING_TIME = "largest_processing_time"
27
29
  FIRST_COME_FIRST_SERVED = "first_come_first_served"
28
30
  MOST_WORK_REMAINING = "most_work_remaining"
29
31
  MOST_OPERATIONS_REMAINING = "most_operations_remaining"
@@ -62,6 +64,9 @@ def dispatching_rule_factory(
62
64
  DispatchingRuleType.SHORTEST_PROCESSING_TIME: (
63
65
  shortest_processing_time_rule
64
66
  ),
67
+ DispatchingRuleType.LARGEST_PROCESSING_TIME: (
68
+ largest_processing_time_rule
69
+ ),
65
70
  DispatchingRuleType.FIRST_COME_FIRST_SERVED: (
66
71
  first_come_first_served_rule
67
72
  ),
@@ -26,6 +26,37 @@ def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
26
26
  )
27
27
 
28
28
 
29
+ def largest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
30
+ """Dispatches the operation with the longest duration."""
31
+ return max(
32
+ dispatcher.available_operations(),
33
+ key=lambda operation: operation.duration,
34
+ )
35
+
36
+
37
+ def largest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
38
+ """Scores each job based on the duration of the next operation.
39
+
40
+ The score is the duration of the next operation in each job.
41
+ Jobs with longer next operations will have higher scores.
42
+
43
+ Args:
44
+ dispatcher:
45
+ The :class:`~job_shop_lib.dispatching.Dispatcher` instance
46
+ containing the job shop instance and the current state of the
47
+ schedule.
48
+
49
+ Returns:
50
+ A list of scores for each job, where the score is the duration of
51
+ the next operation in that job.
52
+ """
53
+ num_jobs = dispatcher.instance.num_jobs
54
+ scores = [0] * num_jobs
55
+ for operation in dispatcher.available_operations():
56
+ scores[operation.job_id] = operation.duration
57
+ return scores
58
+
59
+
29
60
  def first_come_first_served_rule(dispatcher: Dispatcher) -> Operation:
30
61
  """Dispatches the operation with the lowest position in job."""
31
62
  return min(
@@ -98,9 +98,7 @@ def generate_machine_matrix_without_recirculation(
98
98
  (num_jobs, 1),
99
99
  )
100
100
  # Shuffle the columns:
101
- machine_matrix = np.apply_along_axis(
102
- rng.permutation, 1, machine_matrix
103
- )
101
+ machine_matrix = np.apply_along_axis(rng.permutation, 1, machine_matrix)
104
102
  return machine_matrix
105
103
 
106
104
 
@@ -0,0 +1,61 @@
1
+ """Metaheuristic algorithms for solving job shop scheduling problems.
2
+
3
+ This module provides implementations of various metaheuristic optimization
4
+ algorithms designed to solve the job shop scheduling problem.
5
+
6
+ Metaheuristics are particularly well-suited for JSSP due to their ability to:
7
+
8
+ - Handle large solution spaces efficiently
9
+ - Escape local optima through stochastic mechanisms
10
+ - Balance exploration and exploitation of the search space
11
+ - Provide good quality solutions within reasonable computational time
12
+
13
+ Currently implemented algorithms:
14
+
15
+ - Simulated annealing: A probabilistic technique that accepts worse
16
+ solutions with decreasing probability to escape local optima
17
+
18
+ The module aims to contain implementations of other
19
+ metaheuristic algorithms such as genetic algorithms, particle swarm
20
+ optimization, tabu search, etc. Feel free to open an issue if you want to
21
+ contribute!
22
+
23
+ .. autosummary::
24
+ :nosignatures:
25
+
26
+ JobShopAnnealer
27
+ SimulatedAnnealingSolver
28
+ NeighborGenerator
29
+ swap_adjacent_operations
30
+ swap_in_critical_path
31
+ swap_random_operations
32
+ ObjectiveFunction
33
+ get_makespan_with_penalties_objective
34
+
35
+ """
36
+
37
+ from job_shop_lib.metaheuristics._objective_functions import (
38
+ ObjectiveFunction,
39
+ get_makespan_with_penalties_objective,
40
+ )
41
+ from job_shop_lib.metaheuristics._neighbor_generators import (
42
+ NeighborGenerator,
43
+ swap_adjacent_operations,
44
+ swap_in_critical_path,
45
+ swap_random_operations,
46
+ )
47
+ from job_shop_lib.metaheuristics._job_shop_annealer import JobShopAnnealer
48
+ from job_shop_lib.metaheuristics._simulated_annealing_solver import (
49
+ SimulatedAnnealingSolver,
50
+ )
51
+
52
+ __all__ = [
53
+ "JobShopAnnealer",
54
+ "SimulatedAnnealingSolver",
55
+ "NeighborGenerator",
56
+ "swap_adjacent_operations",
57
+ "swap_in_critical_path",
58
+ "swap_random_operations",
59
+ "ObjectiveFunction",
60
+ "get_makespan_with_penalties_objective",
61
+ ]
@@ -0,0 +1,229 @@
1
+ import random
2
+ import math
3
+ import time
4
+
5
+ import simanneal
6
+
7
+ from job_shop_lib import JobShopInstance, Schedule
8
+ from job_shop_lib.exceptions import ValidationError
9
+ from job_shop_lib.metaheuristics import (
10
+ NeighborGenerator,
11
+ ObjectiveFunction,
12
+ swap_in_critical_path,
13
+ get_makespan_with_penalties_objective,
14
+ )
15
+
16
+
17
+ class JobShopAnnealer(simanneal.Annealer):
18
+ """Helper class for the :class:`SimulatedAnnealingSolver`.
19
+
20
+ It uses `simanneal <https://github.com/perrygeo/simanneal>`_ as the
21
+ backend.
22
+
23
+ In the context of the job shop scheduling problem, simulated annealing is
24
+ particularly useful for improving previous solutions.
25
+
26
+ The neighbor move is pluggable via a ``neighbor_generator`` function. By
27
+ default it uses :func:`swap_in_critical_path`, but any function that takes
28
+ a schedule and a random generator and returns a new schedule can be
29
+ provided to tailor the exploration of the search space.
30
+
31
+ The process involves iteratively exploring the solution space:
32
+
33
+ 1. A random move is made to alter the current state. This is done by
34
+ swapping two operations in the sequence of a machine.
35
+ 2. The "energy" of the new state is evaluated using an objective function.
36
+ With the default objective function, the energy is calculated as the
37
+ makespan of the schedule plus penalties for any constraint violations
38
+ (such as deadlines and due dates). See
39
+ :func:`get_makespan_with_penalties_objective` for details. You can
40
+ create custom objective functions by implementing the
41
+ :class:`ObjectiveFunction` interface, which takes a schedule and returns
42
+ a float representing the energy of that schedule.
43
+ 3. The new state is accepted if it has lower energy (a better solution).
44
+ If it has higher energy, it might still be accepted with a certain
45
+ probability, which depends on the current "temperature". The
46
+ temperature decreases over time, reducing the chance of accepting
47
+ worse solutions as the algorithm progresses. This helps to avoid
48
+ getting stuck in local optima.
49
+
50
+ This is repeated until the solution converges or a maximum number of
51
+ steps is reached.
52
+
53
+ Tuning the annealer is crucial for performance. The base
54
+ ``simanneal.Annealer`` class provides parameters that can be adjusted:
55
+
56
+ - ``Tmax``: Maximum (starting) temperature (default: 25000.0).
57
+ - ``Tmin``: Minimum (ending) temperature (default: 2.5).
58
+ - ``steps``: Number of iterations (default: 50000).
59
+ - ``updates``: Number of progress updates (default: 100).
60
+
61
+ A good starting point is to set ``Tmax`` to a value that accepts about 98%
62
+ of moves and ``Tmin`` to a value where the solution no longer improves.
63
+ The number of ``steps`` should be large enough to explore the search space
64
+ thoroughly.
65
+
66
+ These parameters can be set on the annealer instance. For example:
67
+ ``annealer.Tmax = 12000.0``
68
+
69
+ Alternatively, this class provides an ``auto`` method to find reasonable
70
+ parameters based on a desired runtime:
71
+ ``auto_schedule = annealer.auto(minutes=1)``
72
+ ``annealer.set_schedule(auto_schedule)``
73
+
74
+ Attributes:
75
+ instance:
76
+ The job shop instance to solve.
77
+ random_generator:
78
+ Random generator for reproducibility.
79
+ neighbor_generator:
80
+ Function used to generate neighbors from the current schedule.
81
+ Defaults to :func:`swap_in_critical_path`.
82
+ objective_function:
83
+ Function that computes the energy of the schedule. If ``None``,
84
+ it defaults to :func:`get_makespan_with_penalties_objective`.
85
+ This function receives a schedule and returns the energy that will
86
+ be minimized by the annealer.
87
+
88
+ Args:
89
+ instance:
90
+ The job shop instance to solve. It retrieves the jobs and
91
+ machines from the instance and uses them to create the schedule.
92
+ initial_state:
93
+ Initial state of the schedule as a list of lists, where each
94
+ sublist represents the operations of a job.
95
+ seed:
96
+ Random seed for reproducibility. If ``None``, random behavior
97
+ will be non-deterministic.
98
+ neighbor_generator:
99
+ Function that receives the current schedule and a random generator
100
+ and returns a new schedule to explore. Defaults to
101
+ :func:`swap_in_critical_path`. Use this to plug in custom
102
+ neighborhoods (e.g., adjacent swaps).
103
+ objective_function:
104
+ Function that computes the energy of the schedule. If ``None``,
105
+ it defaults to :func:`get_makespan_with_penalties_objective`.
106
+ This callable receives a :class:`~job_shop_lib.Schedule` and
107
+ returns a float that will be minimized by the annealer.
108
+ """
109
+
110
+ copy_strategy = "method"
111
+
112
+ def __init__(
113
+ self,
114
+ instance: JobShopInstance,
115
+ initial_state: Schedule,
116
+ *,
117
+ seed: int | None = None,
118
+ neighbor_generator: NeighborGenerator = swap_in_critical_path,
119
+ objective_function: ObjectiveFunction | None = None,
120
+ ):
121
+ super().__init__(initial_state)
122
+ self.instance = instance
123
+ if objective_function is None:
124
+ self.objective_function = get_makespan_with_penalties_objective()
125
+ else:
126
+ self.objective_function = objective_function
127
+ self.random_generator = random.Random(seed)
128
+ self.neighbor_generator = neighbor_generator
129
+
130
+ def _get_state(self) -> Schedule:
131
+ """Returns the current state of the annealer.
132
+
133
+ This method facilitates type checking.
134
+ """
135
+ return self.state
136
+
137
+ def move(self) -> None:
138
+ """Generates a neighbor state using the configured neighbor generator.
139
+
140
+ Delegates to ``self.neighbor_generator`` with the current schedule and
141
+ the internal random generator, enabling pluggable neighborhoods.
142
+ """
143
+ self.state = self.neighbor_generator(
144
+ self._get_state(), self.random_generator
145
+ )
146
+
147
+ def anneal(self) -> tuple[Schedule, float]:
148
+ """Minimizes the energy of a system by simulated annealing.
149
+
150
+ Overrides the ``anneal`` method from the base class to use the
151
+ random generator defined in the constructor.
152
+
153
+ Returns:
154
+ The best state and energy found during the annealing process.
155
+ """
156
+ step = 0
157
+ self.start = time.time()
158
+
159
+ # Precompute factor for exponential cooling from Tmax to Tmin
160
+ if self.Tmin <= 0.0:
161
+ raise ValidationError(
162
+ "Exponential cooling requires a minimum "
163
+ "temperature greater than zero."
164
+ )
165
+ t_factor = -math.log(self.Tmax / self.Tmin)
166
+
167
+ # Note initial state
168
+ t = self.Tmax
169
+ current_energy = self.energy()
170
+ prev_state = self.copy_state(self.state)
171
+ prev_energy = current_energy
172
+ self.best_state = self.copy_state(self.state)
173
+ self.best_energy = current_energy
174
+ trials, accepts, improves = 0, 0, 0
175
+ update_wave_length = 0 # not used, but avoids pylint warning
176
+ if self.updates > 0:
177
+ update_wave_length = self.steps / self.updates
178
+ self.update(step, t, current_energy, None, None)
179
+
180
+ # Attempt moves to new states
181
+ while step < self.steps and not self.user_exit:
182
+ step += 1
183
+ t = self.Tmax * math.exp(t_factor * step / self.steps)
184
+ self.move()
185
+ current_energy = self.energy()
186
+ delta_e = current_energy - prev_energy
187
+ trials += 1
188
+ if (
189
+ delta_e > 0.0
190
+ and math.exp(-delta_e / t) < self.random_generator.random()
191
+ ):
192
+ # Restore previous state
193
+ self.state = self.copy_state(prev_state)
194
+ current_energy = prev_energy
195
+ else:
196
+ # Accept new state and compare to best state
197
+ accepts += 1
198
+ if delta_e < 0.0:
199
+ improves += 1
200
+ prev_state = self.copy_state(self.state)
201
+ prev_energy = current_energy
202
+ if current_energy < self.best_energy:
203
+ self.best_state = self.copy_state(self.state)
204
+ self.best_energy = current_energy
205
+ if self.updates < 1:
206
+ continue
207
+ if (step // update_wave_length) > (
208
+ (step - 1) // update_wave_length
209
+ ):
210
+ self.update(
211
+ step,
212
+ t,
213
+ current_energy,
214
+ accepts / trials,
215
+ improves / trials,
216
+ )
217
+ trials, accepts, improves = 0, 0, 0
218
+
219
+ self.state = self.copy_state(self.best_state)
220
+ if self.save_state_on_exit:
221
+ self.save_state()
222
+
223
+ return self.best_state, self.best_energy
224
+
225
+ def energy(self) -> float:
226
+ """Computes the energy of the current schedule using the objective
227
+ function provided."""
228
+ schedule = self._get_state()
229
+ return self.objective_function(schedule)