From 91698ba6c1bedf5de0bb024c66f1efc1648713e6 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 18:35:02 -0500 Subject: [PATCH 01/73] Initial commit --- src/ch07/__init__.py | 0 src/ch07/c1_breed_rats.py | 48 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/ch07/__init__.py create mode 100644 src/ch07/c1_breed_rats.py diff --git a/src/ch07/__init__.py b/src/ch07/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py new file mode 100644 index 0000000..63a75ce --- /dev/null +++ b/src/ch07/c1_breed_rats.py @@ -0,0 +1,48 @@ +"""Efficiently breed rats to an average weight of 50000 grams. + +Use genetic algorithm on a mixed population of male and female rats. + +Running as a program will output simulation in :func:`main` and the time (in +seconds) it took to run the simulation. + +Weights and number of each gender vary and can be set by modifying the +following: + +Attributes: + +""" +import time +import random +import statistics + + +def populate(pop_total, minimum_wt, maximum_wt, mode_wt): + + +def measure(population, target_wt): + + +def select(population, to_keep): + + +def crossover(males, females, litter_sz): + + +def mutate(children, mut_odds, mut_min, mut_max): + + +def main(): + """Simulate genetic algorithm. + + After initializing population, repeat cycle of measure, select, crossover, + and mutate to meet goal of 50000 grams. + + """ + + +if __name__ == '__main__': + start_time = time.time() + main() + end_time = time.time() + duration = end_time - start_time + print("\nRuntime for this program was {} seconds.".format(duration)) From 992d252f23f71fa65950abe23d315689c2a2af09 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 19:23:55 -0500 Subject: [PATCH 02/73] Preemptively add module docstring --- src/ch07/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ch07/__init__.py b/src/ch07/__init__.py index e69de29..a16958a 100644 --- a/src/ch07/__init__.py +++ b/src/ch07/__init__.py @@ -0,0 +1 @@ +"""Chapter 7.""" From 79e0456d07eee421923ebb2bbe10fa923bee9ac6 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 19:25:15 -0500 Subject: [PATCH 03/73] Add constants --- src/ch07/c1_breed_rats.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 63a75ce..bd68eff 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -15,6 +15,21 @@ import random import statistics +# CONSTANTS (weights in grams) +TARGET_WT = 50000 # Target weight in grams. +NUM_MALES = 4 # Number of male rats in population. +NUM_FEMALES = 16 # Number of female rats in population. +INIT_MIN_WT = 200 # Minimum weight of adult rat in initial population. +INIT_MAX_WT = 600 # Maximum weight of adult rat in initial population. +INIT_MALE_MODE_WT = 300 # Most common adult male rat weight in initial population. +INIT_FEMALE_MODE_WT = 250 # Most common adult female rat weight in initial population. +MUT_ODDS = 0.01 # Probability of a mutation occurring in a pup. +MUT_MIN = 0.5 # Scalar on rat weight of least beneficial mutation. +MUT_MAX = 1.2 # Scalar on rat weight of most beneficial mutation. +LITTER_SZ = 8 # Number of pups per pair of breeding rats. +LITTERS_PER_YR = 10 # Number of litters per year per pair of breeding rats. +GEN_LIMIT = 500 # Generational cutoff to stop breeding program. + def populate(pop_total, minimum_wt, maximum_wt, mode_wt): From 0afabb45e124a13f39e75dbd1ca21d52f81e1231 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 19:25:47 -0500 Subject: [PATCH 04/73] Add constants as attributes to module docstring --- src/ch07/c1_breed_rats.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index bd68eff..0cf008c 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -9,6 +9,19 @@ following: Attributes: + TARGET_WT (int): Target weight in grams. + NUM_MALES (int): Number of male rats in population. + NUM_FEMALES (int): Number of female rats in population. + INIT_MIN_WT (int): Minimum weight of adult rat in initial population. + INIT_MAX_WT (int): Maximum weight of adult rat in initial population. + INIT_MALE_MODE_WT (int): Most common adult male rat weight in initial population. + INIT_FEMALE_MODE_WT (int): Most common adult female rat weight in initial population. + MUT_ODDS (float): Probability of a mutation occurring in a pup. + MUT_MIN (float): Scalar on rat weight of least beneficial mutation. + MUT_MAX (float): Scalar on rat weight of most beneficial mutation. + LITTER_SZ (int): Number of pups per pair of breeding rats. + LITTERS_PER_YR (int): Number of litters per year per pair of breeding rats. + GEN_LIMIT (int): Generational cutoff to stop breeding program. """ import time From 7de78a00f5beacc338b91f9483ab11646ae85dc3 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 19:26:21 -0500 Subject: [PATCH 05/73] Complete populate --- src/ch07/c1_breed_rats.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 0cf008c..9bcde84 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -44,7 +44,25 @@ GEN_LIMIT = 500 # Generational cutoff to stop breeding program. -def populate(pop_total, minimum_wt, maximum_wt, mode_wt): +def populate(pop_total: int, minimum_wt: int, + maximum_wt: int, mode_wt: int) -> list: + """Generate population with a triangular distribution of weights. + + Use :py:mod:`~random.triangular` to generate a population with a triangular + distribution of weights based on *minimum_wt*, *maximum_wt*, and *mode_wt*. + + Args: + pop_total (int): Total number of rats in population. + minimum_wt (int): Minimum weight of adult rat in initial population. + maximum_wt (int): Maximum weight of adult rat in initial population. + mode_wt (int): Most common adult rat weight in initial population. + + Returns: + List of triangularly distributed weights of a given rat population. + + """ + return [int(random.triangular(minimum_wt, maximum_wt, mode_wt)) + for _ in range(pop_total)] def measure(population, target_wt): From 93e1be6c9be24059f6b294492e2b37428163cbc9 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 19:27:10 -0500 Subject: [PATCH 06/73] Populate empty functions with pass to avoid unittest errors --- src/ch07/c1_breed_rats.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 9bcde84..8002984 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -66,15 +66,19 @@ def populate(pop_total: int, minimum_wt: int, def measure(population, target_wt): + pass def select(population, to_keep): + pass def crossover(males, females, litter_sz): + pass def mutate(children, mut_odds, mut_min, mut_max): + pass def main(): @@ -84,6 +88,7 @@ def main(): and mutate to meet goal of 50000 grams. """ + pass if __name__ == '__main__': From 2469f77f1c2df696948e43db969dc7c8b6e187af Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 19:44:31 -0500 Subject: [PATCH 07/73] Fix line-too-long per pylint C0301 --- src/ch07/c1_breed_rats.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 8002984..7dcf511 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -14,8 +14,10 @@ NUM_FEMALES (int): Number of female rats in population. INIT_MIN_WT (int): Minimum weight of adult rat in initial population. INIT_MAX_WT (int): Maximum weight of adult rat in initial population. - INIT_MALE_MODE_WT (int): Most common adult male rat weight in initial population. - INIT_FEMALE_MODE_WT (int): Most common adult female rat weight in initial population. + INIT_MALE_MODE_WT (int): Most common adult male rat weight in initial + population. + INIT_FEMALE_MODE_WT (int): Most common adult female rat weight in initial + population. MUT_ODDS (float): Probability of a mutation occurring in a pup. MUT_MIN (float): Scalar on rat weight of least beneficial mutation. MUT_MAX (float): Scalar on rat weight of most beneficial mutation. @@ -34,8 +36,8 @@ NUM_FEMALES = 16 # Number of female rats in population. INIT_MIN_WT = 200 # Minimum weight of adult rat in initial population. INIT_MAX_WT = 600 # Maximum weight of adult rat in initial population. -INIT_MALE_MODE_WT = 300 # Most common adult male rat weight in initial population. -INIT_FEMALE_MODE_WT = 250 # Most common adult female rat weight in initial population. +INIT_MALE_MODE_WT = 300 # Most common adult male rat weight. +INIT_FEMALE_MODE_WT = 250 # Most common adult female rat weight. MUT_ODDS = 0.01 # Probability of a mutation occurring in a pup. MUT_MIN = 0.5 # Scalar on rat weight of least beneficial mutation. MUT_MAX = 1.2 # Scalar on rat weight of most beneficial mutation. From 0a4bb12c0420999200b7df3536cfbe151a1f02e1 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 19:45:42 -0500 Subject: [PATCH 08/73] Initialize with TestBreedRats --- tests/test_chapter07.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/test_chapter07.py diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py new file mode 100644 index 0000000..c86f1c2 --- /dev/null +++ b/tests/test_chapter07.py @@ -0,0 +1,27 @@ +"""Test Chapter 7.""" +import unittest.mock +from random import Random + +import src.ch07.c1_breed_rats as breed_rats + + +class TestBreedRats(unittest.TestCase): + """Test Breed Rats.""" + + @classmethod + def setUpClass(cls): + """Configure attributes for use in this class only.""" + cls.random = Random(512) + + @unittest.mock.patch('src.ch07.c1_breed_rats.random') + def test_populate(self, mock_random): + """Test populate.""" + # Patch random.triangular to use non-random seed. + mock_random.triangular._mock_side_effect = self.random.triangular + test_pop = breed_rats.populate(10, 100, 300, 200) + expected_pop = [119, 193, 181, 190, 261, 190, 158, 169, 109, 229] + self.assertListEqual(test_pop, expected_pop) + + +if __name__ == '__main__': + unittest.main() From 679597d2918bbb9311a5b609b7e2a8d320852f57 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 19:57:22 -0500 Subject: [PATCH 09/73] Complete measure --- src/ch07/c1_breed_rats.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 7dcf511..77f1521 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -67,8 +67,28 @@ def populate(pop_total: int, minimum_wt: int, for _ in range(pop_total)] -def measure(population, target_wt): - pass +def measure(population: dict, target_wt: int) -> float: + """Measure average weight of population against target. + + Calculate mean weight of **population** and divide by **target_wt** to + determine if goal has been met. + + Args: + population (dict): Dictionary of lists with ``males`` and ``females`` + as keys and specimen weight in grams as values. + target_wt (int): Target average weight of population in grams. + + Returns: + :py:obj:`float` representing decimal percentage of completion where a + value of ``1`` is ``100%``, or complete. + + """ + # Combine genders into same list for measurement. + total = [] + for value in population.values(): + total.extend(value) + mean = statistics.mean(total) + return mean / target_wt def select(population, to_keep): From 4bd6a746796abf7703d21cb08f232b752fa66e28 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 20:09:44 -0500 Subject: [PATCH 10/73] Add test_measure to TestBreedRats --- tests/test_chapter07.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index c86f1c2..fb8fb1d 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -22,6 +22,15 @@ def test_populate(self, mock_random): expected_pop = [119, 193, 181, 190, 261, 190, 158, 169, 109, 229] self.assertListEqual(test_pop, expected_pop) + def test_measure(self): + """Test measure.""" + population = { + 'males': [219, 293, 281, 290, 361, 290, 258, 269, 309, 329], + 'females': [119, 193, 181, 190, 261, 190, 158, 169, 109, 229] + } + completion = breed_rats.measure(population, 500) + self.assertEqual(completion, 0.4698) + if __name__ == '__main__': unittest.main() From 3f91115fe91f32c96e6e3fe188421ba5015580f2 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 20:33:17 -0500 Subject: [PATCH 11/73] Complete select --- src/ch07/c1_breed_rats.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 77f1521..42ea54e 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -91,8 +91,21 @@ def measure(population: dict, target_wt: int) -> float: return mean / target_wt -def select(population, to_keep): - pass +def select(population: list, to_keep: int) -> list: + """Select largest members of population. + + Sort members in descending order, and then keep largest members up to + **to_keep**. + + Args: + population (list): List of members (weights in grams) in population. + to_keep (int): Number of members in population to keep. + + Returns: + List of length **to_keep** of largest members of **population**. + + """ + return sorted(population, reverse=True)[:to_keep] def crossover(males, females, litter_sz): From 3ff510854d2483c28d516cd71e7921c520bcca03 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 20:33:50 -0500 Subject: [PATCH 12/73] Add test_select to TestBreedRats --- tests/test_chapter07.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index fb8fb1d..64d5c7c 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -31,6 +31,26 @@ def test_measure(self): completion = breed_rats.measure(population, 500) self.assertEqual(completion, 0.4698) + def test_select(self): + """Test select.""" + population = [219, 293, 281, 290, 361, 290, 258, 269, 309, 329] + + # Test even numbered populations. + test_population = breed_rats.select(population, 2) + expected_population = [361, 329] + self.assertListEqual(test_population, expected_population) + test_population = breed_rats.select(population, 4) + expected_population = [361, 329, 309, 293] + self.assertListEqual(test_population, expected_population) + + # Test odd numbered populations. + test_population = breed_rats.select(population, 3) + expected_population = [361, 329, 309] + self.assertListEqual(test_population, expected_population) + test_population = breed_rats.select(population, 5) + expected_population = [361, 329, 309, 293, 290] + self.assertListEqual(test_population, expected_population) + if __name__ == '__main__': unittest.main() From 5133632c780a423377d6a4fda3bfd4ae1eaddb91 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 20:37:48 -0500 Subject: [PATCH 13/73] Move timing checks to main function --- src/ch07/c1_breed_rats.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 42ea54e..2d24d57 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -123,12 +123,12 @@ def main(): and mutate to meet goal of 50000 grams. """ + start_time = time.time() pass + end_time = time.time() + duration = end_time - start_time + print("\nRuntime for this program was {} seconds.".format(duration)) if __name__ == '__main__': - start_time = time.time() main() - end_time = time.time() - duration = end_time - start_time - print("\nRuntime for this program was {} seconds.".format(duration)) From 4ba265c9623b90cb47169f3570697fc0f8f3dbfb Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 20:38:02 -0500 Subject: [PATCH 14/73] Update module docstring --- src/ch07/c1_breed_rats.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 2d24d57..b4bccfb 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -2,8 +2,8 @@ Use genetic algorithm on a mixed population of male and female rats. -Running as a program will output simulation in :func:`main` and the time (in -seconds) it took to run the simulation. +Running :func:`main` will output demonstration simulation and the time (in +seconds) it took to run. Weights and number of each gender vary and can be set by modifying the following: From 1d1183cdf9fa1677c51eb29141e1a6b0657ad2e4 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Wed, 9 Oct 2019 20:40:48 -0500 Subject: [PATCH 15/73] Convert print statement to f-string in main --- src/ch07/c1_breed_rats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index b4bccfb..3d05202 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -127,7 +127,7 @@ def main(): pass end_time = time.time() duration = end_time - start_time - print("\nRuntime for this program was {} seconds.".format(duration)) + print(f'Runtime for this program was {duration} seconds.') if __name__ == '__main__': From b7f25589cedf39a8666858e5fd57550137387660 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Thu, 10 Oct 2019 14:02:42 -0500 Subject: [PATCH 16/73] Set random seed in unit tests rather than in class for TestBreedRats --- tests/test_chapter07.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 64d5c7c..e6849d6 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -11,12 +11,13 @@ class TestBreedRats(unittest.TestCase): @classmethod def setUpClass(cls): """Configure attributes for use in this class only.""" - cls.random = Random(512) + cls.random = Random() @unittest.mock.patch('src.ch07.c1_breed_rats.random') def test_populate(self, mock_random): """Test populate.""" # Patch random.triangular to use non-random seed. + self.random.seed(512) mock_random.triangular._mock_side_effect = self.random.triangular test_pop = breed_rats.populate(10, 100, 300, 200) expected_pop = [119, 193, 181, 190, 261, 190, 158, 169, 109, 229] From 80ab8de148a3e737057cc66ce5fa1426867c2d65 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Thu, 10 Oct 2019 16:25:00 -0500 Subject: [PATCH 17/73] Complete crossover --- src/ch07/c1_breed_rats.py | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 3d05202..0a1a548 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -108,8 +108,43 @@ def select(population: list, to_keep: int) -> list: return sorted(population, reverse=True)[:to_keep] -def crossover(males, females, litter_sz): - pass +def crossover(population: dict, litter_sz: int) -> dict: + """Crossover genes among members (weights) of a population. + + Breed population where each breeding pair produces a litter + of **litter_sz** pups. Pup's gender is assigned randomly. + + To accommodate mismatched pairs, breeding pairs are selected randomly, + and once paired, females are removed from the breeding pool while + males remain. + + Args: + population (dict): Dictionary of lists with ``males`` and ``females`` + as keys and specimen weight in grams as values. + litter_sz (int): Number of pups per breeding pair of rats. + + Returns: + Dictionary of lists with ``males`` and ``females`` as keys and + pup weight in grams as values. + + """ + males = population['males'] + females = population['females'].copy() + litter = {'males': [], 'females': []} + while females: + male = random.choice(males) + female = random.choice(females) + for pup in range(litter_sz): + pup = random.randint(female, male) + if random.choice([0, 1]): + litter['males'].append(pup) + else: + litter['females'].append(pup) + females.remove(female) + # Sort output for test consistency. + for value in litter.values(): + value.sort() + return litter def mutate(children, mut_odds, mut_min, mut_max): From edd970b549e152ef7e73c07ec842aa20038c4a42 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Thu, 10 Oct 2019 16:25:44 -0500 Subject: [PATCH 18/73] Add test_crossover to TestBreedRats --- tests/test_chapter07.py | 74 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index e6849d6..027d2ea 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -52,6 +52,80 @@ def test_select(self): expected_population = [361, 329, 309, 293, 290] self.assertListEqual(test_population, expected_population) + @unittest.mock.patch('src.ch07.c1_breed_rats.random') + def test_crossover(self, mock_random): + """Test crossover.""" + # Patch random to use non-random seed. + self.random.seed(411) + mock_random.choice._mock_side_effect = self.random.choice + mock_random.randint._mock_side_effect = self.random.randint + + # Test equal males and females. + population = { + 'males': [219, 293, 281], + 'females': [119, 193, 181] + } + litter_sz = 8 + litter = breed_rats.crossover(population, litter_sz) + expected_litter = { + 'males': [128, 148, 196, 197, 201, 206, 213, 214, 256, 269], + 'females': [120, 160, 170, 182, 187, 193, 196, 197, 203, + 212, 215, 250, 251, 256] + } + self.assertDictEqual(litter, expected_litter) + litter_total = sum([len(value) for value in litter.values()]) + self.assertEqual(litter_total, + litter_sz * len(population['females'])) + + # Test fewer males than females. + population = { + 'males': [219, 293], + 'females': [119, 193, 181] + } + litter_sz = 8 + litter = breed_rats.crossover(population, litter_sz) + expected_litter = { + 'males': [165, 190, 208, 210, 245, 257, 280, 287], + 'females': [128, 140, 179, 181, 182, 182, 184, 187, + 187, 194, 201, 206, 216, 241, 281, 290] + } + self.assertDictEqual(litter, expected_litter) + litter_total = sum([len(value) for value in litter.values()]) + self.assertEqual(litter_total, + litter_sz * len(population['females'])) + + # Test fewer females than males. + population = { + 'males': [219, 293], + 'females': [119] + } + litter_sz = 8 + litter = breed_rats.crossover(population, litter_sz) + expected_litter = { + 'males': [162, 201, 265], + 'females': [205, 228, 254, 261, 282] + } + self.assertDictEqual(litter, expected_litter) + litter_total = sum([len(value) for value in litter.values()]) + self.assertEqual(litter_total, + litter_sz * len(population['females'])) + + # Test different litter size. + population = { + 'males': [219, 293], + 'females': [119] + } + litter_sz = 3 + litter = breed_rats.crossover(population, litter_sz) + expected_litter = { + 'males': [167, 181], + 'females': [291] + } + self.assertDictEqual(litter, expected_litter) + litter_total = sum([len(value) for value in litter.values()]) + self.assertEqual(litter_total, + litter_sz * len(population['females'])) + if __name__ == '__main__': unittest.main() From 925d89dd51ffeb8570b3d22bc16b45ef9f5bbb6e Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Thu, 10 Oct 2019 16:44:28 -0500 Subject: [PATCH 19/73] Complete mutate --- src/ch07/c1_breed_rats.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 0a1a548..0785fbb 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -147,8 +147,30 @@ def crossover(population: dict, litter_sz: int) -> dict: return litter -def mutate(children, mut_odds, mut_min, mut_max): - pass +def mutate(litter, mut_odds, mut_min, mut_max): + """Randomly alter pup weights applying input odds as a scalar. + + For each pup in **litter**, randomly decide if a floating point number + between **mut_min** and **mut_max** from :py:mod:`~random.uniform` will + be used as a scalar to modified their weight. + + Args: + litter (dict): Dictionary of lists with ``males`` and ``females`` + as keys and specimen weight in grams as values. + mut_odds (float): Probability of a mutation occurring in a pup. + mut_min (float): Scalar on rat weight of least beneficial mutation. + mut_max (float): Scalar on rat weight of most beneficial mutation. + + Returns: + Same dictionary of lists with weights potentially modified. + + """ + for gender in litter: + pups = litter[gender] + for index, pup in enumerate(pups): + if mut_odds >= random.random(): + pups[index] = round(pup * random.uniform(mut_min, mut_max)) + return litter def main(): From 6a279bf4e70d18c91edb61ee36723a3e92467d84 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Thu, 10 Oct 2019 16:45:23 -0500 Subject: [PATCH 20/73] Add test_mutate to TestBreedRats --- tests/test_chapter07.py | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 027d2ea..04a6428 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -126,6 +126,52 @@ def test_crossover(self, mock_random): self.assertEqual(litter_total, litter_sz * len(population['females'])) + @unittest.mock.patch('src.ch07.c1_breed_rats.random') + def test_mutate(self, mock_random): + """Test mutate.""" + # Patch random to use non-random seed. + self.random.seed(311) + mock_random.random._mock_side_effect = self.random.random + mock_random.uniform._mock_side_effect = self.random.uniform + + # Test large litter with low mutation chance. + litter = { + 'males': [165, 190, 208, 210, 245, 257, 280, 287], + 'females': [128, 140, 179, 181, 182, 182, 184, 187, + 187, 194, 201, 206, 216, 241, 281, 290] + } + mutated_litter = breed_rats.mutate(litter, 0.01, 0.5, 1.2) + expected = { + 'males': [165, 190, 208, 210, 245, 257, 280, 287], + 'females': [128, 140, 179, 181, 182, 182, 184, 187, + 187, 194, 201, 206, 216, 241, 281, 290] + } + self.assertDictEqual(mutated_litter, expected) + + # Test small litter with large mutation chance. + litter = { + 'males': [162, 201, 265], + 'females': [205, 228, 254, 261, 282] + } + mutated_litter = breed_rats.mutate(litter, 0.90, 0.5, 1.2) + expected = { + 'males': [95, 201, 265], + 'females': [179, 130, 267, 211, 261] + } + self.assertDictEqual(mutated_litter, expected) + + # Test small litter with large mutation chance and scale factor. + litter = { + 'males': [162, 201, 265], + 'females': [205, 228, 254, 261, 282] + } + mutated_litter = breed_rats.mutate(litter, 0.90, 2.0, 3.0) + expected = { + 'males': [338, 442, 655], + 'females': [469, 666, 254, 612, 789] + } + self.assertDictEqual(mutated_litter, expected) + if __name__ == '__main__': unittest.main() From 57ddcbc01cda479fef97b05ada391f32e5d4e8c0 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Thu, 10 Oct 2019 17:48:46 -0500 Subject: [PATCH 21/73] Fix error in crossover when females are larger than males --- src/ch07/c1_breed_rats.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 0785fbb..3104dbb 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -135,7 +135,10 @@ def crossover(population: dict, litter_sz: int) -> dict: male = random.choice(males) female = random.choice(females) for pup in range(litter_sz): - pup = random.randint(female, male) + larger, smaller = male, female + if female > male: + larger, smaller = female, male + pup = random.randint(smaller, larger) if random.choice([0, 1]): litter['males'].append(pup) else: From 13a3cc2b395ebe30a45a8db0634f7df2e686fbb3 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Thu, 10 Oct 2019 17:49:30 -0500 Subject: [PATCH 22/73] Add another test to test_crossover in TestBreedRats --- tests/test_chapter07.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 04a6428..b6f73e8 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -126,6 +126,22 @@ def test_crossover(self, mock_random): self.assertEqual(litter_total, litter_sz * len(population['females'])) + # Test larger female than males. + population = { + 'males': [119, 193], + 'females': [219] + } + litter_sz = 3 + litter = breed_rats.crossover(population, litter_sz) + expected_litter = { + 'males': [139, 150], + 'females': [119] + } + self.assertDictEqual(litter, expected_litter) + litter_total = sum([len(value) for value in litter.values()]) + self.assertEqual(litter_total, + litter_sz * len(population['females'])) + @unittest.mock.patch('src.ch07.c1_breed_rats.random') def test_mutate(self, mock_random): """Test mutate.""" From 3dd2f2ecfc23bec23cab07256c2993e36183fa55 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Thu, 10 Oct 2019 19:22:00 -0500 Subject: [PATCH 23/73] Add combine_values function --- src/ch07/c1_breed_rats.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 3104dbb..6171ac3 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -67,6 +67,24 @@ def populate(pop_total: int, minimum_wt: int, for _ in range(pop_total)] +def combine_values(dictionary: dict) -> list: + """Combine dictionary values. + + Combine values in a dictionary of lists into one list. + + Args: + dictionary (dict): Dictionary of lists. + + Returns: + List containing all values that were in **dictionary**. + + """ + values = [] + for value in dictionary.values(): + values.extend(value) + return values + + def measure(population: dict, target_wt: int) -> float: """Measure average weight of population against target. From 6a9b797a683b8145623ef44b3663cfa4289a8c0f Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Thu, 10 Oct 2019 19:26:52 -0500 Subject: [PATCH 24/73] Add test_combine_values to TestBreedRats --- tests/test_chapter07.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index b6f73e8..519987d 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -23,6 +23,16 @@ def test_populate(self, mock_random): expected_pop = [119, 193, 181, 190, 261, 190, 158, 169, 109, 229] self.assertListEqual(test_pop, expected_pop) + def test_combine_values(self): + """Test combine_values.""" + dictionary = { + 'first': [1, 2, 3, 4, 5], + 'second': [6, 7, 8, 9, 0] + } + combined = breed_rats.combine_values(dictionary) + expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] + self.assertListEqual(combined, expected) + def test_measure(self): """Test measure.""" population = { From 9aab8427973f305ada5cbeea6feedf9e7e822234 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Thu, 10 Oct 2019 19:28:42 -0500 Subject: [PATCH 25/73] Refactor measure to use combine_values --- src/ch07/c1_breed_rats.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 6171ac3..7be72a4 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -101,11 +101,7 @@ def measure(population: dict, target_wt: int) -> float: value of ``1`` is ``100%``, or complete. """ - # Combine genders into same list for measurement. - total = [] - for value in population.values(): - total.extend(value) - mean = statistics.mean(total) + mean = statistics.mean(combine_values(population)) return mean / target_wt From d0bfd0e459ca436412c090a26accc4b904d17796 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Thu, 10 Oct 2019 19:55:18 -0500 Subject: [PATCH 26/73] Refactor select to use dictionary and tuple as inputs --- src/ch07/c1_breed_rats.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 7be72a4..0e9cab3 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -105,21 +105,42 @@ def measure(population: dict, target_wt: int) -> float: return mean / target_wt -def select(population: list, to_keep: int) -> list: +def select(population: dict, to_keep: tuple) -> dict: """Select largest members of population. Sort members in descending order, and then keep largest members up to **to_keep**. Args: - population (list): List of members (weights in grams) in population. - to_keep (int): Number of members in population to keep. + population (dict): Dictionary of lists with ``males`` and ``females`` + as keys and specimen weight in grams as values. + to_keep (tuple): Tuple of integers representing number of males + and females in population to keep. Returns: - List of length **to_keep** of largest members of **population**. + Dictionary of lists of length **to_keep** of largest members of + **population**. + + Examples: + >>> from src.ch07.c1_breed_rats import select + >>> NUM_MALES, NUM_FEMALES = 4, 5 + >>> population = { + ... 'males': [111, 222, 333, 444, 555], + ... 'females': [666, 777, 888, 999, 1, 2]} + >>> print(select(population, (NUM_MALES, NUM_FEMALES))) + {'males': [555, 444, 333, 222], 'females': [999, 888, 777, 666]} """ - return sorted(population, reverse=True)[:to_keep] + new_population = {'males': [], 'females': []} + for gender in population: + num_males, num_females = to_keep + if gender == 'males': + new_population[gender].extend(sorted(population[gender], + reverse=True)[:num_males]) + else: + new_population[gender].extend(sorted(population[gender], + reverse=True)[:num_females]) + return new_population def crossover(population: dict, litter_sz: int) -> dict: From 8b4d7019505461422c493f31ee52efbc9f013434 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Thu, 10 Oct 2019 19:56:48 -0500 Subject: [PATCH 27/73] Refactor test_select in TestBreedRats to use dictionary and tuple --- tests/test_chapter07.py | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 519987d..b65c527 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -44,23 +44,35 @@ def test_measure(self): def test_select(self): """Test select.""" - population = [219, 293, 281, 290, 361, 290, 258, 269, 309, 329] + population = { + 'males': [219, 293, 281, 290, 361, 290, 258, 269, 309, 329], + 'females': [119, 193, 181, 190, 261, 190, 158, 169, 209, 229] + } # Test even numbered populations. - test_population = breed_rats.select(population, 2) - expected_population = [361, 329] - self.assertListEqual(test_population, expected_population) - test_population = breed_rats.select(population, 4) - expected_population = [361, 329, 309, 293] - self.assertListEqual(test_population, expected_population) + test_population = breed_rats.select(population, (2, 2)) + expected_population = { + 'males': [361, 329], 'females': [261, 229] + } + self.assertDictEqual(test_population, expected_population) + test_population = breed_rats.select(population, (4, 4)) + expected_population = { + 'males': [361, 329, 309, 293], 'females': [261, 229, 209, 193] + } + self.assertDictEqual(test_population, expected_population) # Test odd numbered populations. - test_population = breed_rats.select(population, 3) - expected_population = [361, 329, 309] - self.assertListEqual(test_population, expected_population) - test_population = breed_rats.select(population, 5) - expected_population = [361, 329, 309, 293, 290] - self.assertListEqual(test_population, expected_population) + test_population = breed_rats.select(population, (3, 3)) + expected_population = { + 'males': [361, 329, 309], 'females': [261, 229, 209] + } + self.assertDictEqual(test_population, expected_population) + test_population = breed_rats.select(population, (5, 5)) + expected_population = { + 'males': [361, 329, 309, 293, 290], + 'females': [261, 229, 209, 193, 190] + } + self.assertDictEqual(test_population, expected_population) @unittest.mock.patch('src.ch07.c1_breed_rats.random') def test_crossover(self, mock_random): From 6c238f2e16feedbbd8ddb5301c3ad2d88f36aa56 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Thu, 10 Oct 2019 20:04:51 -0500 Subject: [PATCH 28/73] Add breed_rats function --- src/ch07/c1_breed_rats.py | 68 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 0e9cab3..0b99d67 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -211,6 +211,74 @@ def mutate(litter, mut_odds, mut_min, mut_max): return litter +def breed_rats(population: dict, limits: tuple, pop_stats: tuple, + mut_stats: tuple) -> tuple: + """Simulate genetic algorithm by breeding rats. + + Using **population**, repeat cycle of measure, select, crossover, + and mutate until either of **limits** are met. + + Args: + population (dict): Dictionary of lists with ``males`` and ``females`` + as keys and specimen weight in grams as values. + limits (tuple): Tuple of integers representing target weight + (in grams) and generational cutoff to stop breeding program. + pop_stats (tuple): Tuple of integers representing number of male + rats in population, number of female rats in population, and + number of pups per pair of breeding rats. + mut_stats (tuple): Tuple of floats representing probability of a + mutation occurring in a pup, scalar on pup weight of least + beneficial mutation, and scalar on pup weight of most + beneficial mutation. + + Returns: + Tuple containing list of average weights of generations and number + of generations before meeting **target_wt**. + + Examples: + >>> from src.ch07.c1_breed_rats import populate, breed_rats + >>> INIT_MIN_WT, INIT_MAX_WT = 200, 600 + >>> INIT_MALE_MODE_WT, INIT_FEMALE_MODE_WT = 300, 250 + >>> TARGET_WT, GEN_LIMIT = 50000, 500 + >>> NUM_MALES, NUM_FEMALES, LITTER_SZ = 4, 16, 8 + >>> MUT_ODDS, MUT_MIN, MUT_MAX = 0.01, 0.5, 1.2 + >>> population = { + ... 'males': populate(NUM_MALES, INIT_MIN_WT, INIT_MAX_WT, + ... INIT_MALE_MODE_WT), + ... 'females': populate(NUM_FEMALES, INIT_MIN_WT, INIT_MAX_WT, + ... INIT_FEMALE_MODE_WT) + ... } + >>> ave_wt, generations = breed_rats(population, + ... (TARGET_WT, GEN_LIMIT), + ... (NUM_MALES, NUM_FEMALES, + ... LITTER_SZ), + ... (MUT_ODDS, MUT_MIN, MUT_MAX)) + >>> print(generations) + 248 + + """ + litter_sz = pop_stats[2] + target_wt, gen_limit = limits + mut_odds, mut_min, mut_max = mut_stats + + generations = 0 + ave_wt = [] + match = measure(population, target_wt) + + while match < 1 and generations < gen_limit: + population = select(population, pop_stats[:2]) + litter = crossover(population, litter_sz) + litter = mutate(litter, mut_odds, mut_min, mut_max) + for gender in litter: + population[gender].extend(litter[gender]) + match = measure(population, limits[0]) + print(f'Generation {generations} match: {match * 100:.4f}%') + + ave_wt.append(int(statistics.mean(combine_values(population)))) + generations += 1 + return ave_wt, generations + + def main(): """Simulate genetic algorithm. From 63469e5b1f23df9e87d0d1c4ec105706d8b41be3 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 14:08:35 -0500 Subject: [PATCH 29/73] Initial commit --- tests/data/ch07/__init__.py | 0 tests/data/ch07/breed_rats.txt | 12 ++++++++++++ 2 files changed, 12 insertions(+) create mode 100644 tests/data/ch07/__init__.py create mode 100644 tests/data/ch07/breed_rats.txt diff --git a/tests/data/ch07/__init__.py b/tests/data/ch07/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/ch07/breed_rats.txt b/tests/data/ch07/breed_rats.txt new file mode 100644 index 0000000..6e2b8c2 --- /dev/null +++ b/tests/data/ch07/breed_rats.txt @@ -0,0 +1,12 @@ +Generation 0 match: 1.7358% +Generation 1 match: 2.8215% +Generation 2 match: 4.3073% +Generation 3 match: 5.9099% +Generation 4 match: 8.1822% +Generation 5 match: 11.7231% +Generation 6 match: 16.5973% +Generation 7 match: 24.7527% +Generation 8 match: 36.1726% +Generation 9 match: 52.3236% +Generation 10 match: 75.5783% +Generation 11 match: 108.5198% From 2d57b5a51ae7b829598c4afb366a361caa4660e4 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 14:09:11 -0500 Subject: [PATCH 30/73] Add test_breed_rats to TestBreedRats --- tests/test_chapter07.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index b65c527..e5c8d80 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -1,6 +1,8 @@ """Test Chapter 7.""" import unittest.mock +import os from random import Random +from io import StringIO import src.ch07.c1_breed_rats as breed_rats @@ -210,6 +212,30 @@ def test_mutate(self, mock_random): } self.assertDictEqual(mutated_litter, expected) + @unittest.mock.patch('src.ch07.c1_breed_rats.random', new_callable=Random) + @unittest.mock.patch('sys.stdout', new_callable=StringIO) + def test_breed_rats(self, mock_stdout, mock_random): + """Test breed_rats.""" + # Patch random to use non-random seed. + mock_random.seed(311) + + population = { + 'males': [450, 320, 510], + 'females': [250, 300, 220, 160] + } + ave, generations = breed_rats.breed_rats(population, (20000, 500), + (3, 10, 8), + (0.75, .75, 1.5)) + self.assertEqual(generations, 12) + self.assertEqual(ave, [347, 564, 861, 1181, 1636, 2344, 3319, 4950, + 7234, 10464, 15115, 21703]) + + # Test sys.stdout output. + with open(os.path.normpath('tests/data/ch07/breed_rats.txt'), + 'r') as file: + file_data = ''.join(file.readlines()) + self.assertEqual(mock_stdout.getvalue(), file_data) + if __name__ == '__main__': unittest.main() From 9aa007e6e4126cfd12f8a2d4343370ee99152f13 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 14:09:28 -0500 Subject: [PATCH 31/73] Complete main --- src/ch07/c1_breed_rats.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 0b99d67..3ca14b9 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -280,14 +280,32 @@ def breed_rats(population: dict, limits: tuple, pop_stats: tuple, def main(): - """Simulate genetic algorithm. + """Demonstrate breed_rats function. - After initializing population, repeat cycle of measure, select, crossover, - and mutate to meet goal of 50000 grams. + Wrap :func:`populate` and :func:`breed_rats` with module + constants and then display time to run. """ start_time = time.time() - pass + + population = { + 'males': populate(NUM_MALES, INIT_MIN_WT, INIT_MAX_WT, + INIT_MALE_MODE_WT), + 'females': populate(NUM_FEMALES, INIT_MIN_WT, INIT_MAX_WT, + INIT_FEMALE_MODE_WT) + } + match = measure(population, TARGET_WT) + print(f'Initial population match: {match * 100}%') + print(f'Number of males, females to keep: {NUM_MALES}, {NUM_FEMALES}') + ave_wt, generations = breed_rats(population, + (TARGET_WT, GEN_LIMIT), + (NUM_MALES, NUM_FEMALES, LITTER_SZ), + (MUT_ODDS, MUT_MIN, MUT_MAX)) + + print(f'Average weight per generation: {ave_wt}') + print(f'\nNumber of generations: {generations}') + print(f'Number of years: {int(generations/LITTERS_PER_YR)}') + end_time = time.time() duration = end_time - start_time print(f'Runtime for this program was {duration} seconds.') From ae415eff97e7d19d3f3fe8eb8c3f960496b5c412 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 14:22:10 -0500 Subject: [PATCH 32/73] Initial commit --- tests/data/ch07/main/breed_rats.txt | 211 ++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 tests/data/ch07/main/breed_rats.txt diff --git a/tests/data/ch07/main/breed_rats.txt b/tests/data/ch07/main/breed_rats.txt new file mode 100644 index 0000000..ae00834 --- /dev/null +++ b/tests/data/ch07/main/breed_rats.txt @@ -0,0 +1,211 @@ +Initial population match: 0.6993999999999999% +Number of males, females to keep: 4, 16 +Generation 0 match: 0.6763% +Generation 1 match: 0.8846% +Generation 2 match: 0.9594% +Generation 3 match: 1.0266% +Generation 4 match: 1.0600% +Generation 5 match: 1.0689% +Generation 6 match: 1.0786% +Generation 7 match: 1.1509% +Generation 8 match: 1.1816% +Generation 9 match: 1.1840% +Generation 10 match: 1.1860% +Generation 11 match: 1.1860% +Generation 12 match: 1.1876% +Generation 13 match: 1.1920% +Generation 14 match: 1.2738% +Generation 15 match: 1.3727% +Generation 16 match: 1.4070% +Generation 17 match: 1.4156% +Generation 18 match: 1.4134% +Generation 19 match: 1.4149% +Generation 20 match: 1.4150% +Generation 21 match: 1.4106% +Generation 22 match: 1.4319% +Generation 23 match: 1.4809% +Generation 24 match: 1.5012% +Generation 25 match: 1.5169% +Generation 26 match: 1.5375% +Generation 27 match: 1.6406% +Generation 28 match: 1.6742% +Generation 29 match: 1.6887% +Generation 30 match: 1.7272% +Generation 31 match: 1.8235% +Generation 32 match: 1.8446% +Generation 33 match: 1.8502% +Generation 34 match: 1.8430% +Generation 35 match: 1.8603% +Generation 36 match: 1.9234% +Generation 37 match: 2.0487% +Generation 38 match: 2.0849% +Generation 39 match: 2.1066% +Generation 40 match: 2.1209% +Generation 41 match: 2.2104% +Generation 42 match: 2.3183% +Generation 43 match: 2.3558% +Generation 44 match: 2.3979% +Generation 45 match: 2.6029% +Generation 46 match: 2.7094% +Generation 47 match: 2.7573% +Generation 48 match: 2.7541% +Generation 49 match: 2.7610% +Generation 50 match: 2.7736% +Generation 51 match: 2.8156% +Generation 52 match: 2.9536% +Generation 53 match: 3.0460% +Generation 54 match: 3.0531% +Generation 55 match: 3.0599% +Generation 56 match: 3.1893% +Generation 57 match: 3.3412% +Generation 58 match: 3.4400% +Generation 59 match: 3.4996% +Generation 60 match: 3.5317% +Generation 61 match: 3.7245% +Generation 62 match: 4.2546% +Generation 63 match: 4.4829% +Generation 64 match: 4.6759% +Generation 65 match: 4.9930% +Generation 66 match: 5.1067% +Generation 67 match: 5.2001% +Generation 68 match: 5.4559% +Generation 69 match: 5.5223% +Generation 70 match: 5.5676% +Generation 71 match: 5.7401% +Generation 72 match: 6.0965% +Generation 73 match: 6.1851% +Generation 74 match: 6.2199% +Generation 75 match: 6.2550% +Generation 76 match: 6.6570% +Generation 77 match: 7.1252% +Generation 78 match: 7.2423% +Generation 79 match: 7.3414% +Generation 80 match: 7.6177% +Generation 81 match: 8.0474% +Generation 82 match: 8.4778% +Generation 83 match: 8.5551% +Generation 84 match: 8.5943% +Generation 85 match: 8.5973% +Generation 86 match: 8.5953% +Generation 87 match: 8.6259% +Generation 88 match: 8.8294% +Generation 89 match: 8.9842% +Generation 90 match: 9.0423% +Generation 91 match: 9.0237% +Generation 92 match: 9.0480% +Generation 93 match: 9.0247% +Generation 94 match: 9.0463% +Generation 95 match: 9.0370% +Generation 96 match: 9.0918% +Generation 97 match: 9.7119% +Generation 98 match: 10.3309% +Generation 99 match: 10.9136% +Generation 100 match: 12.1193% +Generation 101 match: 12.3139% +Generation 102 match: 12.3290% +Generation 103 match: 12.3340% +Generation 104 match: 12.3075% +Generation 105 match: 12.3340% +Generation 106 match: 12.3340% +Generation 107 match: 12.3409% +Generation 108 match: 12.4644% +Generation 109 match: 13.1881% +Generation 110 match: 13.3787% +Generation 111 match: 13.6540% +Generation 112 match: 14.3537% +Generation 113 match: 14.8379% +Generation 114 match: 14.9674% +Generation 115 match: 15.0241% +Generation 116 match: 15.1602% +Generation 117 match: 15.7452% +Generation 118 match: 16.8613% +Generation 119 match: 17.1151% +Generation 120 match: 17.2924% +Generation 121 match: 17.7822% +Generation 122 match: 19.2245% +Generation 123 match: 19.4887% +Generation 124 match: 19.5949% +Generation 125 match: 19.7033% +Generation 126 match: 19.7646% +Generation 127 match: 19.7524% +Generation 128 match: 19.7660% +Generation 129 match: 19.7137% +Generation 130 match: 19.7161% +Generation 131 match: 19.7660% +Generation 132 match: 19.7184% +Generation 133 match: 19.7947% +Generation 134 match: 19.8487% +Generation 135 match: 20.8109% +Generation 136 match: 22.1507% +Generation 137 match: 22.9251% +Generation 138 match: 24.8349% +Generation 139 match: 26.4692% +Generation 140 match: 26.8086% +Generation 141 match: 27.1594% +Generation 142 match: 27.7669% +Generation 143 match: 27.9254% +Generation 144 match: 28.1132% +Generation 145 match: 29.3268% +Generation 146 match: 31.7899% +Generation 147 match: 32.3443% +Generation 148 match: 33.3064% +Generation 149 match: 35.2271% +Generation 150 match: 35.8846% +Generation 151 match: 35.9729% +Generation 152 match: 35.8795% +Generation 153 match: 35.9490% +Generation 154 match: 36.1555% +Generation 155 match: 36.9230% +Generation 156 match: 37.3824% +Generation 157 match: 37.3552% +Generation 158 match: 38.7906% +Generation 159 match: 41.3846% +Generation 160 match: 43.1931% +Generation 161 match: 43.5718% +Generation 162 match: 43.7987% +Generation 163 match: 43.8242% +Generation 164 match: 44.0769% +Generation 165 match: 45.3220% +Generation 166 match: 45.7661% +Generation 167 match: 46.0268% +Generation 168 match: 46.2422% +Generation 169 match: 46.2025% +Generation 170 match: 46.8829% +Generation 171 match: 47.2227% +Generation 172 match: 49.4635% +Generation 173 match: 52.8801% +Generation 174 match: 53.1827% +Generation 175 match: 53.1192% +Generation 176 match: 53.2938% +Generation 177 match: 53.2440% +Generation 178 match: 53.2664% +Generation 179 match: 53.1639% +Generation 180 match: 53.0472% +Generation 181 match: 53.3548% +Generation 182 match: 53.5466% +Generation 183 match: 56.0562% +Generation 184 match: 60.7658% +Generation 185 match: 61.9034% +Generation 186 match: 62.2861% +Generation 187 match: 65.2147% +Generation 188 match: 68.6984% +Generation 189 match: 70.3274% +Generation 190 match: 70.4157% +Generation 191 match: 70.9391% +Generation 192 match: 72.4079% +Generation 193 match: 76.9236% +Generation 194 match: 80.5213% +Generation 195 match: 82.7053% +Generation 196 match: 89.7543% +Generation 197 match: 95.2849% +Generation 198 match: 97.4329% +Generation 199 match: 97.9471% +Generation 200 match: 98.4308% +Generation 201 match: 98.6094% +Generation 202 match: 98.4553% +Generation 203 match: 101.1761% +Average weight per generation: [338, 442, 479, 513, 530, 534, 539, 575, 590, 592, 593, 593, 593, 596, 636, 686, 703, 707, 706, 707, 707, 705, 715, 740, 750, 758, 768, 820, 837, 844, 863, 911, 922, 925, 921, 930, 961, 1024, 1042, 1053, 1060, 1105, 1159, 1177, 1198, 1301, 1354, 1378, 1377, 1380, 1386, 1407, 1476, 1523, 1526, 1529, 1594, 1670, 1720, 1749, 1765, 1862, 2127, 2241, 2337, 2496, 2553, 2600, 2727, 2761, 2783, 2870, 3048, 3092, 3109, 3127, 3328, 3562, 3621, 3670, 3808, 4023, 4238, 4277, 4297, 4298, 4297, 4312, 4414, 4492, 4521, 4511, 4524, 4512, 4523, 4518, 4545, 4855, 5165, 5456, 6059, 6156, 6164, 6167, 6153, 6167, 6167, 6170, 6232, 6594, 6689, 6827, 7176, 7418, 7483, 7512, 7580, 7872, 8430, 8557, 8646, 8891, 9612, 9744, 9797, 9851, 9882, 9876, 9883, 9856, 9858, 9883, 9859, 9897, 9924, 10405, 11075, 11462, 12417, 13234, 13404, 13579, 13883, 13962, 14056, 14663, 15894, 16172, 16653, 17613, 17942, 17986, 17939, 17974, 18077, 18461, 18691, 18677, 19395, 20692, 21596, 21785, 21899, 21912, 22038, 22661, 22883, 23013, 23121, 23101, 23441, 23611, 24731, 26440, 26591, 26559, 26646, 26621, 26633, 26581, 26523, 26677, 26773, 28028, 30382, 30951, 31143, 32607, 34349, 35163, 35207, 35469, 36203, 38461, 40260, 41352, 44877, 47642, 48716, 48973, 49215, 49304, 49227, 50588] + +Number of generations: 204 +Number of years: 20 +Runtime for this program was 0 seconds. From bb8775830b1aa64807935d957d38ab6ab79557c3 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 14:22:29 -0500 Subject: [PATCH 33/73] Add test_main to TestBreedRats --- tests/test_chapter07.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index e5c8d80..b1c5f26 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -236,6 +236,24 @@ def test_breed_rats(self, mock_stdout, mock_random): file_data = ''.join(file.readlines()) self.assertEqual(mock_stdout.getvalue(), file_data) + @unittest.mock.patch('src.ch07.c1_breed_rats.time') + @unittest.mock.patch('src.ch07.c1_breed_rats.random', new_callable=Random) + @unittest.mock.patch('sys.stdout', new_callable=StringIO) + def test_main(self, mock_stdout, mock_random, mock_time): + """Test main.""" + self.maxDiff = None + # Patch out variances. + mock_random.seed(311) + mock_time.time.return_value = 12345 + + breed_rats.main() + + # Test sys.stdout output. + with open(os.path.normpath('tests/data/ch07/main/breed_rats.txt'), + 'r') as file: + file_data = ''.join(file.readlines()) + self.assertEqual(mock_stdout.getvalue(), file_data) + if __name__ == '__main__': unittest.main() From 66476403e006f2764ca53707f95fa936f6fb27c3 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 14:25:33 -0500 Subject: [PATCH 34/73] Initial commit --- docs/source/src.ch07.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/source/src.ch07.rst diff --git a/docs/source/src.ch07.rst b/docs/source/src.ch07.rst new file mode 100644 index 0000000..6885a5b --- /dev/null +++ b/docs/source/src.ch07.rst @@ -0,0 +1,22 @@ +src.ch07 package +================ + +Submodules +---------- + +src.ch07.c1\_breed\_rats module +------------------------------- + +.. automodule:: src.ch07.c1_breed_rats + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: src.ch07 + :members: + :undoc-members: + :show-inheritance: From 1ff14967014c14a73c97c320c857b14466ceff58 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 14:25:50 -0500 Subject: [PATCH 35/73] Add src.ch07 --- docs/source/src.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/src.rst b/docs/source/src.rst index c328fea..c27558a 100644 --- a/docs/source/src.rst +++ b/docs/source/src.rst @@ -12,6 +12,7 @@ Subpackages src.ch04 src.ch05 src.ch06 + src.ch07 Module contents --------------- From 7cd3e383d87310ce3d0150acdf736469c65935a0 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 14:34:12 -0500 Subject: [PATCH 36/73] Add default values to attributes section in module docstring --- src/ch07/c1_breed_rats.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 3ca14b9..d42e6ad 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -9,21 +9,30 @@ following: Attributes: - TARGET_WT (int): Target weight in grams. - NUM_MALES (int): Number of male rats in population. + TARGET_WT (int): Target weight in grams. Default is ``50000``. + NUM_MALES (int): Number of male rats in population. Default is ``4``. NUM_FEMALES (int): Number of female rats in population. + Default is ``16``. INIT_MIN_WT (int): Minimum weight of adult rat in initial population. + Default is ``200``. INIT_MAX_WT (int): Maximum weight of adult rat in initial population. + Default is ``600``. INIT_MALE_MODE_WT (int): Most common adult male rat weight in initial - population. + population. Default is ``300``. INIT_FEMALE_MODE_WT (int): Most common adult female rat weight in initial - population. + population. Default is ``250``. MUT_ODDS (float): Probability of a mutation occurring in a pup. + Default is ``0.01``. MUT_MIN (float): Scalar on rat weight of least beneficial mutation. + Default is ``0.5``. MUT_MAX (float): Scalar on rat weight of most beneficial mutation. + Default is ``1.2``. LITTER_SZ (int): Number of pups per pair of breeding rats. + Default is ``8``. LITTERS_PER_YR (int): Number of litters per year per pair of breeding rats. + Default is ``10``. GEN_LIMIT (int): Generational cutoff to stop breeding program. + Default is ``500``. """ import time From 4c5cc0d7755662d700ff7904e13f481e46669ffd Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 14:36:13 -0500 Subject: [PATCH 37/73] Update returns section to match new arguments in breed_rats docstring --- src/ch07/c1_breed_rats.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index d42e6ad..9fadfc6 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -242,7 +242,8 @@ def breed_rats(population: dict, limits: tuple, pop_stats: tuple, Returns: Tuple containing list of average weights of generations and number - of generations before meeting **target_wt**. + of generations before meeting target weight or generation limit in + **limits**. Examples: >>> from src.ch07.c1_breed_rats import populate, breed_rats From 7419bacd1144311d622142218052d7cc757d53a0 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 18:09:38 -0500 Subject: [PATCH 38/73] Refactor module as class --- src/ch07/c1_breed_rats.py | 583 ++++++++++++++++++++++---------------- 1 file changed, 344 insertions(+), 239 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 9fadfc6..4b96346 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -39,254 +39,359 @@ import random import statistics -# CONSTANTS (weights in grams) -TARGET_WT = 50000 # Target weight in grams. -NUM_MALES = 4 # Number of male rats in population. -NUM_FEMALES = 16 # Number of female rats in population. -INIT_MIN_WT = 200 # Minimum weight of adult rat in initial population. -INIT_MAX_WT = 600 # Maximum weight of adult rat in initial population. -INIT_MALE_MODE_WT = 300 # Most common adult male rat weight. -INIT_FEMALE_MODE_WT = 250 # Most common adult female rat weight. -MUT_ODDS = 0.01 # Probability of a mutation occurring in a pup. -MUT_MIN = 0.5 # Scalar on rat weight of least beneficial mutation. -MUT_MAX = 1.2 # Scalar on rat weight of most beneficial mutation. -LITTER_SZ = 8 # Number of pups per pair of breeding rats. -LITTERS_PER_YR = 10 # Number of litters per year per pair of breeding rats. -GEN_LIMIT = 500 # Generational cutoff to stop breeding program. - - -def populate(pop_total: int, minimum_wt: int, - maximum_wt: int, mode_wt: int) -> list: - """Generate population with a triangular distribution of weights. - - Use :py:mod:`~random.triangular` to generate a population with a triangular - distribution of weights based on *minimum_wt*, *maximum_wt*, and *mode_wt*. - Args: - pop_total (int): Total number of rats in population. - minimum_wt (int): Minimum weight of adult rat in initial population. - maximum_wt (int): Maximum weight of adult rat in initial population. - mode_wt (int): Most common adult rat weight in initial population. - - Returns: - List of triangularly distributed weights of a given rat population. - - """ - return [int(random.triangular(minimum_wt, maximum_wt, mode_wt)) - for _ in range(pop_total)] - - -def combine_values(dictionary: dict) -> list: - """Combine dictionary values. - - Combine values in a dictionary of lists into one list. - - Args: - dictionary (dict): Dictionary of lists. - - Returns: - List containing all values that were in **dictionary**. - - """ - values = [] - for value in dictionary.values(): - values.extend(value) - return values - - -def measure(population: dict, target_wt: int) -> float: - """Measure average weight of population against target. - - Calculate mean weight of **population** and divide by **target_wt** to - determine if goal has been met. - - Args: - population (dict): Dictionary of lists with ``males`` and ``females`` - as keys and specimen weight in grams as values. - target_wt (int): Target average weight of population in grams. - - Returns: - :py:obj:`float` representing decimal percentage of completion where a - value of ``1`` is ``100%``, or complete. - - """ - mean = statistics.mean(combine_values(population)) - return mean / target_wt - - -def select(population: dict, to_keep: tuple) -> dict: - """Select largest members of population. - - Sort members in descending order, and then keep largest members up to - **to_keep**. +class BreedRats(object): + """Efficiently breed rats to an average weight of **target_wt**. - Args: - population (dict): Dictionary of lists with ``males`` and ``females`` - as keys and specimen weight in grams as values. - to_keep (tuple): Tuple of integers representing number of males - and females in population to keep. - - Returns: - Dictionary of lists of length **to_keep** of largest members of - **population**. - - Examples: - >>> from src.ch07.c1_breed_rats import select - >>> NUM_MALES, NUM_FEMALES = 4, 5 - >>> population = { - ... 'males': [111, 222, 333, 444, 555], - ... 'females': [666, 777, 888, 999, 1, 2]} - >>> print(select(population, (NUM_MALES, NUM_FEMALES))) - {'males': [555, 444, 333, 222], 'females': [999, 888, 777, 666]} - - """ - new_population = {'males': [], 'females': []} - for gender in population: - num_males, num_females = to_keep - if gender == 'males': - new_population[gender].extend(sorted(population[gender], - reverse=True)[:num_males]) - else: - new_population[gender].extend(sorted(population[gender], - reverse=True)[:num_females]) - return new_population - - -def crossover(population: dict, litter_sz: int) -> dict: - """Crossover genes among members (weights) of a population. - - Breed population where each breeding pair produces a litter - of **litter_sz** pups. Pup's gender is assigned randomly. + Use genetic algorithm on a mixed population of male and female rats. - To accommodate mismatched pairs, breeding pairs are selected randomly, - and once paired, females are removed from the breeding pool while - males remain. + Weights and number of each gender vary and can be set by modifying the + following: Args: - population (dict): Dictionary of lists with ``males`` and ``females`` - as keys and specimen weight in grams as values. - litter_sz (int): Number of pups per breeding pair of rats. - - Returns: - Dictionary of lists with ``males`` and ``females`` as keys and - pup weight in grams as values. + num_males (int): Number of male rats in population. + Default is ``4``. + num_females (int): Number of female rats in population. + Default is ``16``. + target_wt (int): Target weight in grams. Default is ``50000``. + gen_limit (int): Generational cutoff to stop breeding program. + Default is ``500``. + + Attributes: + _min_wt (int): Minimum weight of adult rat in initial population. + Default is ``200``. + _max_wt (int): Maximum weight of adult rat in initial population. + Default is ``600``. + _male_mode_wt (int): Most common adult male rat weight in initial + population. Default is ``300``. + _female_mode_wt (int): Most common adult female rat weight in initial + population. Default is ``250``. + _mut_odds (float): Probability of a mutation occurring in a pup. + Default is ``0.01``. + _mut_min (float): Scalar on rat weight of least beneficial mutation. + Default is ``0.5``. + _mut_max (float): Scalar on rat weight of most beneficial mutation. + Default is ``1.2``. + _litter_sz (int): Number of pups per pair of breeding rats. + Default is ``8``. + _litters_per_yr (int): Number of litters per year per pair of breeding rats. + Default is ``10``. """ - males = population['males'] - females = population['females'].copy() - litter = {'males': [], 'females': []} - while females: - male = random.choice(males) - female = random.choice(females) - for pup in range(litter_sz): - larger, smaller = male, female - if female > male: - larger, smaller = female, male - pup = random.randint(smaller, larger) - if random.choice([0, 1]): - litter['males'].append(pup) + def __init__(self, num_males: int = 4, num_females: int = 16, + target_wt: int = 50000, gen_limit: int = 500): + self._min_wt = 200 + self._max_wt = 600 + self._male_mode_wt = 300 + self._female_mode_wt = 250 + self._mut_odds = 0.01 + self._mut_min = 0.5 + self._mut_max = 1.2 + self._litter_sz = 8 + self._litters_per_yr = 10 + + self.num_males = num_males + self.num_females = num_females + self.target_wt = target_wt + self.gen_limit = gen_limit + + @property + def min_wt(self): + """int: Minimum weight of adult rat in initial population.""" + return self._min_wt + + @min_wt.setter + def min_wt(self, value: int): + self._min_wt = value + + @property + def max_wt(self): + """int: Maximum weight of adult rat in initial population.""" + return self._max_wt + + @max_wt.setter + def max_wt(self, value: int): + self._max_wt = value + + @property + def male_mode_wt(self): + """int: Most common adult male rat weight in initial population.""" + return self._male_mode_wt + + @male_mode_wt.setter + def male_mode_wt(self, value: int): + self._male_mode_wt = value + + @property + def female_mode_wt(self): + """int: Most common adult female rat weight in initial population.""" + return self._female_mode_wt + + @female_mode_wt.setter + def female_mode_wt(self, value: int): + self._female_mode_wt = value + + @property + def mut_odds(self): + """float: Probability of a mutation occurring in a pup.""" + return self._mut_odds + + @mut_odds.setter + def mut_odds(self, value: float): + self._mut_odds = value + + @property + def mut_min(self): + """float: Scalar on rat weight of least beneficial mutation.""" + return self._mut_min + + @mut_min.setter + def mut_min(self, value: float): + self._mut_min = value + + @property + def mut_max(self): + """float: Scalar on rat weight of most beneficial mutation.""" + return self._mut_max + + @mut_max.setter + def mut_max(self, value: float): + self._mut_max = value + + @property + def litter_sz(self): + """int: Number of pups per pair of breeding rats.""" + return self._litter_sz + + @litter_sz.setter + def litter_sz(self, value: int): + self._litter_sz = value + + @property + def litters_per_yr(self): + """int: Number of litters per year per pair of breeding rats.""" + return self._litters_per_yr + + @litters_per_yr.setter + def litters_per_yr(self, value: int): + self._litters_per_yr = value + + def populate(self, pop_total: int, mode_wt: int) -> list: + """Generate population with a triangular distribution of weights. + + Use :py:mod:`~random.triangular` to generate a population with a triangular + distribution of weights based on *minimum_wt*, *maximum_wt*, and *mode_wt*. + + Args: + pop_total (int): Total number of rats in population. + mode_wt (int): Most common adult rat weight in initial population. + + Returns: + List of triangularly distributed weights of a given rat population. + + """ + return [int(random.triangular(self._min_wt, self._max_wt, mode_wt)) + for _ in range(pop_total)] + + def get_population(self, num_males: int = None, + num_females: int = None) -> dict: + """Generate random population of rats. + + Wraps :func:`populate` using **num_males** and **num_females**. + + Args: + num_males (int): Number of males in population. + If :obj:`None`, defaults to instance value. + num_females (int): Number of females in population. + If :obj:`None`, defaults to instance value. + + Returns: + Dictionary of lists with ``males`` and ``females`` as keys and + specimen weight in grams as values. + + """ + if num_males is None: + num_males = self.num_males + if num_females is None: + num_females = self.num_females + population = { + 'males': self.populate(num_males, self._male_mode_wt), + 'females': self.populate(num_females, self._female_mode_wt) + } + return population + + @staticmethod + def combine_values(dictionary: dict) -> list: + """Combine dictionary values. + + Combine values in a dictionary of lists into one list. + + Args: + dictionary (dict): Dictionary of lists. + + Returns: + List containing all values that were in **dictionary**. + + """ + values = [] + for value in dictionary.values(): + values.extend(value) + return values + + def measure(self, population: dict) -> float: + """Measure average weight of population against target. + + Calculate mean weight of **population** and divide by **target_wt** to + determine if goal has been met. + + Args: + population (dict): Dictionary of lists with ``males`` and ``females`` + as keys and specimen weight in grams as values. + + Returns: + :py:obj:`float` representing decimal percentage of completion where a + value of ``1`` is ``100%``, or complete. + + """ + mean = statistics.mean(self.combine_values(population)) + return mean / self.target_wt + + def select(self, population: dict) -> dict: + """Select largest members of population. + + Sort members in descending order, and then keep largest members up to + **to_keep**. + + Args: + population (dict): Dictionary of lists with ``males`` and ``females`` + as keys and specimen weight in grams as values. + + Returns: + Dictionary of lists of length **to_keep** of largest members of + **population**. + + Examples: + >>> from src.ch07.c1_breed_rats import BreedRats + >>> sample_one = BreedRats(num_males = 4, num_females = 4) + >>> s1_population = sample_one.get_population(num_males = 5, + ... num_females = 10) + >>> selected_population = sample_one.select(s1_population) + >>> print(selected_population) + {'males': [555, 444, 333, 222], 'females': [999, 888, 777, 666]} + + """ + new_population = {'males': [], 'females': []} + for gender in population: + if gender == 'males': + new_population[gender].extend(sorted(population[gender], + reverse=True)[:self.num_males]) else: - litter['females'].append(pup) - females.remove(female) - # Sort output for test consistency. - for value in litter.values(): - value.sort() - return litter - - -def mutate(litter, mut_odds, mut_min, mut_max): - """Randomly alter pup weights applying input odds as a scalar. - - For each pup in **litter**, randomly decide if a floating point number - between **mut_min** and **mut_max** from :py:mod:`~random.uniform` will - be used as a scalar to modified their weight. - - Args: - litter (dict): Dictionary of lists with ``males`` and ``females`` - as keys and specimen weight in grams as values. - mut_odds (float): Probability of a mutation occurring in a pup. - mut_min (float): Scalar on rat weight of least beneficial mutation. - mut_max (float): Scalar on rat weight of most beneficial mutation. - - Returns: - Same dictionary of lists with weights potentially modified. - - """ - for gender in litter: - pups = litter[gender] - for index, pup in enumerate(pups): - if mut_odds >= random.random(): - pups[index] = round(pup * random.uniform(mut_min, mut_max)) - return litter - - -def breed_rats(population: dict, limits: tuple, pop_stats: tuple, - mut_stats: tuple) -> tuple: - """Simulate genetic algorithm by breeding rats. - - Using **population**, repeat cycle of measure, select, crossover, - and mutate until either of **limits** are met. - - Args: - population (dict): Dictionary of lists with ``males`` and ``females`` - as keys and specimen weight in grams as values. - limits (tuple): Tuple of integers representing target weight - (in grams) and generational cutoff to stop breeding program. - pop_stats (tuple): Tuple of integers representing number of male - rats in population, number of female rats in population, and - number of pups per pair of breeding rats. - mut_stats (tuple): Tuple of floats representing probability of a - mutation occurring in a pup, scalar on pup weight of least - beneficial mutation, and scalar on pup weight of most - beneficial mutation. - - Returns: - Tuple containing list of average weights of generations and number - of generations before meeting target weight or generation limit in - **limits**. - - Examples: - >>> from src.ch07.c1_breed_rats import populate, breed_rats - >>> INIT_MIN_WT, INIT_MAX_WT = 200, 600 - >>> INIT_MALE_MODE_WT, INIT_FEMALE_MODE_WT = 300, 250 - >>> TARGET_WT, GEN_LIMIT = 50000, 500 - >>> NUM_MALES, NUM_FEMALES, LITTER_SZ = 4, 16, 8 - >>> MUT_ODDS, MUT_MIN, MUT_MAX = 0.01, 0.5, 1.2 - >>> population = { - ... 'males': populate(NUM_MALES, INIT_MIN_WT, INIT_MAX_WT, - ... INIT_MALE_MODE_WT), - ... 'females': populate(NUM_FEMALES, INIT_MIN_WT, INIT_MAX_WT, - ... INIT_FEMALE_MODE_WT) - ... } - >>> ave_wt, generations = breed_rats(population, - ... (TARGET_WT, GEN_LIMIT), - ... (NUM_MALES, NUM_FEMALES, - ... LITTER_SZ), - ... (MUT_ODDS, MUT_MIN, MUT_MAX)) - >>> print(generations) - 248 - - """ - litter_sz = pop_stats[2] - target_wt, gen_limit = limits - mut_odds, mut_min, mut_max = mut_stats - - generations = 0 - ave_wt = [] - match = measure(population, target_wt) - - while match < 1 and generations < gen_limit: - population = select(population, pop_stats[:2]) - litter = crossover(population, litter_sz) - litter = mutate(litter, mut_odds, mut_min, mut_max) + new_population[gender].extend(sorted(population[gender], + reverse=True)[:self.num_females]) + return new_population + + def crossover(self, population: dict) -> dict: + """Crossover genes among members (weights) of a population. + + Breed population where each breeding pair produces a litter + of **litter_sz** pups. Pup's gender is assigned randomly. + + To accommodate mismatched pairs, breeding pairs are selected randomly, + and once paired, females are removed from the breeding pool while + males remain. + + Args: + population (dict): Dictionary of lists with ``males`` and ``females`` + as keys and specimen weight in grams as values. + + Returns: + Dictionary of lists with ``males`` and ``females`` as keys and + pup weight in grams as values. + + """ + males = population['males'] + females = population['females'].copy() + litter = {'males': [], 'females': []} + while females: + male = random.choice(males) + female = random.choice(females) + for pup in range(self._litter_sz): + larger, smaller = male, female + if female > male: + larger, smaller = female, male + pup = random.randint(smaller, larger) + if random.choice([0, 1]): + litter['males'].append(pup) + else: + litter['females'].append(pup) + females.remove(female) + # Sort output for test consistency. + for value in litter.values(): + value.sort() + return litter + + def mutate(self, litter): + """Randomly alter pup weights applying input odds as a scalar. + + For each pup in **litter**, randomly decide if a floating point number + between **mut_min** and **mut_max** from :py:mod:`~random.uniform` will + be used as a scalar to modified their weight. + + Args: + litter (dict): Dictionary of lists with ``males`` and ``females`` + as keys and specimen weight in grams as values. + + Returns: + Same dictionary of lists with weights potentially modified. + + """ for gender in litter: - population[gender].extend(litter[gender]) - match = measure(population, limits[0]) - print(f'Generation {generations} match: {match * 100:.4f}%') - - ave_wt.append(int(statistics.mean(combine_values(population)))) - generations += 1 - return ave_wt, generations + pups = litter[gender] + for index, pup in enumerate(pups): + if self._mut_odds >= random.random(): + pups[index] = round(pup * random.uniform(self._mut_min, self._mut_max)) + return litter + + def breed_rats(self, population: dict) -> tuple: + """Simulate genetic algorithm by breeding rats. + + Using **population**, repeat cycle of measure, select, crossover, + and mutate until either of **limits** are met. + + Args: + population (dict): Dictionary of lists with ``males`` and ``females`` + as keys and specimen weight in grams as values. + + Returns: + Tuple containing list of average weights of generations and number + of generations before meeting target weight or generation limit in + **limits**. + + Examples: + >>> from src.ch07.c1_breed_rats import BreedRats + >>> sample_one = BreedRats() + >>> s1_population = sample_one.get_population() + >>> ave_wt, generations = sample_one.breed_rats(s1_population) + >>> print(generations) + 248 + + """ + generations = 0 + ave_wt = [] + match = self.measure(population) + + while match < 1 and generations < self.gen_limit: + population = self.select(population) + litter = self.crossover(population) + litter = self.mutate(litter) + for gender in litter: + population[gender].extend(litter[gender]) + match = self.measure(population) + print(f'Generation {generations} match: {match * 100:.4f}%') + + ave_wt.append(int(statistics.mean(self.combine_values(population)))) + generations += 1 + return ave_wt, generations def main(): From eed33f8ab8103d56e9787655f22f112e5f8c7d82 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 18:13:35 -0500 Subject: [PATCH 39/73] Trim module docstring to reflect refactoring --- src/ch07/c1_breed_rats.py | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 4b96346..2a96b3f 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -2,38 +2,6 @@ Use genetic algorithm on a mixed population of male and female rats. -Running :func:`main` will output demonstration simulation and the time (in -seconds) it took to run. - -Weights and number of each gender vary and can be set by modifying the -following: - -Attributes: - TARGET_WT (int): Target weight in grams. Default is ``50000``. - NUM_MALES (int): Number of male rats in population. Default is ``4``. - NUM_FEMALES (int): Number of female rats in population. - Default is ``16``. - INIT_MIN_WT (int): Minimum weight of adult rat in initial population. - Default is ``200``. - INIT_MAX_WT (int): Maximum weight of adult rat in initial population. - Default is ``600``. - INIT_MALE_MODE_WT (int): Most common adult male rat weight in initial - population. Default is ``300``. - INIT_FEMALE_MODE_WT (int): Most common adult female rat weight in initial - population. Default is ``250``. - MUT_ODDS (float): Probability of a mutation occurring in a pup. - Default is ``0.01``. - MUT_MIN (float): Scalar on rat weight of least beneficial mutation. - Default is ``0.5``. - MUT_MAX (float): Scalar on rat weight of most beneficial mutation. - Default is ``1.2``. - LITTER_SZ (int): Number of pups per pair of breeding rats. - Default is ``8``. - LITTERS_PER_YR (int): Number of litters per year per pair of breeding rats. - Default is ``10``. - GEN_LIMIT (int): Generational cutoff to stop breeding program. - Default is ``500``. - """ import time import random From a6c0a1981f0b73d8a972191a9336c6503b4098d2 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 18:15:19 -0500 Subject: [PATCH 40/73] Refactor main function to use BreedRats class. --- src/ch07/c1_breed_rats.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 2a96b3f..6dba952 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -363,31 +363,26 @@ def breed_rats(self, population: dict) -> tuple: def main(): - """Demonstrate breed_rats function. + """Demonstrate BreedRats class. - Wrap :func:`populate` and :func:`breed_rats` with module - constants and then display time to run. + Use default values to run a demonstration simulation and display time + (in seconds) it took to run. """ start_time = time.time() + experiment = BreedRats() - population = { - 'males': populate(NUM_MALES, INIT_MIN_WT, INIT_MAX_WT, - INIT_MALE_MODE_WT), - 'females': populate(NUM_FEMALES, INIT_MIN_WT, INIT_MAX_WT, - INIT_FEMALE_MODE_WT) - } - match = measure(population, TARGET_WT) + population = experiment.get_population() + match = experiment.measure(population) + print(f'Initial population: {population}') print(f'Initial population match: {match * 100}%') - print(f'Number of males, females to keep: {NUM_MALES}, {NUM_FEMALES}') - ave_wt, generations = breed_rats(population, - (TARGET_WT, GEN_LIMIT), - (NUM_MALES, NUM_FEMALES, LITTER_SZ), - (MUT_ODDS, MUT_MIN, MUT_MAX)) + print(f'Number of males, females to keep: {experiment.num_males}, ' + f'{experiment.num_females}') + ave_wt, generations = experiment.breed_rats(population) print(f'Average weight per generation: {ave_wt}') print(f'\nNumber of generations: {generations}') - print(f'Number of years: {int(generations/LITTERS_PER_YR)}') + print(f'Number of years: {int(generations/experiment.litters_per_yr)}') end_time = time.time() duration = end_time - start_time From a1f66d237d4ceead645ce095c8f8bfd89e3ef1c1 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 18:18:46 -0500 Subject: [PATCH 41/73] Update populate docstring to reflect new arguments --- src/ch07/c1_breed_rats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 6dba952..9cf493b 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -148,7 +148,7 @@ def populate(self, pop_total: int, mode_wt: int) -> list: """Generate population with a triangular distribution of weights. Use :py:mod:`~random.triangular` to generate a population with a triangular - distribution of weights based on *minimum_wt*, *maximum_wt*, and *mode_wt*. + distribution of weights based on **mode_wt**. Args: pop_total (int): Total number of rats in population. From 94dddd705e96c82983b699ff06eb43a047680ce8 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 18:21:44 -0500 Subject: [PATCH 42/73] Update select docstring to reflect new arguments --- src/ch07/c1_breed_rats.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 9cf493b..f2d64ac 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -228,14 +228,14 @@ def select(self, population: dict) -> dict: """Select largest members of population. Sort members in descending order, and then keep largest members up to - **to_keep**. + instance values for **num_males** and **num_females**. Args: population (dict): Dictionary of lists with ``males`` and ``females`` as keys and specimen weight in grams as values. Returns: - Dictionary of lists of length **to_keep** of largest members of + Dictionary of lists of specified length of largest members of **population**. Examples: From f6b4b036916a9007353594b0a3f8624a056ba438 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 18:23:00 -0500 Subject: [PATCH 43/73] Update crossover docstring to reflect new arguments --- src/ch07/c1_breed_rats.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index f2d64ac..97a4708 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -261,8 +261,9 @@ def select(self, population: dict) -> dict: def crossover(self, population: dict) -> dict: """Crossover genes among members (weights) of a population. - Breed population where each breeding pair produces a litter - of **litter_sz** pups. Pup's gender is assigned randomly. + Breed **population** where each breeding pair produces a litter + of instance value for **_litter_sz** pups. Pup's gender is assigned + randomly. To accommodate mismatched pairs, breeding pairs are selected randomly, and once paired, females are removed from the breeding pool while From 68174493bb85e87126ed1086f0457f858a43f9c8 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 18:23:54 -0500 Subject: [PATCH 44/73] Update mutate docstring to reflect new arguments --- src/ch07/c1_breed_rats.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 97a4708..0d13710 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -303,8 +303,9 @@ def mutate(self, litter): """Randomly alter pup weights applying input odds as a scalar. For each pup in **litter**, randomly decide if a floating point number - between **mut_min** and **mut_max** from :py:mod:`~random.uniform` will - be used as a scalar to modified their weight. + between instance values for **_mut_min** and **_mut_max** from + :py:mod:`~random.uniform` will be used as a scalar to modified their + weight. Args: litter (dict): Dictionary of lists with ``males`` and ``females`` From 9f4618608e43668f63444386eca2f908d0b066a3 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 18:26:08 -0500 Subject: [PATCH 45/73] Add type hints to mutate --- src/ch07/c1_breed_rats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 0d13710..a3d3e9a 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -299,7 +299,7 @@ def crossover(self, population: dict) -> dict: value.sort() return litter - def mutate(self, litter): + def mutate(self, litter: dict) -> dict: """Randomly alter pup weights applying input odds as a scalar. For each pup in **litter**, randomly decide if a floating point number From 71da3e8fbb15c1eba13d569d474a625a28296f84 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 18:29:50 -0500 Subject: [PATCH 46/73] Update breed_rats docstring to reflect new arguments --- src/ch07/c1_breed_rats.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index a3d3e9a..6f6197e 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -326,7 +326,7 @@ def breed_rats(self, population: dict) -> tuple: """Simulate genetic algorithm by breeding rats. Using **population**, repeat cycle of measure, select, crossover, - and mutate until either of **limits** are met. + and mutate until either **target_wt** or **gen_limit** are met. Args: population (dict): Dictionary of lists with ``males`` and ``females`` @@ -334,8 +334,7 @@ def breed_rats(self, population: dict) -> tuple: Returns: Tuple containing list of average weights of generations and number - of generations before meeting target weight or generation limit in - **limits**. + of generations. Examples: >>> from src.ch07.c1_breed_rats import BreedRats From 8cf86e02059f62f99314d651f342ec2e70693723 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 18:30:31 -0500 Subject: [PATCH 47/73] Rename breed_rats method as simulate --- src/ch07/c1_breed_rats.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 6f6197e..2bd2d49 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -322,7 +322,7 @@ def mutate(self, litter: dict) -> dict: pups[index] = round(pup * random.uniform(self._mut_min, self._mut_max)) return litter - def breed_rats(self, population: dict) -> tuple: + def simulate(self, population: dict) -> tuple: """Simulate genetic algorithm by breeding rats. Using **population**, repeat cycle of measure, select, crossover, @@ -340,7 +340,7 @@ def breed_rats(self, population: dict) -> tuple: >>> from src.ch07.c1_breed_rats import BreedRats >>> sample_one = BreedRats() >>> s1_population = sample_one.get_population() - >>> ave_wt, generations = sample_one.breed_rats(s1_population) + >>> ave_wt, generations = sample_one.simulate(s1_population) >>> print(generations) 248 @@ -379,7 +379,7 @@ def main(): print(f'Initial population match: {match * 100}%') print(f'Number of males, females to keep: {experiment.num_males}, ' f'{experiment.num_females}') - ave_wt, generations = experiment.breed_rats(population) + ave_wt, generations = experiment.simulate(population) print(f'Average weight per generation: {ave_wt}') print(f'\nNumber of generations: {generations}') From be1e120524d882213c849d6b0924ead2063d1817 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 19:12:02 -0500 Subject: [PATCH 48/73] Fix line-too-long per pylint C0301 --- src/ch07/c1_breed_rats.py | 45 ++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 2bd2d49..0512a27 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -42,8 +42,8 @@ class BreedRats(object): Default is ``1.2``. _litter_sz (int): Number of pups per pair of breeding rats. Default is ``8``. - _litters_per_yr (int): Number of litters per year per pair of breeding rats. - Default is ``10``. + _litters_per_yr (int): Number of litters per year per pair of + breeding rats. Default is ``10``. """ def __init__(self, num_males: int = 4, num_females: int = 16, @@ -147,8 +147,8 @@ def litters_per_yr(self, value: int): def populate(self, pop_total: int, mode_wt: int) -> list: """Generate population with a triangular distribution of weights. - Use :py:mod:`~random.triangular` to generate a population with a triangular - distribution of weights based on **mode_wt**. + Use :py:mod:`~random.triangular` to generate a population with a + triangular distribution of weights based on **mode_wt**. Args: pop_total (int): Total number of rats in population. @@ -213,12 +213,12 @@ def measure(self, population: dict) -> float: determine if goal has been met. Args: - population (dict): Dictionary of lists with ``males`` and ``females`` - as keys and specimen weight in grams as values. + population (dict): Dictionary of lists with ``males`` and + ``females`` as keys and specimen weight in grams as values. Returns: - :py:obj:`float` representing decimal percentage of completion where a - value of ``1`` is ``100%``, or complete. + :py:obj:`float` representing decimal percentage of completion + where a value of ``1`` is ``100%``, or complete. """ mean = statistics.mean(self.combine_values(population)) @@ -231,8 +231,8 @@ def select(self, population: dict) -> dict: instance values for **num_males** and **num_females**. Args: - population (dict): Dictionary of lists with ``males`` and ``females`` - as keys and specimen weight in grams as values. + population (dict): Dictionary of lists with ``males`` and + ``females`` as keys and specimen weight in grams as values. Returns: Dictionary of lists of specified length of largest members of @@ -251,11 +251,13 @@ def select(self, population: dict) -> dict: new_population = {'males': [], 'females': []} for gender in population: if gender == 'males': - new_population[gender].extend(sorted(population[gender], - reverse=True)[:self.num_males]) + new_population[gender].extend( + sorted(population[gender], + reverse=True)[:self.num_males]) else: - new_population[gender].extend(sorted(population[gender], - reverse=True)[:self.num_females]) + new_population[gender].extend( + sorted(population[gender], + reverse=True)[:self.num_females]) return new_population def crossover(self, population: dict) -> dict: @@ -270,8 +272,8 @@ def crossover(self, population: dict) -> dict: males remain. Args: - population (dict): Dictionary of lists with ``males`` and ``females`` - as keys and specimen weight in grams as values. + population (dict): Dictionary of lists with ``males`` and + ``females`` as keys and specimen weight in grams as values. Returns: Dictionary of lists with ``males`` and ``females`` as keys and @@ -319,7 +321,9 @@ def mutate(self, litter: dict) -> dict: pups = litter[gender] for index, pup in enumerate(pups): if self._mut_odds >= random.random(): - pups[index] = round(pup * random.uniform(self._mut_min, self._mut_max)) + pups[index] = round(pup * + random.uniform(self._mut_min, + self._mut_max)) return litter def simulate(self, population: dict) -> tuple: @@ -329,8 +333,8 @@ def simulate(self, population: dict) -> tuple: and mutate until either **target_wt** or **gen_limit** are met. Args: - population (dict): Dictionary of lists with ``males`` and ``females`` - as keys and specimen weight in grams as values. + population (dict): Dictionary of lists with ``males`` and + ``females`` as keys and specimen weight in grams as values. Returns: Tuple containing list of average weights of generations and number @@ -358,7 +362,8 @@ def simulate(self, population: dict) -> tuple: match = self.measure(population) print(f'Generation {generations} match: {match * 100:.4f}%') - ave_wt.append(int(statistics.mean(self.combine_values(population)))) + ave_wt.append(int(statistics.mean( + self.combine_values(population)))) generations += 1 return ave_wt, generations From a0e12f29d4e18e46ce73b70349fe9130a10f8102 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 19:13:54 -0500 Subject: [PATCH 49/73] Fix useless-object-inheritance per pylint R0205 --- src/ch07/c1_breed_rats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 0512a27..6b63503 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -8,7 +8,7 @@ import statistics -class BreedRats(object): +class BreedRats: """Efficiently breed rats to an average weight of **target_wt**. Use genetic algorithm on a mixed population of male and female rats. From b9c073ea50d8eec795999d6677729990b577ac0b Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 19:22:15 -0500 Subject: [PATCH 50/73] Locally disable too-many-instance-attributes per pylint R0902 --- src/ch07/c1_breed_rats.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 6b63503..73e9dbf 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -46,6 +46,13 @@ class BreedRats: breeding rats. Default is ``10``. """ + + # pylint: disable=too-many-instance-attributes + # Limit is 7, but analysis like this requires many constants. + # I am opting to make them modifiable in something that is isn't a + # dictionary. + # If there is a better way, please submit an issue for discussion. + def __init__(self, num_males: int = 4, num_females: int = 16, target_wt: int = 50000, gen_limit: int = 500): self._min_wt = 200 From 3eeaf1c4928d2cd175829055020926ac0b89280e Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 19:26:32 -0500 Subject: [PATCH 51/73] Update docstrings to use property values --- src/ch07/c1_breed_rats.py | 51 +++++++++++++++------------------------ 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 73e9dbf..f809e82 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -25,26 +25,6 @@ class BreedRats: gen_limit (int): Generational cutoff to stop breeding program. Default is ``500``. - Attributes: - _min_wt (int): Minimum weight of adult rat in initial population. - Default is ``200``. - _max_wt (int): Maximum weight of adult rat in initial population. - Default is ``600``. - _male_mode_wt (int): Most common adult male rat weight in initial - population. Default is ``300``. - _female_mode_wt (int): Most common adult female rat weight in initial - population. Default is ``250``. - _mut_odds (float): Probability of a mutation occurring in a pup. - Default is ``0.01``. - _mut_min (float): Scalar on rat weight of least beneficial mutation. - Default is ``0.5``. - _mut_max (float): Scalar on rat weight of most beneficial mutation. - Default is ``1.2``. - _litter_sz (int): Number of pups per pair of breeding rats. - Default is ``8``. - _litters_per_yr (int): Number of litters per year per pair of - breeding rats. Default is ``10``. - """ # pylint: disable=too-many-instance-attributes @@ -72,7 +52,8 @@ def __init__(self, num_males: int = 4, num_females: int = 16, @property def min_wt(self): - """int: Minimum weight of adult rat in initial population.""" + """int: Minimum weight of adult rat in initial population. + Default is ``200``.""" return self._min_wt @min_wt.setter @@ -81,7 +62,8 @@ def min_wt(self, value: int): @property def max_wt(self): - """int: Maximum weight of adult rat in initial population.""" + """int: Maximum weight of adult rat in initial population. + Default is ``600``.""" return self._max_wt @max_wt.setter @@ -90,7 +72,8 @@ def max_wt(self, value: int): @property def male_mode_wt(self): - """int: Most common adult male rat weight in initial population.""" + """int: Most common adult male rat weight in initial population. + Default is ``300``.""" return self._male_mode_wt @male_mode_wt.setter @@ -99,7 +82,8 @@ def male_mode_wt(self, value: int): @property def female_mode_wt(self): - """int: Most common adult female rat weight in initial population.""" + """int: Most common adult female rat weight in initial population. + Default is ``250``.""" return self._female_mode_wt @female_mode_wt.setter @@ -108,7 +92,8 @@ def female_mode_wt(self, value: int): @property def mut_odds(self): - """float: Probability of a mutation occurring in a pup.""" + """float: Probability of a mutation occurring in a pup. + Default is ``0.01``.""" return self._mut_odds @mut_odds.setter @@ -117,7 +102,8 @@ def mut_odds(self, value: float): @property def mut_min(self): - """float: Scalar on rat weight of least beneficial mutation.""" + """float: Scalar on rat weight of least beneficial mutation. + Default is ``0.5``.""" return self._mut_min @mut_min.setter @@ -126,7 +112,8 @@ def mut_min(self, value: float): @property def mut_max(self): - """float: Scalar on rat weight of most beneficial mutation.""" + """float: Scalar on rat weight of most beneficial mutation. + Default is ``1.2``.""" return self._mut_max @mut_max.setter @@ -135,7 +122,8 @@ def mut_max(self, value: float): @property def litter_sz(self): - """int: Number of pups per pair of breeding rats.""" + """int: Number of pups per pair of breeding rats. + Default is ``8``.""" return self._litter_sz @litter_sz.setter @@ -144,7 +132,8 @@ def litter_sz(self, value: int): @property def litters_per_yr(self): - """int: Number of litters per year per pair of breeding rats.""" + """int: Number of litters per year per pair of breeding rats. + Default is ``10``.""" return self._litters_per_yr @litters_per_yr.setter @@ -271,7 +260,7 @@ def crossover(self, population: dict) -> dict: """Crossover genes among members (weights) of a population. Breed **population** where each breeding pair produces a litter - of instance value for **_litter_sz** pups. Pup's gender is assigned + of instance value for **litter_sz** pups. Pup's gender is assigned randomly. To accommodate mismatched pairs, breeding pairs are selected randomly, @@ -312,7 +301,7 @@ def mutate(self, litter: dict) -> dict: """Randomly alter pup weights applying input odds as a scalar. For each pup in **litter**, randomly decide if a floating point number - between instance values for **_mut_min** and **_mut_max** from + between instance values for **mut_min** and **mut_max** from :py:mod:`~random.uniform` will be used as a scalar to modified their weight. From 659c445521dafdaf636366900bc51c9a6ec991d7 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 19:31:11 -0500 Subject: [PATCH 52/73] Fix missing blank line and triple quotes on separate line per pydocstyle D205 and D209 --- src/ch07/c1_breed_rats.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index f809e82..55f4736 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -53,7 +53,9 @@ def __init__(self, num_males: int = 4, num_females: int = 16, @property def min_wt(self): """int: Minimum weight of adult rat in initial population. - Default is ``200``.""" + + Default is ``200``. + """ return self._min_wt @min_wt.setter @@ -63,7 +65,9 @@ def min_wt(self, value: int): @property def max_wt(self): """int: Maximum weight of adult rat in initial population. - Default is ``600``.""" + + Default is ``600``. + """ return self._max_wt @max_wt.setter @@ -73,7 +77,9 @@ def max_wt(self, value: int): @property def male_mode_wt(self): """int: Most common adult male rat weight in initial population. - Default is ``300``.""" + + Default is ``300``. + """ return self._male_mode_wt @male_mode_wt.setter @@ -83,7 +89,9 @@ def male_mode_wt(self, value: int): @property def female_mode_wt(self): """int: Most common adult female rat weight in initial population. - Default is ``250``.""" + + Default is ``250``. + """ return self._female_mode_wt @female_mode_wt.setter @@ -93,7 +101,9 @@ def female_mode_wt(self, value: int): @property def mut_odds(self): """float: Probability of a mutation occurring in a pup. - Default is ``0.01``.""" + + Default is ``0.01``. + """ return self._mut_odds @mut_odds.setter @@ -103,7 +113,9 @@ def mut_odds(self, value: float): @property def mut_min(self): """float: Scalar on rat weight of least beneficial mutation. - Default is ``0.5``.""" + + Default is ``0.5``. + """ return self._mut_min @mut_min.setter @@ -113,7 +125,9 @@ def mut_min(self, value: float): @property def mut_max(self): """float: Scalar on rat weight of most beneficial mutation. - Default is ``1.2``.""" + + Default is ``1.2``. + """ return self._mut_max @mut_max.setter @@ -123,7 +137,9 @@ def mut_max(self, value: float): @property def litter_sz(self): """int: Number of pups per pair of breeding rats. - Default is ``8``.""" + + Default is ``8``. + """ return self._litter_sz @litter_sz.setter @@ -133,7 +149,9 @@ def litter_sz(self, value: int): @property def litters_per_yr(self): """int: Number of litters per year per pair of breeding rats. - Default is ``10``.""" + + Default is ``10``. + """ return self._litters_per_yr @litters_per_yr.setter From 1212a8532ec5412e1594ca244a45b6a9d66f5c9d Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 19:38:03 -0500 Subject: [PATCH 53/73] Fix missing docstring in public method per pydocstyle D107 --- src/ch07/c1_breed_rats.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 55f4736..9b2d150 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -35,6 +35,7 @@ class BreedRats: def __init__(self, num_males: int = 4, num_females: int = 16, target_wt: int = 50000, gen_limit: int = 500): + """Initialize class.""" self._min_wt = 200 self._max_wt = 600 self._male_mode_wt = 300 From a42a292c7d0f9c681d08b9a73a7c7b74b680dda8 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 19:42:19 -0500 Subject: [PATCH 54/73] Add test_defaults to TestBreedRats --- tests/test_chapter07.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index b1c5f26..57cbba5 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -15,6 +15,23 @@ def setUpClass(cls): """Configure attributes for use in this class only.""" cls.random = Random() + def test_defaults(self): + """Test default property values.""" + experiment = breed_rats.BreedRats() + self.assertEqual(experiment.num_males, 4) + self.assertEqual(experiment.num_females, 16) + self.assertEqual(experiment.target_wt, 50000) + self.assertEqual(experiment.gen_limit, 500) + self.assertEqual(experiment.min_wt, 200) + self.assertEqual(experiment.max_wt, 600) + self.assertEqual(experiment.male_mode_wt, 300) + self.assertEqual(experiment.female_mode_wt, 250) + self.assertEqual(experiment.mut_odds, 0.01) + self.assertEqual(experiment.mut_min, 0.5) + self.assertEqual(experiment.mut_max, 1.2) + self.assertEqual(experiment.litters_per_yr, 10) + self.assertEqual(experiment.litter_sz, 8) + @unittest.mock.patch('src.ch07.c1_breed_rats.random') def test_populate(self, mock_random): """Test populate.""" From 59ce18ffe5eee62fbbd4669d95b21699a2e02680 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 19:44:49 -0500 Subject: [PATCH 55/73] Remove maxDiff from test_main in TestBreedRats --- tests/test_chapter07.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 57cbba5..bafc856 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -258,7 +258,6 @@ def test_breed_rats(self, mock_stdout, mock_random): @unittest.mock.patch('sys.stdout', new_callable=StringIO) def test_main(self, mock_stdout, mock_random, mock_time): """Test main.""" - self.maxDiff = None # Patch out variances. mock_random.seed(311) mock_time.time.return_value = 12345 From 14f9e00b76adaef0332e3b29540e8d218c4dd56f Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 19:45:36 -0500 Subject: [PATCH 56/73] Add initial population output --- tests/data/ch07/main/breed_rats.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/data/ch07/main/breed_rats.txt b/tests/data/ch07/main/breed_rats.txt index ae00834..a2c5f4e 100644 --- a/tests/data/ch07/main/breed_rats.txt +++ b/tests/data/ch07/main/breed_rats.txt @@ -1,3 +1,4 @@ +Initial population: {'males': [453, 301, 253, 339], 'females': [535, 255, 349, 344, 271, 291, 288, 410, 368, 405, 394, 284, 456, 355, 365, 278]} Initial population match: 0.6993999999999999% Number of males, females to keep: 4, 16 Generation 0 match: 0.6763% From 0a161acff194bc404f67bca1a60ee27b99aa134b Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 19:50:37 -0500 Subject: [PATCH 57/73] Update test_populate in TestBreedRats to use class --- tests/test_chapter07.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index bafc856..b233ed5 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -38,7 +38,10 @@ def test_populate(self, mock_random): # Patch random.triangular to use non-random seed. self.random.seed(512) mock_random.triangular._mock_side_effect = self.random.triangular - test_pop = breed_rats.populate(10, 100, 300, 200) + experiment = breed_rats.BreedRats() + experiment.min_wt = 100 + experiment.max_wt = 300 + test_pop = experiment.populate(10, 200) expected_pop = [119, 193, 181, 190, 261, 190, 158, 169, 109, 229] self.assertListEqual(test_pop, expected_pop) From bb32661a5ad1fd6722499db77513288d6a953b99 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 19:51:59 -0500 Subject: [PATCH 58/73] Update test_combine_values in TestBreedRats to use class --- tests/test_chapter07.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index b233ed5..408ad3a 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -51,7 +51,8 @@ def test_combine_values(self): 'first': [1, 2, 3, 4, 5], 'second': [6, 7, 8, 9, 0] } - combined = breed_rats.combine_values(dictionary) + experiment = breed_rats.BreedRats() + combined = experiment.combine_values(dictionary) expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] self.assertListEqual(combined, expected) From 5ace618490dce556a9cec2f8732634125873a8d1 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 19:53:47 -0500 Subject: [PATCH 59/73] Update test_measure in TestBreedRats to use class --- tests/test_chapter07.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 408ad3a..57cdf3e 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -62,7 +62,8 @@ def test_measure(self): 'males': [219, 293, 281, 290, 361, 290, 258, 269, 309, 329], 'females': [119, 193, 181, 190, 261, 190, 158, 169, 109, 229] } - completion = breed_rats.measure(population, 500) + experiment = breed_rats.BreedRats(target_wt=500) + completion = experiment.measure(population) self.assertEqual(completion, 0.4698) def test_select(self): From bb1751dd8911ab1766a0f36153828a33a30e7930 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 19:58:33 -0500 Subject: [PATCH 60/73] Update test_select in TestBreedRats to use class --- tests/test_chapter07.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 57cdf3e..7584c8f 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -72,26 +72,35 @@ def test_select(self): 'males': [219, 293, 281, 290, 361, 290, 258, 269, 309, 329], 'females': [119, 193, 181, 190, 261, 190, 158, 169, 209, 229] } + experiment = breed_rats.BreedRats() # Test even numbered populations. - test_population = breed_rats.select(population, (2, 2)) + experiment.num_males = 2 + experiment.num_females = 2 + test_population = experiment.select(population) expected_population = { 'males': [361, 329], 'females': [261, 229] } self.assertDictEqual(test_population, expected_population) - test_population = breed_rats.select(population, (4, 4)) + experiment.num_males = 4 + experiment.num_females = 4 + test_population = experiment.select(population) expected_population = { 'males': [361, 329, 309, 293], 'females': [261, 229, 209, 193] } self.assertDictEqual(test_population, expected_population) # Test odd numbered populations. - test_population = breed_rats.select(population, (3, 3)) + experiment.num_males = 3 + experiment.num_females = 3 + test_population = experiment.select(population) expected_population = { 'males': [361, 329, 309], 'females': [261, 229, 209] } self.assertDictEqual(test_population, expected_population) - test_population = breed_rats.select(population, (5, 5)) + experiment.num_males = 5 + experiment.num_females = 5 + test_population = experiment.select(population) expected_population = { 'males': [361, 329, 309, 293, 290], 'females': [261, 229, 209, 193, 190] From 073a52da354ce0a293c970c3b71dfdc0b961c301 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 20:03:16 -0500 Subject: [PATCH 61/73] Update test_crossover in TestBreedRats to use class --- tests/test_chapter07.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 7584c8f..6934a28 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -120,8 +120,9 @@ def test_crossover(self, mock_random): 'males': [219, 293, 281], 'females': [119, 193, 181] } - litter_sz = 8 - litter = breed_rats.crossover(population, litter_sz) + experiment = breed_rats.BreedRats() + experiment.litter_sz = 8 + litter = experiment.crossover(population) expected_litter = { 'males': [128, 148, 196, 197, 201, 206, 213, 214, 256, 269], 'females': [120, 160, 170, 182, 187, 193, 196, 197, 203, @@ -130,15 +131,14 @@ def test_crossover(self, mock_random): self.assertDictEqual(litter, expected_litter) litter_total = sum([len(value) for value in litter.values()]) self.assertEqual(litter_total, - litter_sz * len(population['females'])) + experiment.litter_sz * len(population['females'])) # Test fewer males than females. population = { 'males': [219, 293], 'females': [119, 193, 181] } - litter_sz = 8 - litter = breed_rats.crossover(population, litter_sz) + litter = experiment.crossover(population) expected_litter = { 'males': [165, 190, 208, 210, 245, 257, 280, 287], 'females': [128, 140, 179, 181, 182, 182, 184, 187, @@ -147,15 +147,14 @@ def test_crossover(self, mock_random): self.assertDictEqual(litter, expected_litter) litter_total = sum([len(value) for value in litter.values()]) self.assertEqual(litter_total, - litter_sz * len(population['females'])) + experiment.litter_sz * len(population['females'])) # Test fewer females than males. population = { 'males': [219, 293], 'females': [119] } - litter_sz = 8 - litter = breed_rats.crossover(population, litter_sz) + litter = experiment.crossover(population) expected_litter = { 'males': [162, 201, 265], 'females': [205, 228, 254, 261, 282] @@ -163,15 +162,15 @@ def test_crossover(self, mock_random): self.assertDictEqual(litter, expected_litter) litter_total = sum([len(value) for value in litter.values()]) self.assertEqual(litter_total, - litter_sz * len(population['females'])) + experiment.litter_sz * len(population['females'])) # Test different litter size. population = { 'males': [219, 293], 'females': [119] } - litter_sz = 3 - litter = breed_rats.crossover(population, litter_sz) + experiment.litter_sz = 3 + litter = experiment.crossover(population) expected_litter = { 'males': [167, 181], 'females': [291] @@ -179,15 +178,14 @@ def test_crossover(self, mock_random): self.assertDictEqual(litter, expected_litter) litter_total = sum([len(value) for value in litter.values()]) self.assertEqual(litter_total, - litter_sz * len(population['females'])) + experiment.litter_sz * len(population['females'])) # Test larger female than males. population = { 'males': [119, 193], 'females': [219] } - litter_sz = 3 - litter = breed_rats.crossover(population, litter_sz) + litter = experiment.crossover(population) expected_litter = { 'males': [139, 150], 'females': [119] @@ -195,7 +193,7 @@ def test_crossover(self, mock_random): self.assertDictEqual(litter, expected_litter) litter_total = sum([len(value) for value in litter.values()]) self.assertEqual(litter_total, - litter_sz * len(population['females'])) + experiment.litter_sz * len(population['females'])) @unittest.mock.patch('src.ch07.c1_breed_rats.random') def test_mutate(self, mock_random): From b7983eb4744f8f5fb961561e3394ba5608139df4 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 20:06:55 -0500 Subject: [PATCH 62/73] Update test_mutate in TestBreedRats to use class --- tests/test_chapter07.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 6934a28..d965ab7 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -203,13 +203,15 @@ def test_mutate(self, mock_random): mock_random.random._mock_side_effect = self.random.random mock_random.uniform._mock_side_effect = self.random.uniform + experiment = breed_rats.BreedRats() + # Test large litter with low mutation chance. litter = { 'males': [165, 190, 208, 210, 245, 257, 280, 287], 'females': [128, 140, 179, 181, 182, 182, 184, 187, 187, 194, 201, 206, 216, 241, 281, 290] } - mutated_litter = breed_rats.mutate(litter, 0.01, 0.5, 1.2) + mutated_litter = experiment.mutate(litter) expected = { 'males': [165, 190, 208, 210, 245, 257, 280, 287], 'females': [128, 140, 179, 181, 182, 182, 184, 187, @@ -222,7 +224,8 @@ def test_mutate(self, mock_random): 'males': [162, 201, 265], 'females': [205, 228, 254, 261, 282] } - mutated_litter = breed_rats.mutate(litter, 0.90, 0.5, 1.2) + experiment.mut_odds = 0.90 + mutated_litter = experiment.mutate(litter) expected = { 'males': [95, 201, 265], 'females': [179, 130, 267, 211, 261] @@ -234,7 +237,9 @@ def test_mutate(self, mock_random): 'males': [162, 201, 265], 'females': [205, 228, 254, 261, 282] } - mutated_litter = breed_rats.mutate(litter, 0.90, 2.0, 3.0) + experiment.mut_min = 2.0 + experiment.mut_max = 3.0 + mutated_litter = experiment.mutate(litter) expected = { 'males': [338, 442, 655], 'females': [469, 666, 254, 612, 789] From 6b3de8e892777579d225f279387cc0a9c8a4be9c Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 20:11:24 -0500 Subject: [PATCH 63/73] Update test_breed_rats in TestBreedRats to use class --- tests/test_chapter07.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index d965ab7..531f0cc 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -257,9 +257,12 @@ def test_breed_rats(self, mock_stdout, mock_random): 'males': [450, 320, 510], 'females': [250, 300, 220, 160] } - ave, generations = breed_rats.breed_rats(population, (20000, 500), - (3, 10, 8), - (0.75, .75, 1.5)) + experiment = breed_rats.BreedRats(num_males=3, num_females=10, + target_wt=20000, gen_limit=500) + experiment.mut_odds = 0.75 + experiment.mut_min = 0.75 + experiment.mut_max = 1.5 + ave, generations = experiment.simulate(population) self.assertEqual(generations, 12) self.assertEqual(ave, [347, 564, 861, 1181, 1636, 2344, 3319, 4950, 7234, 10464, 15115, 21703]) From 9239c7403dfd2e718e3e3fd3330aafd9a6d54e0c Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Fri, 11 Oct 2019 20:11:49 -0500 Subject: [PATCH 64/73] Rename test_breed_rats in TestBreedRats to test_simulate --- tests/test_chapter07.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 531f0cc..95ff570 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -248,8 +248,8 @@ def test_mutate(self, mock_random): @unittest.mock.patch('src.ch07.c1_breed_rats.random', new_callable=Random) @unittest.mock.patch('sys.stdout', new_callable=StringIO) - def test_breed_rats(self, mock_stdout, mock_random): - """Test breed_rats.""" + def test_simulate(self, mock_stdout, mock_random): + """Test simulate.""" # Patch random to use non-random seed. mock_random.seed(311) From 31df563d427cd8f8593d412d9823dd9e229f1d8c Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Sat, 12 Oct 2019 17:09:17 -0500 Subject: [PATCH 65/73] Make class arguments private variables --- src/ch07/c1_breed_rats.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 9b2d150..621b0dc 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -46,10 +46,10 @@ def __init__(self, num_males: int = 4, num_females: int = 16, self._litter_sz = 8 self._litters_per_yr = 10 - self.num_males = num_males - self.num_females = num_females - self.target_wt = target_wt - self.gen_limit = gen_limit + self._num_males = num_males + self._num_females = num_females + self._target_wt = target_wt + self._gen_limit = gen_limit @property def min_wt(self): @@ -194,9 +194,9 @@ def get_population(self, num_males: int = None, """ if num_males is None: - num_males = self.num_males + num_males = self._num_males if num_females is None: - num_females = self.num_females + num_females = self._num_females population = { 'males': self.populate(num_males, self._male_mode_wt), 'females': self.populate(num_females, self._female_mode_wt) @@ -237,7 +237,7 @@ def measure(self, population: dict) -> float: """ mean = statistics.mean(self.combine_values(population)) - return mean / self.target_wt + return mean / self._target_wt def select(self, population: dict) -> dict: """Select largest members of population. @@ -268,11 +268,11 @@ def select(self, population: dict) -> dict: if gender == 'males': new_population[gender].extend( sorted(population[gender], - reverse=True)[:self.num_males]) + reverse=True)[:self._num_males]) else: new_population[gender].extend( sorted(population[gender], - reverse=True)[:self.num_females]) + reverse=True)[:self._num_females]) return new_population def crossover(self, population: dict) -> dict: @@ -368,7 +368,7 @@ def simulate(self, population: dict) -> tuple: ave_wt = [] match = self.measure(population) - while match < 1 and generations < self.gen_limit: + while match < 1 and generations < self._gen_limit: population = self.select(population) litter = self.crossover(population) litter = self.mutate(litter) From be9c0cbcb1cc3372d4862478aaccf1278430c80b Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Sat, 12 Oct 2019 17:09:32 -0500 Subject: [PATCH 66/73] Add getters and setters for class arguments --- src/ch07/c1_breed_rats.py | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index 621b0dc..bf26284 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -159,6 +159,54 @@ def litters_per_yr(self): def litters_per_yr(self, value: int): self._litters_per_yr = value + @property + def num_males(self): + """int: Number of male rats in population. + + Default is ``4``. + """ + return self._num_males + + @num_males.setter + def num_males(self, value: int): + self._num_males = value + + @property + def num_females(self): + """int: Number of female rats in population. + + Default is ``16``. + """ + return self._num_females + + @num_females.setter + def num_females(self, value: int): + self._num_females = value + + @property + def target_wt(self): + """int: Target weight in grams. + + Default is ``50000``. + """ + return self._target_wt + + @target_wt.setter + def target_wt(self, value: int): + self._target_wt = value + + @property + def gen_limit(self): + """int: Generational cutoff to stop breeding program. + + Default is ``500``. + """ + return self._gen_limit + + @gen_limit.setter + def gen_limit(self, value: int): + self._gen_limit = value + def populate(self, pop_total: int, mode_wt: int) -> list: """Generate population with a triangular distribution of weights. From 987657a9a2608b74adce4b7930e2c9094446570c Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Sat, 12 Oct 2019 17:11:43 -0500 Subject: [PATCH 67/73] Locally disable pylint too-many-public-methods --- src/ch07/c1_breed_rats.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ch07/c1_breed_rats.py b/src/ch07/c1_breed_rats.py index bf26284..6b2715f 100644 --- a/src/ch07/c1_breed_rats.py +++ b/src/ch07/c1_breed_rats.py @@ -27,8 +27,9 @@ class BreedRats: """ - # pylint: disable=too-many-instance-attributes - # Limit is 7, but analysis like this requires many constants. + # pylint: disable=too-many-instance-attributes,too-many-public-methods + # Limit is for instance attributes and public methods are 7 and 20, + # but analysis like this requires many constants. # I am opting to make them modifiable in something that is isn't a # dictionary. # If there is a better way, please submit an issue for discussion. From 05dfa545d7271a53193275e06c2017742b156fe6 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Sat, 12 Oct 2019 17:16:35 -0500 Subject: [PATCH 68/73] Rename test_defaults in TestBreedRats to test_properties --- tests/test_chapter07.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 95ff570..35fce44 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -15,7 +15,7 @@ def setUpClass(cls): """Configure attributes for use in this class only.""" cls.random = Random() - def test_defaults(self): + def test_properties(self): """Test default property values.""" experiment = breed_rats.BreedRats() self.assertEqual(experiment.num_males, 4) From 231baa27134d4f49867f53b3528d8d275209e310 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Sat, 12 Oct 2019 17:25:44 -0500 Subject: [PATCH 69/73] Refactor test_properties docstring in TestBreedRats --- tests/test_chapter07.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 35fce44..67f8802 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -16,7 +16,7 @@ def setUpClass(cls): cls.random = Random() def test_properties(self): - """Test default property values.""" + """Test properties.""" experiment = breed_rats.BreedRats() self.assertEqual(experiment.num_males, 4) self.assertEqual(experiment.num_females, 16) From 3651609a8a6f8755cb0e7d215e7d8221143a8a19 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Sat, 12 Oct 2019 17:26:16 -0500 Subject: [PATCH 70/73] Add comment to test_properties in TestBreedRats separating default values test --- tests/test_chapter07.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 67f8802..4223fb6 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -18,6 +18,8 @@ def setUpClass(cls): def test_properties(self): """Test properties.""" experiment = breed_rats.BreedRats() + + # Test default property values. self.assertEqual(experiment.num_males, 4) self.assertEqual(experiment.num_females, 16) self.assertEqual(experiment.target_wt, 50000) From 8cb1fec670519b2c0b0d9555edcf807067645931 Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Sat, 12 Oct 2019 17:27:50 -0500 Subject: [PATCH 71/73] Add setter tests to test_properties in TestBreedRats --- tests/test_chapter07.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 4223fb6..41a15c6 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -34,6 +34,34 @@ def test_properties(self): self.assertEqual(experiment.litters_per_yr, 10) self.assertEqual(experiment.litter_sz, 8) + # Test setters. + experiment.num_males = 10 + self.assertEqual(experiment.num_males, 10) + experiment.num_females = 20 + self.assertEqual(experiment.num_females, 20) + experiment.target_wt = 20000 + self.assertEqual(experiment.target_wt, 20000) + experiment.gen_limit = 200 + self.assertEqual(experiment.gen_limit, 200) + experiment.min_wt = 250 + self.assertEqual(experiment.min_wt, 250) + experiment.max_wt = 700 + self.assertEqual(experiment.max_wt, 700) + experiment.male_mode_wt = 400 + self.assertEqual(experiment.male_mode_wt, 400) + experiment.female_mode_wt = 300 + self.assertEqual(experiment.female_mode_wt, 300) + experiment.mut_odds = 0.93 + self.assertEqual(experiment.mut_odds, 0.93) + experiment.mut_min = 2.5 + self.assertEqual(experiment.mut_min, 2.5) + experiment.mut_max = 3.0 + self.assertEqual(experiment.mut_max, 3.0) + experiment.litters_per_yr = 8 + self.assertEqual(experiment.litters_per_yr, 8) + experiment.litter_sz = 3 + self.assertEqual(experiment.litter_sz, 3) + @unittest.mock.patch('src.ch07.c1_breed_rats.random') def test_populate(self, mock_random): """Test populate.""" From 0857c4b23d92e0df305a2eb9f238468f50f42fec Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Sat, 12 Oct 2019 18:00:43 -0500 Subject: [PATCH 72/73] Refactor time.time patch in TestBreedRats test_main to return multiple values --- tests/test_chapter07.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_chapter07.py b/tests/test_chapter07.py index 41a15c6..ed6466c 100644 --- a/tests/test_chapter07.py +++ b/tests/test_chapter07.py @@ -310,7 +310,7 @@ def test_main(self, mock_stdout, mock_random, mock_time): """Test main.""" # Patch out variances. mock_random.seed(311) - mock_time.time.return_value = 12345 + mock_time.time.side_effect = [12345, 67890] breed_rats.main() From 4c2571e4bea2d6588f0db73a7bd205f36d49ffca Mon Sep 17 00:00:00 2001 From: JoseALermaIII Date: Sat, 12 Oct 2019 18:01:14 -0500 Subject: [PATCH 73/73] Update output to reflect changes --- tests/data/ch07/main/breed_rats.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/ch07/main/breed_rats.txt b/tests/data/ch07/main/breed_rats.txt index a2c5f4e..1911645 100644 --- a/tests/data/ch07/main/breed_rats.txt +++ b/tests/data/ch07/main/breed_rats.txt @@ -209,4 +209,4 @@ Average weight per generation: [338, 442, 479, 513, 530, 534, 539, 575, 590, 592 Number of generations: 204 Number of years: 20 -Runtime for this program was 0 seconds. +Runtime for this program was 55545 seconds.