|
| 1 | +"""Random mutation respects NumPy integer limits before converting candidates.""" |
| 2 | + |
| 3 | +import numpy |
| 4 | +import pytest |
| 5 | + |
| 6 | +import pygad |
| 7 | + |
| 8 | + |
| 9 | +def fitness_func(ga_instance, solution, solution_index): |
| 10 | + return float(sum(int(value) for value in solution)) |
| 11 | + |
| 12 | + |
| 13 | +def make_ga(**options): |
| 14 | + parameters = dict(num_generations=2, num_parents_mating=2, |
| 15 | + fitness_func=fitness_func, sol_per_pop=4, num_genes=3, |
| 16 | + gene_type=numpy.uint8, init_range_low=0, init_range_high=3, |
| 17 | + random_seed=7, suppress_warnings=True) |
| 18 | + parameters.update(options) |
| 19 | + return pygad.GA(**parameters) |
| 20 | + |
| 21 | + |
| 22 | +integer_types = [numpy.int8, numpy.int16, numpy.int32, numpy.int64, |
| 23 | + numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64] |
| 24 | + |
| 25 | + |
| 26 | +@pytest.mark.parametrize("gene_type", integer_types) |
| 27 | +@pytest.mark.parametrize("mutation_by_replacement", [False, True]) |
| 28 | +def test_mutation_clamps_scalar_and_array_values_at_both_integer_limits(gene_type, mutation_by_replacement): |
| 29 | + ga_instance = make_ga(gene_type=gene_type) |
| 30 | + type_limits = numpy.iinfo(gene_type) |
| 31 | + for boundary, offset in [(int(type_limits.min), -1), (int(type_limits.max), 1)]: |
| 32 | + random_value = boundary + offset if mutation_by_replacement else offset |
| 33 | + converted = ga_instance.mutation_change_gene_dtype_and_round( |
| 34 | + random_value, 0, gene_type(boundary), mutation_by_replacement) |
| 35 | + assert type(converted) is gene_type |
| 36 | + assert int(converted) == boundary |
| 37 | + |
| 38 | + random_values = numpy.array([[random_value, boundary if mutation_by_replacement else 0]], dtype=object) |
| 39 | + original_values = random_values.copy() |
| 40 | + converted = ga_instance.mutation_change_gene_dtype_and_round( |
| 41 | + random_values, 0, gene_type(boundary), mutation_by_replacement) |
| 42 | + assert converted.dtype == numpy.dtype(gene_type) |
| 43 | + numpy.testing.assert_array_equal(converted, [[boundary, boundary]]) |
| 44 | + numpy.testing.assert_array_equal(random_values, original_values) |
| 45 | + |
| 46 | + |
| 47 | +@pytest.mark.parametrize("random_values", [numpy.array(-1), numpy.array([-1, 0], dtype=object), |
| 48 | + numpy.empty((0, 2), dtype=object)]) |
| 49 | +def test_unsigned_mutation_preserves_candidate_shape(random_values): |
| 50 | + ga_instance = make_ga() |
| 51 | + converted = ga_instance.mutation_change_gene_dtype_and_round( |
| 52 | + random_values, 0, numpy.uint8(0), False) |
| 53 | + assert numpy.shape(converted) == random_values.shape |
| 54 | + numpy.testing.assert_array_equal(converted, numpy.zeros(random_values.shape, dtype=numpy.uint8)) |
| 55 | + |
| 56 | + |
| 57 | +@pytest.mark.parametrize("gene_type", integer_types) |
| 58 | +def test_fractional_offsets_are_added_before_integer_truncation(gene_type): |
| 59 | + ga_instance = make_ga(gene_type=gene_type) |
| 60 | + converted = ga_instance.mutation_change_gene_dtype_and_round( |
| 61 | + [-0.2, 0.2, 1.9], 0, gene_type(1), False) |
| 62 | + numpy.testing.assert_array_equal(converted, [0, 1, 2]) |
| 63 | + |
| 64 | + |
| 65 | +@pytest.mark.parametrize("gene_type", [int, float, object, numpy.float32]) |
| 66 | +@pytest.mark.parametrize("mutation_by_replacement", [False, True]) |
| 67 | +def test_other_gene_types_keep_values_outside_narrow_integer_limits(gene_type, mutation_by_replacement): |
| 68 | + ga_instance = make_ga(gene_type=gene_type) |
| 69 | + random_values = [-2**20, 2**20] |
| 70 | + converted = ga_instance.mutation_change_gene_dtype_and_round( |
| 71 | + random_values, 0, 0, mutation_by_replacement) |
| 72 | + numpy.testing.assert_array_equal(converted, random_values) |
| 73 | + |
| 74 | + |
| 75 | +@pytest.mark.parametrize("gene_type", [int, object]) |
| 76 | +def test_python_integer_mutation_keeps_unbounded_scalar_values(gene_type): |
| 77 | + ga_instance = make_ga(gene_type=gene_type) |
| 78 | + converted = ga_instance.mutation_change_gene_dtype_and_round(1, 0, 2**80, False) |
| 79 | + assert converted == 2**80 + 1 |
| 80 | + |
| 81 | + |
| 82 | +@pytest.mark.parametrize("random_seed", range(20)) |
| 83 | +def test_reported_unsigned_population_runs_with_default_mutation(random_seed): |
| 84 | + ga_instance = make_ga(num_generations=30, num_parents_mating=4, |
| 85 | + sol_per_pop=10, num_genes=6, random_seed=random_seed) |
| 86 | + ga_instance.run() |
| 87 | + assert ga_instance.generations_completed == 30 |
| 88 | + assert ga_instance.population.dtype == numpy.dtype(numpy.uint8) |
| 89 | + |
| 90 | + |
| 91 | +@pytest.mark.parametrize("mutation_type", ['random', 'adaptive']) |
| 92 | +@pytest.mark.parametrize("use_probability", [False, True]) |
| 93 | +@pytest.mark.parametrize("gene_space", [None, [None, None, None], [[None, 1], [None, 1], [None, 1]]]) |
| 94 | +@pytest.mark.parametrize("mutation_by_replacement", [False, True]) |
| 95 | +def test_mutation_backends_share_integer_limits(mutation_type, use_probability, gene_space, mutation_by_replacement): |
| 96 | + parameters = dict(mutation_type=mutation_type, gene_space=gene_space, |
| 97 | + mutation_by_replacement=mutation_by_replacement, |
| 98 | + gene_type=[numpy.uint8, numpy.int8, numpy.uint64], |
| 99 | + initial_population=[[0, 127, 2**64 - 1]] * 4, |
| 100 | + random_mutation_min_val=[-2, 1, 1], |
| 101 | + random_mutation_max_val=[-1, 2, 2], |
| 102 | + crossover_type=None, keep_elitism=0, keep_parents=0) |
| 103 | + if use_probability: |
| 104 | + parameters['mutation_probability'] = [1.0, 1.0] if mutation_type == 'adaptive' else 1.0 |
| 105 | + else: |
| 106 | + parameters['mutation_num_genes'] = [3, 3] if mutation_type == 'adaptive' else 3 |
| 107 | + ga_instance = make_ga(**parameters) |
| 108 | + ga_instance.run() |
| 109 | + for solution in ga_instance.population: |
| 110 | + for gene_index, gene_type in enumerate([numpy.uint8, numpy.int8, numpy.uint64]): |
| 111 | + assert type(solution[gene_index]) is gene_type |
| 112 | + assert int(solution[0]) in [0, 1] |
| 113 | + if gene_space is None: |
| 114 | + numpy.testing.assert_array_equal(solution, [0, 1, 1] if mutation_by_replacement else [0, 127, 2**64 - 1]) |
| 115 | + |
| 116 | + |
| 117 | +def test_constraints_receive_clamped_mutation_candidates(): |
| 118 | + candidate_batches = [] |
| 119 | + |
| 120 | + def gene_constraint(solution, values): |
| 121 | + candidate_batches.append(values.copy()) |
| 122 | + assert all(type(value) is numpy.uint8 and int(value) == 0 for value in values) |
| 123 | + return values |
| 124 | + |
| 125 | + ga_instance = make_ga(initial_population=[[0, 0, 0]] * 4, |
| 126 | + gene_constraint=[gene_constraint] * 3, |
| 127 | + random_mutation_min_val=-2, random_mutation_max_val=-1, |
| 128 | + mutation_num_genes=3, keep_elitism=0, keep_parents=0) |
| 129 | + ga_instance.run() |
| 130 | + assert candidate_batches |
| 131 | + numpy.testing.assert_array_equal(ga_instance.population, 0) |
| 132 | + |
| 133 | + |
| 134 | +@pytest.mark.parametrize("mutation_by_replacement", [False, True]) |
| 135 | +def test_duplicate_repair_uses_representable_mutation_candidates(mutation_by_replacement): |
| 136 | + ga_instance = make_ga(allow_duplicate_genes=False) |
| 137 | + solution, duplicate_indices, num_unsolved_duplicates = ga_instance.solve_duplicate_genes_randomly( |
| 138 | + numpy.array([0, 0, 255], dtype=numpy.uint8), -2, 2, |
| 139 | + mutation_by_replacement=mutation_by_replacement, gene_type=ga_instance.gene_type) |
| 140 | + assert not duplicate_indices |
| 141 | + assert num_unsolved_duplicates == 0 |
| 142 | + assert solution.dtype == numpy.dtype(numpy.uint8) |
| 143 | + numpy.testing.assert_array_equal(numpy.sort(solution), [0, 1, 255]) |
0 commit comments