gimuby 0.7.2
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.
- data/Gemfile +1 -0
- data/LICENSE.md +25 -0
- data/README.md +0 -0
- data/lib/gimuby.rb +10 -0
- data/lib/gimuby/config.rb +39 -0
- data/lib/gimuby/dependencies.rb +37 -0
- data/lib/gimuby/event/event.rb +29 -0
- data/lib/gimuby/event/event_manager.rb +34 -0
- data/lib/gimuby/factory.rb +275 -0
- data/lib/gimuby/genetic/archipelago/archipelago.rb +305 -0
- data/lib/gimuby/genetic/archipelago/connect_strategy/barabasi_albert_connect_strategy.rb +77 -0
- data/lib/gimuby/genetic/archipelago/connect_strategy/circle_connect_strategy.rb +11 -0
- data/lib/gimuby/genetic/archipelago/connect_strategy/connect_strategy.rb +34 -0
- data/lib/gimuby/genetic/archipelago/connect_strategy/constant_degree_connect_strategy.rb +22 -0
- data/lib/gimuby/genetic/archipelago/connect_strategy/fully_connected_connect_strategy.rb +11 -0
- data/lib/gimuby/genetic/archipelago/connect_strategy/random_connect_strategy.rb +29 -0
- data/lib/gimuby/genetic/archipelago/connect_strategy/watts_strogatz_connect_strategy.rb +63 -0
- data/lib/gimuby/genetic/archipelago/measure/clustering_coefficient_measure.rb +92 -0
- data/lib/gimuby/genetic/archipelago/measure/connected_measure.rb +64 -0
- data/lib/gimuby/genetic/archipelago/measure/diameter_measure.rb +38 -0
- data/lib/gimuby/genetic/archipelago/measure/measure.rb +7 -0
- data/lib/gimuby/genetic/archipelago/measure/shortest_paths_measure.rb +46 -0
- data/lib/gimuby/genetic/population/pick_strategy/bests_pick_strategy.rb +17 -0
- data/lib/gimuby/genetic/population/pick_strategy/pick_strategy.rb +21 -0
- data/lib/gimuby/genetic/population/pick_strategy/random_wheel_pick_strategy.rb +40 -0
- data/lib/gimuby/genetic/population/pick_strategy/tournament_pick_strategy.rb +26 -0
- data/lib/gimuby/genetic/population/population.rb +97 -0
- data/lib/gimuby/genetic/population/replace_strategy/replace_strategy.rb +9 -0
- data/lib/gimuby/genetic/population/replace_strategy/replace_worst_replace_strategy.rb +52 -0
- data/lib/gimuby/genetic/population/replace_strategy/uniform_replace_strategy.rb +48 -0
- data/lib/gimuby/genetic/solution/check_strategy/check_strategy.rb +8 -0
- data/lib/gimuby/genetic/solution/check_strategy/permutation_check_strategy.rb +37 -0
- data/lib/gimuby/genetic/solution/check_strategy/solution_space_check_strategy.rb +74 -0
- data/lib/gimuby/genetic/solution/function_based_solution.rb +64 -0
- data/lib/gimuby/genetic/solution/mutation_strategy/mutation_strategy.rb +22 -0
- data/lib/gimuby/genetic/solution/mutation_strategy/permutation_mutation_strategy.rb +17 -0
- data/lib/gimuby/genetic/solution/mutation_strategy/solution_space_mutation_strategy.rb +69 -0
- data/lib/gimuby/genetic/solution/new_generation_strategy/average_new_generation_strategy.rb +41 -0
- data/lib/gimuby/genetic/solution/new_generation_strategy/combined_new_generation_strategy.rb +27 -0
- data/lib/gimuby/genetic/solution/new_generation_strategy/cross_over_new_generation_strategy.rb +40 -0
- data/lib/gimuby/genetic/solution/new_generation_strategy/new_generation_strategy.rb +9 -0
- data/lib/gimuby/genetic/solution/new_generation_strategy/parent_range_new_generation_strategy.rb +42 -0
- data/lib/gimuby/genetic/solution/solution.rb +86 -0
- data/lib/gimuby/problem/foxholes/foxholes.rb +76 -0
- data/lib/gimuby/problem/foxholes/foxholes_solution.rb +29 -0
- data/lib/gimuby/problem/lennard_jones/lennard_jones.rb +38 -0
- data/lib/gimuby/problem/lennard_jones/lennard_jones_solution.rb +62 -0
- data/lib/gimuby/problem/rastrigin/rastrigin.rb +26 -0
- data/lib/gimuby/problem/rastrigin/rastrigin_solution.rb +35 -0
- data/lib/gimuby/problem/rosenbrock/rosenbrock.rb +14 -0
- data/lib/gimuby/problem/rosenbrock/rosenbrock_solution.rb +39 -0
- data/lib/gimuby/problem/schaffer/schaffer.rb +18 -0
- data/lib/gimuby/problem/schaffer/schaffer_solution.rb +29 -0
- data/lib/gimuby/problem/sphere/sphere.rb +9 -0
- data/lib/gimuby/problem/sphere/sphere_solution.rb +27 -0
- data/lib/gimuby/problem/step/step.rb +9 -0
- data/lib/gimuby/problem/step/step_solution.rb +29 -0
- data/lib/gimuby/problem/tsp/tsp.rb +76 -0
- data/lib/gimuby/problem/tsp/tsp_solution.rb +46 -0
- metadata +128 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'gimuby/dependencies'
|
2
|
+
require 'gimuby/genetic/population/pick_strategy/random_wheel_pick_strategy'
|
3
|
+
require 'gimuby/genetic/population/replace_strategy/replace_worst_replace_strategy'
|
4
|
+
|
5
|
+
class Population
|
6
|
+
|
7
|
+
def initialize(solutions = nil)
|
8
|
+
if solutions.nil?
|
9
|
+
@solutions = []
|
10
|
+
else
|
11
|
+
@solutions = solutions
|
12
|
+
end
|
13
|
+
@pick_strategy ||= RandomWheelPickStrategy.new
|
14
|
+
@replace_strategy ||= ReplaceWorstReplaceStrategy.new
|
15
|
+
|
16
|
+
trigger(:on_population_init)
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :solutions
|
20
|
+
attr_accessor :pick_strategy
|
21
|
+
attr_accessor :selection_rate
|
22
|
+
attr_accessor :replace_strategy
|
23
|
+
|
24
|
+
# Run a step of genetic algorithm: reproduction + mutation
|
25
|
+
def generation_step
|
26
|
+
reproduce
|
27
|
+
@solutions.each do |solution|
|
28
|
+
solution.mutate
|
29
|
+
end
|
30
|
+
trigger(:on_population_generation_step)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Replace part of the population (used by {Archipelago})
|
34
|
+
# @internal
|
35
|
+
def replace(solutions)
|
36
|
+
@solutions = @replace_strategy.replace(self, solutions)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Simply pick some solutions and make them reproduce
|
40
|
+
# @internal
|
41
|
+
def reproduce
|
42
|
+
@solutions = @replace_strategy.replace(self)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Pick some solutions
|
46
|
+
# @internal
|
47
|
+
def pick
|
48
|
+
@pick_strategy.pick(self)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Add a solution
|
52
|
+
# @param solution [Solution]
|
53
|
+
def add_solution(solution)
|
54
|
+
@solutions.push solution
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_population_size
|
58
|
+
@solutions.size
|
59
|
+
end
|
60
|
+
|
61
|
+
# Can be overridden
|
62
|
+
def get_fitness(solution)
|
63
|
+
solution.get_fitness
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_average_fitness
|
67
|
+
sum = 0
|
68
|
+
@solutions.each do |solution|
|
69
|
+
sum += solution.get_fitness
|
70
|
+
end
|
71
|
+
# Beware of that division by 0
|
72
|
+
sum / @solutions.length
|
73
|
+
end
|
74
|
+
|
75
|
+
def get_best_fitness
|
76
|
+
best_solution = get_best_solution
|
77
|
+
best_solution.get_fitness
|
78
|
+
end
|
79
|
+
|
80
|
+
def get_best_solution
|
81
|
+
best_solution = @solutions.min_by do |solution|
|
82
|
+
solution.get_fitness
|
83
|
+
end
|
84
|
+
best_solution
|
85
|
+
end
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
def trigger(event_type)
|
90
|
+
event_data = {:population => self}
|
91
|
+
get_event_manager.trigger_event(event_type, event_data)
|
92
|
+
end
|
93
|
+
|
94
|
+
def get_event_manager
|
95
|
+
$dependencies.event_manager
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'gimuby/genetic/population/replace_strategy/replace_strategy'
|
2
|
+
|
3
|
+
class ReplaceWorstReplaceStrategy < ReplaceStrategy
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@replace_proportion = 50.to_f / 100.to_f
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_accessor :replace_proportion
|
10
|
+
|
11
|
+
def replace(population, selected = nil)
|
12
|
+
solutions = population.solutions.clone
|
13
|
+
|
14
|
+
wished_length = solutions.length
|
15
|
+
solutions.sort! do |x, y|
|
16
|
+
x_fitness = population.get_fitness(x)
|
17
|
+
y_fitness = population.get_fitness(y)
|
18
|
+
x_fitness <=> y_fitness
|
19
|
+
end
|
20
|
+
if selected.nil?
|
21
|
+
selected = population.pick
|
22
|
+
end
|
23
|
+
number_to_remove = (wished_length * @replace_proportion).floor
|
24
|
+
if number_to_remove == wished_length
|
25
|
+
number_to_remove -= 1
|
26
|
+
end
|
27
|
+
solutions.slice!(-number_to_remove, number_to_remove)
|
28
|
+
|
29
|
+
while solutions.length < wished_length
|
30
|
+
begin
|
31
|
+
random_index1 = rand(selected.length)
|
32
|
+
random_index2 = rand(selected.length)
|
33
|
+
end while ((random_index1 == random_index2) && (selected.length != 1))
|
34
|
+
new_solutions = reproduce(selected[random_index1],
|
35
|
+
selected[random_index2])
|
36
|
+
new_solutions.each do |new_solution|
|
37
|
+
solutions.push(new_solution)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
solutions.slice!(wished_length) # we could have one that should be dropped
|
42
|
+
solutions
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def reproduce(solution1, solution2)
|
48
|
+
solution1.reproduce(solution1, solution2)
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'gimuby/genetic/population/replace_strategy/replace_strategy'
|
2
|
+
|
3
|
+
class UniformReplaceStrategy < ReplaceStrategy
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@replace_proportion = 50.to_f / 100.to_f
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_accessor :replace_proportion
|
10
|
+
|
11
|
+
def replace(population, selected = nil)
|
12
|
+
solutions = population.solutions.clone
|
13
|
+
|
14
|
+
wished_length = solutions.length
|
15
|
+
solutions.shuffle!
|
16
|
+
if selected.nil?
|
17
|
+
selected = population.pick
|
18
|
+
end
|
19
|
+
number_to_remove = (wished_length * @replace_proportion).floor
|
20
|
+
if number_to_remove == wished_length
|
21
|
+
number_to_remove -= 1
|
22
|
+
end
|
23
|
+
solutions.slice!(-number_to_remove, number_to_remove)
|
24
|
+
|
25
|
+
while solutions.length < wished_length
|
26
|
+
begin
|
27
|
+
random_index1 = rand(selected.length)
|
28
|
+
random_index2 = rand(selected.length)
|
29
|
+
end while ((random_index1 == random_index2) && (selected.length != 1))
|
30
|
+
new_solutions = reproduce(selected[random_index1],
|
31
|
+
selected[random_index2])
|
32
|
+
new_solutions.each do |new_solution|
|
33
|
+
solutions.push(new_solution)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
solutions.slice!(wished_length) # we could have one that should be dropped
|
38
|
+
solutions
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
def reproduce(solution1, solution2)
|
44
|
+
solution1.reproduce(solution1, solution2)
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'gimuby/genetic/solution/check_strategy/check_strategy'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Permutation goes represented as permutation from [0, ... l] indexes
|
5
|
+
class PermutationCheckStrategy < CheckStrategy
|
6
|
+
|
7
|
+
def check(solution_representation)
|
8
|
+
permutation = solution_representation
|
9
|
+
expected_elements = *(0..permutation.length - 1)
|
10
|
+
duplicate = []
|
11
|
+
missing = []
|
12
|
+
expected_elements.each do |element|
|
13
|
+
match = permutation.select do |concreteElement|
|
14
|
+
concreteElement == element
|
15
|
+
end
|
16
|
+
case match.length <=> 1
|
17
|
+
when -1 then
|
18
|
+
missing.push(element)
|
19
|
+
when 1 then
|
20
|
+
duplicate.push(element)
|
21
|
+
else
|
22
|
+
# do nothing
|
23
|
+
end
|
24
|
+
end
|
25
|
+
missing.shuffle!
|
26
|
+
duplicate.each do |to_remove|
|
27
|
+
to_insert = missing.pop()
|
28
|
+
ind = permutation.index(to_remove)
|
29
|
+
permutation[ind] = to_insert
|
30
|
+
end
|
31
|
+
unless missing.empty?
|
32
|
+
solution_representation = check(solution_representation)
|
33
|
+
end
|
34
|
+
solution_representation
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'gimuby/genetic/solution/check_strategy/check_strategy'
|
2
|
+
|
3
|
+
# Permutation goes represented as permutation from [0, ... l] indexes
|
4
|
+
class SolutionSpaceCheckStrategy < CheckStrategy
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@default_min = nil
|
8
|
+
@default_max = nil
|
9
|
+
@mins = {}
|
10
|
+
@maxs = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def check(solution_representation)
|
14
|
+
solution_representation.each_index do |index|
|
15
|
+
value = solution_representation[index]
|
16
|
+
|
17
|
+
if value.class == Array
|
18
|
+
value = check(value)
|
19
|
+
else
|
20
|
+
min = get_min(index)
|
21
|
+
unless min.nil?
|
22
|
+
if value < min
|
23
|
+
value = min
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
max = get_max(index)
|
28
|
+
unless max.nil?
|
29
|
+
if value > max
|
30
|
+
value = max
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
solution_representation[index] = value
|
36
|
+
end
|
37
|
+
|
38
|
+
solution_representation
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_min(min, index = nil)
|
42
|
+
if index.nil?
|
43
|
+
@default_min = min
|
44
|
+
else
|
45
|
+
@mins[index] = min
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_max(max, index = nil)
|
50
|
+
if index.nil?
|
51
|
+
@default_max = max
|
52
|
+
else
|
53
|
+
@maxs[index] = max
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def get_min(index)
|
60
|
+
min = @default_min
|
61
|
+
if @mins.has_key?(index)
|
62
|
+
min = @mins[index]
|
63
|
+
end
|
64
|
+
min
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_max(index)
|
68
|
+
max = @default_max
|
69
|
+
if @maxs.has_key?(index)
|
70
|
+
max = @maxs[index]
|
71
|
+
end
|
72
|
+
max
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'gimuby/genetic/solution/solution'
|
2
|
+
require 'gimuby/genetic/solution/check_strategy/solution_space_check_strategy'
|
3
|
+
require 'gimuby/genetic/solution/new_generation_strategy/combined_new_generation_strategy'
|
4
|
+
require 'gimuby/genetic/solution/new_generation_strategy/parent_range_new_generation_strategy'
|
5
|
+
require 'gimuby/genetic/solution/new_generation_strategy/cross_over_new_generation_strategy'
|
6
|
+
require 'gimuby/genetic/solution/new_generation_strategy/average_new_generation_strategy'
|
7
|
+
require 'gimuby/genetic/solution/mutation_strategy/solution_space_mutation_strategy'
|
8
|
+
|
9
|
+
class FunctionBasedSolution < Solution
|
10
|
+
|
11
|
+
def initialize(x_values = nil)
|
12
|
+
super(x_values)
|
13
|
+
|
14
|
+
@check_strategy = SolutionSpaceCheckStrategy.new
|
15
|
+
@check_strategy.set_min(get_x_value_min)
|
16
|
+
@check_strategy.set_max(get_x_value_max)
|
17
|
+
|
18
|
+
@new_generation_strategy = CombinedNewGenerationStrategy.new
|
19
|
+
@new_generation_strategy.add_strategy(ParentRangeNewGenerationStrategy.new)
|
20
|
+
@new_generation_strategy.add_strategy(CrossOverNewGenerationStrategy.new)
|
21
|
+
@new_generation_strategy.add_strategy(AverageNewGenerationStrategy.new)
|
22
|
+
|
23
|
+
@mutation_strategy = SolutionSpaceMutationStrategy.new
|
24
|
+
@mutation_strategy.set_min(get_x_value_min)
|
25
|
+
@mutation_strategy.set_max(get_x_value_max)
|
26
|
+
end
|
27
|
+
|
28
|
+
def evaluate
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_solution_representation
|
33
|
+
@x_values.clone
|
34
|
+
end
|
35
|
+
|
36
|
+
def set_solution_representation(x_values)
|
37
|
+
@x_values = x_values.clone
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def init_representation
|
43
|
+
@x_values = []
|
44
|
+
dimension = get_dimension_number
|
45
|
+
dimension.times do |_|
|
46
|
+
range = get_x_value_max - get_x_value_min
|
47
|
+
x_value = (rand() * range) + get_x_value_min
|
48
|
+
@x_values.push(x_value)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_x_value_min
|
53
|
+
raise NotImplementedError
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_x_value_max
|
57
|
+
raise NotImplementedError
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_dimension_number
|
61
|
+
raise NotImplementedError
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class MutationStrategy
|
4
|
+
|
5
|
+
def initialize(mutation_rate = 0.01)
|
6
|
+
@mutation_rate = mutation_rate
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_accessor :mutation_rate
|
10
|
+
|
11
|
+
def mutate(solution)
|
12
|
+
if rand < @mutation_rate
|
13
|
+
perform_mutation(solution)
|
14
|
+
solution.reset_fitness_state
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def perform_mutation(solution)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'gimuby/genetic/solution/mutation_strategy/mutation_strategy'
|
2
|
+
|
3
|
+
class PermutationMutationStrategy < MutationStrategy
|
4
|
+
|
5
|
+
def perform_mutation(solution)
|
6
|
+
permutation = solution.get_solution_representation
|
7
|
+
begin
|
8
|
+
index1 = rand(permutation.length)
|
9
|
+
index2 = rand(permutation.length)
|
10
|
+
end while index1 == index2
|
11
|
+
tmp = permutation[index1]
|
12
|
+
permutation[index1] = permutation[index2]
|
13
|
+
permutation[index2] = tmp
|
14
|
+
solution.set_solution_representation(permutation)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'gimuby/genetic/solution/mutation_strategy/mutation_strategy'
|
2
|
+
|
3
|
+
class SolutionSpaceMutationStrategy < MutationStrategy
|
4
|
+
|
5
|
+
def initialize(mutation_rate = 0.01)
|
6
|
+
super(mutation_rate)
|
7
|
+
@default_min = nil
|
8
|
+
@default_max = nil
|
9
|
+
@mins = {}
|
10
|
+
@maxs = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform_mutation(solution)
|
14
|
+
x_values = solution.get_solution_representation
|
15
|
+
x_values = perform_mutation_from_representation(x_values)
|
16
|
+
solution.set_solution_representation(x_values)
|
17
|
+
end
|
18
|
+
|
19
|
+
def set_min(min, index = nil)
|
20
|
+
if index.nil?
|
21
|
+
@default_min = min
|
22
|
+
else
|
23
|
+
@mins[index] = min
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_max(max, index = nil)
|
28
|
+
if index.nil?
|
29
|
+
@default_max = max
|
30
|
+
else
|
31
|
+
@maxs[index] = max
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def perform_mutation_from_representation(x_values)
|
38
|
+
index = rand(x_values.length)
|
39
|
+
|
40
|
+
x_value = x_values[index]
|
41
|
+
if x_value.class == Array
|
42
|
+
x_value = perform_mutation_from_representation(x_value)
|
43
|
+
else
|
44
|
+
min = get_min(index)
|
45
|
+
max = get_max(index)
|
46
|
+
range = max - min
|
47
|
+
x_value = rand() * range + min
|
48
|
+
end
|
49
|
+
x_values[index] = x_value
|
50
|
+
x_values
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_min(index)
|
54
|
+
min = @default_min
|
55
|
+
if @mins.has_key?(index)
|
56
|
+
min = @mins[index]
|
57
|
+
end
|
58
|
+
min
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_max(index)
|
62
|
+
max = @default_max
|
63
|
+
if @maxs.has_key?(index)
|
64
|
+
max = @maxs[index]
|
65
|
+
end
|
66
|
+
max
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|