Skip to content

Commit 0e36edd

Browse files
committed
v0.0.17 Wrapped GeneticAlgorithm, a little refactor
1 parent 0964bfd commit 0e36edd

File tree

13 files changed

+332
-71
lines changed

13 files changed

+332
-71
lines changed

examples/object_oriented/nqueens/scripts/solve_nqueens.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515
from greyjack.agents import *
1616
from greyjack.Solver import Solver
1717
from greyjack.agents.TabuSearch import TabuSearch
18+
from greyjack.agents.GeneticAlgorithm import GeneticAlgorithm
1819

1920
if __name__ == "__main__":
2021

2122
# build domain model
22-
domain_builder = DomainBuilderNQueens(10000, random_seed=45)
23+
domain_builder = DomainBuilderNQueens(1024, random_seed=45)
2324
cotwin_builder = CotwinBuilderNQueens()
2425

2526
#termination_strategy = StepsLimit(step_count_limit=1000)
@@ -29,6 +30,9 @@
2930
agent = TabuSearch(neighbours_count=20, tabu_entity_rate=0.0,
3031
mutation_rate_multiplier=None, move_probas=[0, 1, 0, 0, 0, 0],
3132
migration_frequency=1, termination_strategy=termination_strategy)
33+
"""agent = GeneticAlgorithm(population_size=128, crossover_probability=0.5, p_best_rate=0.05,
34+
tabu_entity_rate=0.0, mutation_rate_multiplier=1.0, move_probas=[0, 1, 0, 0, 0, 0],
35+
migration_rate=0.00001, migration_frequency=1, termination_strategy=termination_strategy)"""
3236

3337
solver = Solver(domain_builder, cotwin_builder, agent,
3438
n_jobs=10, parallelization_backend="processing", #processing, threading

greyjack/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

greyjack/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "greyjack"
3-
version = "0.0.16"
3+
version = "0.0.17"
44
edition = "2021"
55

66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
from greyjack.agents.base.Agent import Agent
3+
from greyjack.agents.metaheuristic_bases.GeneticAlgorithmBase import GeneticAlgorithmBase
4+
from greyjack.score_calculation.score_requesters.OOPScoreRequester import OOPScoreRequester
5+
6+
class GeneticAlgorithm(Agent):
7+
def __init__(
8+
self,
9+
population_size=128, crossover_probability=0.5, p_best_rate=0.05,
10+
tabu_entity_rate=0.0, mutation_rate_multiplier=None, move_probas=None,
11+
migration_rate=0.00001, migration_frequency=10, termination_strategy=None,
12+
):
13+
14+
super().__init__(migration_rate, migration_frequency, termination_strategy)
15+
16+
self.population_size = population_size
17+
self.crossover_probability = crossover_probability
18+
self.p_best_rate = p_best_rate
19+
self.tabu_entity_rate = tabu_entity_rate
20+
self.mutation_rate_multiplier = mutation_rate_multiplier
21+
self.move_probas = move_probas
22+
23+
def _build_metaheuristic_base(self):
24+
self.score_requester = OOPScoreRequester(self.cotwin)
25+
semantic_groups_dict = self.score_requester.variables_manager.semantic_groups_map.copy()
26+
discrete_ids = self.score_requester.variables_manager.discrete_ids.copy()
27+
28+
self.metaheuristic_base = GeneticAlgorithmBase.new(
29+
self.cotwin.score_calculator.score_variant,
30+
self.score_requester.variables_manager,
31+
self.population_size,
32+
self.crossover_probability,
33+
self.p_best_rate,
34+
self.tabu_entity_rate,
35+
semantic_groups_dict,
36+
self.mutation_rate_multiplier,
37+
self.move_probas.copy() if self.move_probas else None,
38+
discrete_ids,
39+
)
40+
41+
return self
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
from greyjack.greyjack import GeneticAlgorithmSimple, GeneticAlgorithmHardSoft, GeneticAlgorithmHardMediumSoft
3+
from greyjack.score_calculation.scores.ScoreVariants import ScoreVariants
4+
5+
class GeneticAlgorithmBase:
6+
def new(score_variant, variables_manager_py,
7+
population_size, crossover_probability, p_best_rate,
8+
tabu_entity_rate,
9+
semantic_groups_dict,
10+
mutation_rate_multiplier, move_probas, discrete_ids):
11+
if score_variant == ScoreVariants.SimpleScore:
12+
return GeneticAlgorithmSimple(variables_manager_py, population_size,
13+
crossover_probability, p_best_rate, tabu_entity_rate,
14+
semantic_groups_dict,
15+
mutation_rate_multiplier, move_probas, discrete_ids)
16+
if score_variant == ScoreVariants.HardSoftScore:
17+
return GeneticAlgorithmHardSoft(variables_manager_py, population_size,
18+
crossover_probability, p_best_rate, tabu_entity_rate,
19+
semantic_groups_dict,
20+
mutation_rate_multiplier, move_probas, discrete_ids)
21+
if score_variant == ScoreVariants.HardMediumSoftScore:
22+
return GeneticAlgorithmHardMediumSoft(variables_manager_py, population_size,
23+
crossover_probability, p_best_rate, tabu_entity_rate,
24+
semantic_groups_dict,
25+
mutation_rate_multiplier, move_probas, discrete_ids)
26+
27+
raise Exception("score_variant unrecognized")

greyjack/pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ features = ["pyo3/extension-module"]
77

88
[project]
99
name = "greyjack"
10-
version = "0.0.16"
10+
version = "0.0.17"
1111
requires-python = ">=3.9"
1212
dependencies = [
1313
"dill",
@@ -28,14 +28,15 @@ authors = [
2828
maintainers = [
2929
{name = "Egor Chnegov (CameleoGrey)", email = "cameleogrey@yandex.ru"},
3030
]
31-
description = "GreyJack Solver is an AI constraint solver for Python (current version) built on top of Polars. It empowers you to solve a wide range of constraint optimization problems, including continuous, integer, and mixed-integer challenges."
31+
description = "GreyJack Solver is a metaheuristic constraint solver for Python (current version) built on top of Polars. It empowers you to solve a wide range of constraint optimization problems, including continuous, integer, and mixed-integer challenges."
3232
readme = "README.md"
3333
license = { file = "LICENSE" }
3434
keywords = ["greyjack", "solver", "optimization", "polars", "metaheuristic", "genetic algorithm"]
3535
classifiers = [
3636
'Intended Audience :: Science/Research',
3737
'Intended Audience :: Developers',
3838
"License :: OSI Approved :: MIT License",
39+
"License :: OSI Approved :: Apache Software License",
3940
'Programming Language :: Python',
4041
'Programming Language :: Python :: 3',
4142
'Programming Language :: Python :: 3.9',
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
2+
3+
#[macro_export]
4+
macro_rules! build_concrete_genetic_algorithm_base {
5+
($me_base_name: ident, $individual_variant: ident, $score_type: ty) => {
6+
#[pyclass]
7+
pub struct $me_base_name {
8+
9+
pub population_size: usize,
10+
pub half_population_size: usize,
11+
pub crossover_probability: f64,
12+
pub mutation_rate_multiplier: f64,
13+
pub p_best_rate: f64,
14+
pub tabu_entity_rate: f64,
15+
16+
pub metaheuristic_kind: String,
17+
pub metaheuristic_name: String,
18+
19+
pub group_mutation_rates_map: HashMap<String, f64>,
20+
pub discrete_ids: Option<Vec<usize>>,
21+
pub mover: Mover,
22+
pub variables_manager: VariablesManager,
23+
}
24+
25+
#[pymethods]
26+
impl $me_base_name {
27+
28+
#[new]
29+
#[pyo3(signature = (variables_manager_py, population_size,
30+
crossover_probability, p_best_rate, tabu_entity_rate,
31+
semantic_groups_dict,
32+
mutation_rate_multiplier=None, move_probas=None, discrete_ids=None))]
33+
pub fn new(
34+
variables_manager_py: VariablesManagerPy,
35+
population_size: usize,
36+
crossover_probability: f64,
37+
p_best_rate: f64,
38+
tabu_entity_rate: f64,
39+
semantic_groups_dict: HashMap<String, Vec<usize>>,
40+
mutation_rate_multiplier: Option<f64>,
41+
move_probas: Option<Vec<f64>>,
42+
discrete_ids: Option<Vec<usize>>,
43+
) -> Self {
44+
45+
let half_population_size = (0.5 * (population_size as f64)).ceil() as usize;
46+
let current_mutation_rate_multiplier;
47+
match mutation_rate_multiplier {
48+
Some(x) => current_mutation_rate_multiplier = mutation_rate_multiplier.unwrap(),
49+
None => current_mutation_rate_multiplier = 0.0 // 0.0 - always use minimal possible move size, 1.0 - is more intuitive,
50+
}
51+
let mut group_mutation_rates_map: HashMap<String, f64> = HashMap::new();
52+
for group_name in semantic_groups_dict.keys() {
53+
let group_size = semantic_groups_dict[group_name].len();
54+
let current_group_mutation_rate = current_mutation_rate_multiplier * (1.0 / (group_size as f64));
55+
group_mutation_rates_map.insert(group_name.clone(), current_group_mutation_rate);
56+
}
57+
58+
Self {
59+
population_size: population_size,
60+
half_population_size: half_population_size,
61+
crossover_probability: crossover_probability,
62+
mutation_rate_multiplier: current_mutation_rate_multiplier,
63+
p_best_rate: p_best_rate,
64+
tabu_entity_rate: tabu_entity_rate,
65+
66+
metaheuristic_kind: "Population".to_string(),
67+
metaheuristic_name: "GeneticAlgorithm".to_string(),
68+
69+
group_mutation_rates_map: group_mutation_rates_map.clone(),
70+
discrete_ids: discrete_ids.clone(),
71+
mover: Mover::new(tabu_entity_rate, HashMap::new(), HashMap::new(), HashMap::new(), group_mutation_rates_map.clone(), move_probas),
72+
variables_manager: VariablesManager::new(variables_manager_py.variables_vec.clone()),
73+
}
74+
}
75+
76+
fn select_p_best_id(&mut self) -> usize {
77+
78+
let p_best_proba = Uniform::new(0.000001, self.p_best_rate).sample(&mut StdRng::from_entropy());
79+
let last_top_id = (p_best_proba * (self.population_size as f64)).ceil() as usize;
80+
let chosen_id:usize = Uniform::new(0, last_top_id).sample(&mut StdRng::from_entropy());
81+
82+
return chosen_id;
83+
}
84+
85+
fn select_p_worst_id(&mut self) -> usize {
86+
87+
let p_best_proba = Uniform::new(0.000001, self.p_best_rate).sample(&mut StdRng::from_entropy());
88+
let last_top_id = (p_best_proba * (self.population_size as f64)).ceil() as usize;
89+
let chosen_id: usize = Uniform::new(self.population_size - last_top_id, self.population_size).sample(&mut StdRng::from_entropy());
90+
91+
return chosen_id;
92+
}
93+
94+
fn cross(&mut self, candidate_1: Vec<f64>, candidate_2: Vec<f64>) -> (Vec<f64>, Vec<f64>) {
95+
96+
let variables_count = candidate_1.len();
97+
let mut weights = vec![Uniform::new_inclusive(0.0, 1.0).sample(&mut StdRng::from_entropy()); variables_count];
98+
99+
match &self.discrete_ids {
100+
None => (),
101+
Some(discrete_ids) => discrete_ids.into_iter().for_each(|i| weights[*i] = math_utils::rint(weights[*i]))
102+
}
103+
104+
let new_candidate_1: Vec<f64> =
105+
weights.iter()
106+
.zip(candidate_1.iter())
107+
.zip(candidate_2.iter())
108+
.map(|((w, c_1), c_2)| {
109+
c_1 * w + c_2 * (1.0 - w)
110+
})
111+
.collect();
112+
113+
let new_candidate_2: Vec<f64> =
114+
weights.iter()
115+
.zip(candidate_1.iter())
116+
.zip(candidate_2.iter())
117+
.map(|((w, c_1), c_2)| {
118+
c_2 * w + c_1 * (1.0 - w)
119+
})
120+
.collect();
121+
122+
return (new_candidate_1, new_candidate_2);
123+
}
124+
125+
fn sample_candidates_plain(
126+
&mut self,
127+
population: Vec<$individual_variant>,
128+
current_top_individual: $individual_variant,
129+
) -> Vec<Vec<f64>> {
130+
131+
if self.mover.tabu_entity_size_map.len() == 0 {
132+
let semantic_groups_map = self.variables_manager.semantic_groups_map.clone();
133+
for (group_name, group_ids) in semantic_groups_map {
134+
self.mover.tabu_ids_sets_map.insert(group_name.clone(), HashSet::new());
135+
self.mover.tabu_entity_size_map.insert(group_name.clone(), max((self.tabu_entity_rate * (group_ids.len() as f64)).ceil() as usize, 1));
136+
self.mover.tabu_ids_vecdeque_map.insert(group_name.clone(), VecDeque::new());
137+
}
138+
}
139+
140+
let mut population = population;
141+
population.sort();
142+
143+
let mut candidates: Vec<Vec<f64>> = Vec::new();
144+
for i in 0..self.half_population_size {
145+
let mut candidate_1 = population[self.select_p_best_id()].variable_values.clone();
146+
let mut candidate_2 = population[self.select_p_best_id()].variable_values.clone();
147+
148+
if Uniform::new_inclusive(0.0, 1.0).sample(&mut StdRng::from_entropy()) <= self.crossover_probability {
149+
(candidate_1, candidate_2) = self.cross(candidate_1, candidate_2);
150+
}
151+
152+
let (changed_candidate_1, changed_columns_1, _) = self.mover.do_move(&mut candidate_1, &self.variables_manager, false);
153+
let (changed_candidate_2, changed_columns_2, _) = self.mover.do_move(&mut candidate_2, &self.variables_manager, false);
154+
155+
candidate_1 = changed_candidate_1.unwrap();
156+
candidate_2 = changed_candidate_2.unwrap();
157+
158+
159+
// for crossover with rint() one doesn't need for fixing the whole candidate vector
160+
// float values are crossed without rint, but due to the convex sum they will be still into the bounds
161+
// all sampled values are always in the bounds
162+
// problems can occur only by swap mutations, so fix all changed by a move columns
163+
self.variables_manager.fix_variables(&mut candidate_1, changed_columns_1);
164+
self.variables_manager.fix_variables(&mut candidate_2, changed_columns_2);
165+
166+
candidates.push(candidate_1);
167+
candidates.push(candidate_2);
168+
}
169+
170+
return candidates;
171+
}
172+
173+
fn sample_candidates_incremental(
174+
&mut self,
175+
population: Vec<$individual_variant>,
176+
current_top_individual: $individual_variant,
177+
) -> (Vec<f64>, Vec<Vec<(usize, f64)>>) {
178+
panic!("Incremental candidates sampling is available only for local search approaches (TabuSearch, LateAcceptance, etc).")
179+
}
180+
181+
fn build_updated_population(
182+
&mut self,
183+
current_population: Vec<$individual_variant>,
184+
candidates: Vec<$individual_variant>
185+
) -> Vec<$individual_variant> {
186+
187+
let mut winners: Vec<$individual_variant> = Vec::new();
188+
for i in 0..self.population_size {
189+
let chosen_id = self.select_p_worst_id();
190+
let weak_native = current_population[chosen_id].clone();
191+
let candidate = &candidates[i];
192+
let winner = if &candidate.score <= &weak_native.score {candidate.clone()} else {weak_native.clone()};
193+
winners.push(winner);
194+
}
195+
196+
return winners;
197+
}
198+
199+
fn build_updated_population_incremental(
200+
&mut self,
201+
current_population: Vec<$individual_variant>,
202+
sample: Vec<f64>,
203+
deltas: Vec<Vec<(usize, f64)>>,
204+
scores: Vec<$score_type>,
205+
) -> Vec<$individual_variant> {
206+
207+
panic!("Incremental candidates sampling is available only for local search approaches (TabuSearch, LateAcceptance, etc).")
208+
}
209+
210+
#[getter]
211+
pub fn metaheuristic_kind(&self) -> String {
212+
self.metaheuristic_kind.clone()
213+
}
214+
215+
#[getter]
216+
pub fn metaheuristic_name(&self) -> String {
217+
self.metaheuristic_name.clone()
218+
}
219+
220+
}
221+
};
222+
}

0 commit comments

Comments
 (0)