Skip to content

Commit 08af3be

Browse files
committed
v0.0.23 Icremental score calculator + examples with it (NQueens, TSP, VRP)
1 parent 436f516 commit 08af3be

24 files changed

+607
-98
lines changed

examples/object_oriented/nqueens/persistence/CotwinBuilderNQueens.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33

44
from greyjack.persistence.CotwinBuilderBase import CotwinBuilderBase
55
from examples.object_oriented.nqueens.cotwin.NQueensCotwin import NQueensCotwin
6-
from examples.object_oriented.nqueens.score.NQueensScoreCalculator import NQueensScoreCalculator
6+
from examples.object_oriented.nqueens.score.PlainScoreCalculatorNQueens import PlainScoreCalculatorNQueens
7+
from examples.object_oriented.nqueens.score.IncrementalScoreCalculatorNQueens import IncrementalScoreCalculatorNQueens
78
from examples.object_oriented.nqueens.cotwin.CotQueen import CotQueen
89
from greyjack.variables.GJInteger import GJInteger
910

1011

1112
class CotwinBuilderNQueens(CotwinBuilderBase):
12-
def __init__(self):
13+
def __init__(self, use_incremental_score_calculator):
14+
self.use_incremental_score_calculator = use_incremental_score_calculator
1315
pass
1416

1517
def build_cotwin(self, domain_model, is_already_initialized):
@@ -27,7 +29,10 @@ def build_cotwin(self, domain_model, is_already_initialized):
2729

2830
nqueens_cotwin = NQueensCotwin()
2931
nqueens_cotwin.add_planning_entities_list( cot_queens, "queens" )
30-
nqueens_cotwin.set_score_calculator( NQueensScoreCalculator() )
32+
if self.use_incremental_score_calculator:
33+
nqueens_cotwin.set_score_calculator( IncrementalScoreCalculatorNQueens() )
34+
else:
35+
nqueens_cotwin.set_score_calculator( PlainScoreCalculatorNQueens() )
3136

3237
return nqueens_cotwin
3338

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
2+
from greyjack.score_calculation.score_calculators.IncrementalScoreCalculator import IncrementalScoreCalculator
3+
from greyjack.score_calculation.scores.SimpleScore import SimpleScore
4+
from greyjack.score_calculation.scores.ScoreVariants import ScoreVariants
5+
import polars as pl
6+
import numpy as np
7+
import numba
8+
from numba import jit, int64, float64, vectorize
9+
10+
11+
class IncrementalScoreCalculatorNQueens(IncrementalScoreCalculator):
12+
def __init__(self):
13+
super().__init__()
14+
self.score_variant = ScoreVariants.SimpleScore
15+
self.add_constraint("all_different", self.all_different)
16+
pass
17+
18+
def all_different(self, planning_entity_dfs, problem_fact_dfs, delta_dfs):
19+
# pseudo-incremental manner: just use deltas to reduce planning dfs building cost
20+
# the score still computes on the whole vector sample
21+
# relatively easy implement and understand (comparable to true incremental or super optimized manner)
22+
# with huge performance boost on large datasets
23+
24+
queens_df = planning_entity_dfs["queens"]
25+
queens_deltas_df = delta_dfs["queens"]
26+
27+
native_column_ids = queens_df["column_id"].to_numpy()
28+
native_row_ids = queens_df["row_id"].to_numpy()
29+
target_unique_row_ids_count = native_row_ids.shape[0]
30+
31+
scores = []
32+
splitted_dfs = queens_deltas_df.partition_by(["sample_id"], maintain_order=True, include_key=False)
33+
for sample_df in splitted_dfs:
34+
delta_df_row_ids = sample_df["candidate_df_row_id"].to_numpy()
35+
delta_row_ids = sample_df["row_id"].to_numpy()
36+
sample_row_ids = native_row_ids.copy()
37+
38+
sample_row_ids[delta_df_row_ids] = delta_row_ids
39+
40+
unique_sample_row_ids = np.unique(sample_row_ids)
41+
unique_sample_desc_ids = np.unique(native_column_ids + sample_row_ids)
42+
unique_sample_asc_ids = np.unique(native_column_ids - sample_row_ids)
43+
44+
unique_rows_penalty = target_unique_row_ids_count - unique_sample_row_ids.shape[0]
45+
unique_desc_ids_penalty = target_unique_row_ids_count - unique_sample_desc_ids.shape[0]
46+
unique_asc_ids_penalty = target_unique_row_ids_count - unique_sample_asc_ids.shape[0]
47+
48+
total_penalty = unique_rows_penalty + unique_desc_ids_penalty + unique_asc_ids_penalty
49+
scores.append(SimpleScore(total_penalty))
50+
51+
return scores
52+
53+
"""def all_different(self, planning_entity_dfs, problem_fact_dfs, delta_dfs):
54+
# alternative (much more complex and faster) version
55+
# after experiments: better works on medium/low dimensions (especially with jit)
56+
57+
queens_df = planning_entity_dfs["queens"]
58+
queens_deltas_df = delta_dfs["queens"]
59+
60+
native_column_ids = queens_df["column_id"].to_numpy()
61+
native_row_ids = queens_df["row_id"].to_numpy()
62+
target_unique_row_ids_count = native_row_ids.shape[0]
63+
64+
n_samples = queens_deltas_df["sample_id"].n_unique() # fast call to Rust function
65+
sample_array = queens_deltas_df.select(["sample_id", "candidate_df_row_id", "row_id"]).to_numpy() # guarantee of order
66+
scores = compute_score_values(
67+
native_column_ids,
68+
native_row_ids,
69+
target_unique_row_ids_count,
70+
sample_array[:, 0],
71+
sample_array[:, 1],
72+
sample_array[:, 2],
73+
n_samples
74+
) # jitted code
75+
scores = [SimpleScore(score_value) for score_value in scores]
76+
77+
return scores"""
78+
79+
@staticmethod
80+
@jit(nopython=True)
81+
#@numba.cfunc("float64[:](int64[:], int64[:], int64, int64[:], int64[:], int64[:], int64)")
82+
def compute_score_values(
83+
native_column_ids,
84+
native_row_ids,
85+
target_unique_row_ids_count,
86+
sample_ids,
87+
delta_df_row_ids,
88+
delta_row_ids,
89+
n_samples
90+
):
91+
92+
score_values = np.zeros((n_samples, ), dtype=np.float64)
93+
sample_array_id = 0
94+
batch_start = sample_array_id
95+
for sample_id in range(n_samples):
96+
sample_row_ids = native_row_ids.copy()
97+
98+
while sample_ids[batch_start] == sample_ids[sample_array_id]:
99+
sample_row_ids[delta_df_row_ids[sample_array_id]] = delta_row_ids[sample_array_id]
100+
sample_array_id += 1
101+
if sample_array_id >= sample_ids.shape[0]:
102+
break
103+
batch_start = sample_array_id
104+
105+
unique_sample_row_ids = np.unique(sample_row_ids)
106+
unique_sample_desc_ids = np.unique(native_column_ids + sample_row_ids)
107+
unique_sample_asc_ids = np.unique(native_column_ids - sample_row_ids)
108+
109+
unique_rows_penalty = target_unique_row_ids_count - unique_sample_row_ids.shape[0]
110+
unique_desc_ids_penalty = target_unique_row_ids_count - unique_sample_desc_ids.shape[0]
111+
unique_asc_ids_penalty = target_unique_row_ids_count - unique_sample_asc_ids.shape[0]
112+
113+
score_values[sample_id] = unique_rows_penalty + unique_desc_ids_penalty + unique_asc_ids_penalty
114+
115+
return score_values

examples/object_oriented/nqueens/score/NQueensScoreCalculator.py renamed to examples/object_oriented/nqueens/score/PlainScoreCalculatorNQueens.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,8 @@
55
from greyjack.score_calculation.scores.ScoreVariants import ScoreVariants
66
import polars as pl
77

8-
#import os
9-
#os.environ["POLARS_MAX_THREADS"] = "1"
10-
#os.environ["MIMALLOC_ABANDONED_PAGE_RESET"] = "1"
118

12-
13-
class NQueensScoreCalculator(PlainScoreCalculator):
9+
class PlainScoreCalculatorNQueens(PlainScoreCalculator):
1410
def __init__(self):
1511

1612
super().__init__()

examples/object_oriented/nqueens/scripts/solve_nqueens.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@
2222
if __name__ == "__main__":
2323

2424
# build domain model
25-
domain_builder = DomainBuilderNQueens(256, random_seed=45)
26-
cotwin_builder = CotwinBuilderNQueens()
25+
domain_builder = DomainBuilderNQueens(1024, random_seed=45)
26+
cotwin_builder = CotwinBuilderNQueens(use_incremental_score_calculator=True)
2727

2828
#termination_strategy = StepsLimit(step_count_limit=1000)
29-
termination_strategy = TimeSpentLimit(time_seconds_limit=60)
29+
#termination_strategy = TimeSpentLimit(time_seconds_limit=60)
3030
#termination_strategy = ScoreNoImprovement(time_seconds_limit=15)
31-
#termination_strategy = ScoreLimit(score_to_compare=[0])
31+
termination_strategy = ScoreLimit(score_to_compare=[0])
3232
agent = TabuSearch(neighbours_count=128, tabu_entity_rate=0.0,
33-
mutation_rate_multiplier=None, move_probas=None,
34-
migration_frequency=10, termination_strategy=termination_strategy)
33+
mutation_rate_multiplier=None, move_probas=[0, 1, 0, 0, 0, 0],
34+
migration_frequency=1, termination_strategy=termination_strategy)
3535
"""agent = GeneticAlgorithm(population_size=128, crossover_probability=0.5, p_best_rate=0.05,
3636
tabu_entity_rate=0.0, mutation_rate_multiplier=1.0, move_probas=[0, 1, 0, 0, 0, 0],
3737
migration_rate=0.00001, migration_frequency=1, termination_strategy=termination_strategy)"""
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""OR-Tools solution to the N-queens problem."""
2+
import sys
3+
import time
4+
from ortools.sat.python import cp_model
5+
6+
7+
class NQueenSolutionPrinter(cp_model.CpSolverSolutionCallback):
8+
"""Print intermediate solutions."""
9+
10+
def __init__(self, queens: list[cp_model.IntVar]):
11+
cp_model.CpSolverSolutionCallback.__init__(self)
12+
self.__queens = queens
13+
self.__solution_count = 0
14+
self.__start_time = time.time()
15+
16+
@property
17+
def solution_count(self) -> int:
18+
return self.__solution_count
19+
20+
def on_solution_callback(self):
21+
current_time = time.time()
22+
print(
23+
f"Solution {self.__solution_count}, "
24+
f"time = {current_time - self.__start_time} s"
25+
)
26+
self.__solution_count += 1
27+
28+
"""all_queens = range(len(self.__queens))
29+
for i in all_queens:
30+
for j in all_queens:
31+
if self.value(self.__queens[j]) == i:
32+
# There is a queen in column j, row i.
33+
print("Q", end=" ")
34+
else:
35+
print("_", end=" ")
36+
print()
37+
print()"""
38+
39+
40+
41+
def main(board_size: int) -> None:
42+
# Creates the solver.
43+
model = cp_model.CpModel()
44+
45+
# Creates the variables.
46+
# There are `board_size` number of variables, one for a queen in each column
47+
# of the board. The value of each variable is the row that the queen is in.
48+
queens = [model.new_int_var(0, board_size - 1, f"x_{i}") for i in range(board_size)]
49+
50+
# Creates the constraints.
51+
# All rows must be different.
52+
model.add_all_different(queens)
53+
54+
# No two queens can be on the same diagonal.
55+
model.add_all_different(queens[i] + i for i in range(board_size))
56+
model.add_all_different(queens[i] - i for i in range(board_size))
57+
58+
# Solve the model.
59+
solver = cp_model.CpSolver()
60+
solution_printer = NQueenSolutionPrinter(queens)
61+
solver.parameters.enumerate_all_solutions = False
62+
solver.solve(model, solution_printer)
63+
64+
# Statistics.
65+
print("\nStatistics")
66+
print(f" conflicts : {solver.num_conflicts}")
67+
print(f" branches : {solver.num_branches}")
68+
print(f" wall time : {solver.wall_time} s")
69+
print(f" solutions found: {solution_printer.solution_count}")
70+
71+
72+
if __name__ == "__main__":
73+
# By default, solve the 8x8 problem.
74+
size = 1024
75+
if len(sys.argv) > 1:
76+
size = int(sys.argv[1])
77+
main(size)

examples/object_oriented/tsp/persistence/CotwinBuilder.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@
44
from examples.object_oriented.tsp.cotwin.CotStop import CotStop
55
from examples.object_oriented.tsp.cotwin.EdgeDistance import EdgeDistance
66
from examples.object_oriented.tsp.cotwin.TSPCotwin import TSPCotwin
7-
from examples.object_oriented.tsp.score.TSPPlainScoreCalculator import TSPPlainScoreCalculator
7+
from examples.object_oriented.tsp.score.PlainScoreCalculatorTSP import PlainScoreCalculatorTSP
8+
from examples.object_oriented.tsp.score.IncrementalScoreCalculatorTSP import IncrementalScoreCalculatorTSP
89

910
from greyjack.persistence.CotwinBuilderBase import CotwinBuilderBase
1011
from greyjack.variables.GJInteger import GJInteger
1112

1213

1314
class CotwinBuilder(CotwinBuilderBase):
1415

15-
def __init__(self):
16+
def __init__(self, use_incremental_score_calculator):
1617

1718
super().__init__()
1819

20+
self.use_incremental_score_calculator = use_incremental_score_calculator
21+
1922
pass
2023

2124
def build_cotwin(self, domain_model, is_already_initialized):
@@ -27,7 +30,11 @@ def build_cotwin(self, domain_model, is_already_initialized):
2730
# (!) better use distance matrix mapping than building and joining large distance dataframe!
2831
#tsp_cotwin.add_problem_facts_list(self._build_edge_distances(domain_model), "edge_distances")
2932

30-
score_calculator = TSPPlainScoreCalculator()
33+
if self.use_incremental_score_calculator:
34+
score_calculator = PlainScoreCalculatorTSP()
35+
else:
36+
score_calculator = IncrementalScoreCalculatorTSP()
37+
3138
score_calculator.utility_objects["distance_matrix"] = domain_model.distance_matrix
3239
tsp_cotwin.set_score_calculator( score_calculator )
3340

examples/object_oriented/tsp/persistence/DomainBuilder.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import re
44
import numpy as np
5+
from numba import jit
56
from examples.object_oriented.tsp.domain.TravelSchedule import TravelSchedule
67
from examples.object_oriented.tsp.domain.Vehicle import Vehicle
78
from examples.object_oriented.tsp.domain.Location import Location
@@ -65,21 +66,26 @@ def build_from_solution(self, solution):
6566
def build_from_domain(self, domain):
6667
return super().build_from_domain(domain)
6768

68-
69-
7069
def _build_distance_matrix(self, locations_list):
7170

71+
@staticmethod
72+
@jit()
73+
def compute_distance_matrix(latitudes, longitudes, n_locations):
74+
distance_matrix = np.zeros( (n_locations, n_locations), dtype=np.int64 )
75+
for i in range(n_locations):
76+
for j in range(n_locations):
77+
distance_from_to = np.sqrt((latitudes[i] - latitudes[j])**2 + (longitudes[i] - longitudes[j])**2)
78+
distance_matrix[i][j] = round(1000 * distance_from_to, 0)
79+
return distance_matrix
7280

7381
n_locations = len(locations_list)
74-
distance_matrix = np.zeros( (n_locations, n_locations), dtype=np.int64 )
75-
#distance_matrix = np.zeros((n_locations, n_locations), dtype=np.float32)
76-
for i in range(n_locations):
77-
for j in range(n_locations):
78-
location_from = locations_list[i]
79-
location_to = locations_list[j]
80-
distance_from_to = location_from.get_distance_to_other_location( location_to )
81-
distance_matrix[i][j] = round(1000 * distance_from_to, 0)
82-
#distance_matrix[i][j] = distance_from_to
82+
latitudes = np.zeros((n_locations, ), dtype=np.float64)
83+
longitudes = np.zeros((n_locations, ), dtype=np.float64)
84+
for i, location in enumerate(locations_list):
85+
latitudes[i] = location.latitude
86+
longitudes[i] = location.longitude
87+
88+
distance_matrix = compute_distance_matrix(latitudes, longitudes, n_locations)
8389

8490
return distance_matrix
8591

0 commit comments

Comments
 (0)