diff --git a/README.md b/README.md index 1c3449c..7a5fd55 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Source code from the book Genetic Algorithms with Python by Clinton Sheppard Description === +[Edición española](https://github.com/handcraftsman/GeneticAlgorithmsWithPython/tree/master/es) + Genetic Algorithms with Python cover Get a hands-on introduction to machine learning with genetic algorithms using Python. Step-by-step tutorials build your skills from Hello World! to optimizing one genetic algorithm with another, and finally genetic programming; thus preparing you to apply genetic algorithms to problems in your own field of expertise. diff --git a/es/README.md b/es/README.md new file mode 100644 index 0000000..c6d9010 --- /dev/null +++ b/es/README.md @@ -0,0 +1,74 @@ +# Algoritmos Genéticos con Python +El código del libro Algoritmos Genéticos con Python por Clinton Sheppard + +Descripción +=== + +[English edition](https://github.com/handcraftsman/GeneticAlgorithmsWithPython) + +tapa del libro Algoritmos Genéticos con Python +Obtenga una introducción práctica al aprendizaje de la máquina con algoritmos genéticos usando Python. Tutoriales paso a paso construye sus habilidades de ¡Hola Mundo! a optimizar un algoritmo genético con otro, y finalmente a la programación genética; así, preparándose para aplicar algoritmos genéticos a problemas en su propio campo de experiencia. + +Algoritmos genéticos son una de las herramientas que puede utilizar para aplicar el aprendizaje de la máquina a la búsqueda de soluciones buenas, a veces incluso óptimas, a los problemas que tienen miles de millones de soluciones potenciales. Este libro le da la experiencia de hacer que los algoritmos genéticos funcionen para usted, usando los problemas fáciles de seguir del ejemplo que usted puede recurrir al aprender a usar otras herramientas y técnicas de aprendizaje de la máquina. Cada capítulo es un tutorial paso a paso que ayuda a desarrollar sus habilidades en el uso de algoritmos genéticos para resolver problemas utilizando Python. + +- https://www.amazon.com/dp/B072NDMJ4L (Versión Kindle) + +Tabla de contenido +=== + +Una breve introducción a los algoritmos genéticos + +Capítulo 1: ¡Hola Mundo! +- Adivine una contraseña dando el número de letras correctas en el grupo. Construir un motor de mutación. + +Capítulo 2: El Problema One-max +- Produce una matriz de bits donde todos son 1s. Expande el motor para funcionar con cualquier tipo de gene. + +Capítulo 3: Números ordenados +- Produsca una matriz de números enteros ordenados. Demuestre el manejo de múltiples metas y restricciones entre los genes. + +Capítulo 4: El rompecabezas de 8 reinas +- Encuentre las posiciones seguras de la reina en un tablero de ajedrez de 8x8 y luego amplíe a NxN. Demuestre la diferencia entre el fenotipo y el genotipo. + +Capítulo 5: Coloración gráfica +- Coloree un mapa de los países donde Español es el idioma nacional usando solamente 4 colores. Introdusca grupos de datos comunes y trabaje con archivos. También introdusca el uso de reglas para funcionar con restricciones genéticas. + +Capítulo 6: Problema de la carta +- Más restricciones genéticas. Introdusca la mutación personalizada, los algoritmos meméticos, y la técnica de suma-de-diferencia. También demuestre un cromosoma donde la forma en que se utiliza un gene depende de su posición en la matriz génica. + +Capítulo 7: Problema de los caballos de ajedrez +- Encuentre el número mínimo de caballos necesarios para atacar todas las posiciones en un tablero de ajedrez. Presente genes personalizados y creación de matrices de genes. También demuestra mínimos y máximos locales. + +Capítulo 8: Cuadrados mágicos +- Encuentre cuadrados donde todas las filas, columnas y ambas diagonales de una matriz de NxN tienen la misma suma. Introduce el recocido simulado. + +Capítulo 9: Problema de la mochila +- Optimizar el contenido de un contenedor para una o más variables. Presenta rama y límite y los cromosomas de la longitud variable. + +Capítulo 10: Resolución de ecuaciones lineales +- Encuentre las soluciones de ecuaciones lineales con 2, 3 y 4 desconocidos. Variación de ramas y límite. Refuerza la flexibilidad del genotipo. + +Capítulo 11: Generando Sudoku +- Un ejercicio guiado en la generación de rompecabezas de Sudoku. + +Capítulo 12: Problema del vendedor viajero +- Encuentre la ruta óptima para visitar las ciudades. Introduce intercambio de genes y una piscina de padres. + +Capítulo 13: Aproximación de Pi +- Encuentre los dos números de 10 bits cuyo dividendo es el más cercano a Pi. Introdusca utilizando un algoritmo genético para afinar otro. + +Capítulo 14: Generación de ecuaciones +- Encuentre la ecuación más corta que produzca un resultado específico usando suma, resta, multiplicación, etc. Introduce la programación genética simbólica. + +Capítulo 15: Problema de la cortadora de pasto +- Genere una serie de instrucciones que hacen que una cortadora de pasto corte un campo de pasto. Programación genética con estructuras de control, objetos, y funciones definidas automáticamentes. + +Capítulo 16: Circuitos lógicos +- Construya circuitos que se comporten como puertas lógicas básicas, combinaciones de compuertas, y finalmente un sumador de 2 bits que utilice nodos de árbol y ascenso de la colina. + +Capítulo 17: Expresiones regulares +- Busque expresiones regulares que coincidan con las cadenas deseadas. Presente la reparación cromosómica y el control del crecimiento. + +Capítulo 18: Tic-tac-toe +- Crear reglas para jugar el juego sin perder. Presenta la selección de torneos. + diff --git "a/es/ch01/contrase\303\261a.py" "b/es/ch01/contrase\303\261a.py" new file mode 100644 index 0000000..c3aa03d --- /dev/null +++ "b/es/ch01/contrase\303\261a.py" @@ -0,0 +1,77 @@ +# File: contraseña.py +# Del capítulo 1 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import random +import unittest + +import genetic + + +def obtener_aptitud(conjetura, objetivo): + return sum(1 for esperado, real in zip(objetivo, conjetura) + if esperado == real) + + +def mostrar(candidato, horaInicio): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + print("{}\t{}\t{}".format( + candidato.Genes, candidato.Aptitud, diferencia)) + + +# `nosetests` no admite caracteres como ñ en el nombre de la clase +class PruebasDeContrasena(unittest.TestCase): + geneSet = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!¡.," + + def test_Hola_Mundo(self): + objetivo = "¡Hola Mundo!" + self.adivine_contraseña(objetivo) + + def test_Porque_me_formaste_de_una_manera_formidable_y_maravillosa( + self): + objetivo = "Porque me formaste de una manera formidable y " \ + "maravillosa." + self.adivine_contraseña(objetivo) + + def adivine_contraseña(self, objetivo): + horaInicio = datetime.datetime.now() + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes, objetivo) + + def fnMostrar(candidato): + mostrar(candidato, horaInicio) + + aptitudÓptima = len(objetivo) + mejor = genetic.obtener_mejor(fnObtenerAptitud, len(objetivo), + aptitudÓptima, self.geneSet, + fnMostrar) + self.assertEqual(mejor.Genes, objetivo) + + def test_aleatorio(self): + longitud = 150 + objetivo = ''.join(random.choice(self.geneSet) + for _ in range(longitud)) + + self.adivine_contraseña(objetivo) + + def test_comparativa(self): + genetic.Comparar.ejecutar(self.test_aleatorio) + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch01/genetic.py b/es/ch01/genetic.py new file mode 100644 index 0000000..90bf08c --- /dev/null +++ b/es/ch01/genetic.py @@ -0,0 +1,86 @@ +# File: genetic.py +# Del capítulo 1 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import random +import statistics +import sys +import time + + +def _generar_padre(longitud, geneSet, obtener_aptitud): + genes = [] + while len(genes) < longitud: + tamañoMuestral = min(longitud - len(genes), len(geneSet)) + genes.extend(random.sample(geneSet, tamañoMuestral)) + genes = ''.join(genes) + aptitud = obtener_aptitud(genes) + return Cromosoma(genes, aptitud) + + +def _mudar(padre, geneSet, obtener_aptitud): + índice = random.randrange(0, len(padre.Genes)) + genesDelNiño = list(padre.Genes) + nuevoGen, alterno = random.sample(geneSet, 2) + genesDelNiño[índice] = alterno if nuevoGen == genesDelNiño[ + índice] else nuevoGen + genes = ''.join(genesDelNiño) + aptitud = obtener_aptitud(genes) + return Cromosoma(genes, aptitud) + + +def obtener_mejor(obtener_aptitud, longitudObjetivo, aptitudÓptima, geneSet, + mostrar): + random.seed() + mejorPadre = _generar_padre(longitudObjetivo, geneSet, obtener_aptitud) + mostrar(mejorPadre) + if mejorPadre.Aptitud >= aptitudÓptima: + return mejorPadre + while True: + niño = _mudar(mejorPadre, geneSet, obtener_aptitud) + if mejorPadre.Aptitud >= niño.Aptitud: + continue + mostrar(niño) + if niño.Aptitud >= aptitudÓptima: + return niño + mejorPadre = niño + + +class Cromosoma: + def __init__(self, genes, aptitud): + self.Genes = genes + self.Aptitud = aptitud + + +class Comparar: + @staticmethod + def ejecutar(función): + print(función) + cronometrajes = [] + stdout = sys.stdout + for i in range(100): + sys.stdout = None + horaInicio = time.time() + función() + segundos = time.time() - horaInicio + sys.stdout = stdout + cronometrajes.append(segundos) + promedio = statistics.mean(cronometrajes) + if i < 10 or i % 10 == 9: + print("{} {:3.2f} {:3.2f}".format( + 1 + i, promedio, + statistics.stdev(cronometrajes, + promedio) if i > 1 else 0)) diff --git "a/es/ch02/contrase\303\261a.py" "b/es/ch02/contrase\303\261a.py" new file mode 100644 index 0000000..19f9ff8 --- /dev/null +++ "b/es/ch02/contrase\303\261a.py" @@ -0,0 +1,76 @@ +# File: contraseña.py +# Del capítulo 2 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import random +import unittest + +import genetic + + +def obtener_aptitud(conjetura, objetivo): + return sum(1 for esperado, real in zip(objetivo, conjetura) + if esperado == real) + + +def mostrar(candidato, horaInicio): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + print("{}\t{}\t{}".format( + ''.join(candidato.Genes), + candidato.Aptitud, + diferencia)) + + +# `nosetests` no admite caracteres como ñ en el nombre de la clase +class PruebasDeContrasena(unittest.TestCase): + geneSet = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!¡.," + + def test_Hola_Mundo(self): + objetivo = "¡Hola Mundo!" + self.adivine_contraseña(objetivo) + + def test_Porque_me_formaste_de_una_manera_formidable_y_maravillosa(self): + objetivo = "Porque me formaste de una manera formidable y maravillosa." + self.adivine_contraseña(objetivo) + + def adivine_contraseña(self, objetivo): + horaInicio = datetime.datetime.now() + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes, objetivo) + + def fnMostrar(candidato): + mostrar(candidato, horaInicio) + + aptitudÓptima = len(objetivo) + mejor = genetic.obtener_mejor(fnObtenerAptitud, len(objetivo), + aptitudÓptima, self.geneSet, fnMostrar) + self.assertEqual(''.join(mejor.Genes), objetivo) + + def test_aleatorio(self): + longitud = 150 + objetivo = ''.join(random.choice(self.geneSet) + for _ in range(longitud)) + + self.adivine_contraseña(objetivo) + + def test_comparativa(self): + genetic.Comparar.ejecutar(self.test_aleatorio) + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch02/genetic.py b/es/ch02/genetic.py new file mode 100644 index 0000000..175079b --- /dev/null +++ b/es/ch02/genetic.py @@ -0,0 +1,82 @@ +# File: genetic.py +# Del capítulo 2 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import random +import statistics +import sys +import time + + +def _generar_padre(longitud, geneSet, obtener_aptitud): + genes = [] + while len(genes) < longitud: + tamañoMuestral = min(longitud - len(genes), len(geneSet)) + genes.extend(random.sample(geneSet, tamañoMuestral)) + aptitud = obtener_aptitud(genes) + return Cromosoma(genes, aptitud) + + +def _mudar(padre, geneSet, obtener_aptitud): + genesDelNiño = padre.Genes[:] + índice = random.randrange(0, len(padre.Genes)) + nuevoGen, alterno = random.sample(geneSet, 2) + genesDelNiño[índice] = alterno if nuevoGen == genesDelNiño[ + índice] else nuevoGen + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud) + + +def obtener_mejor(obtener_aptitud, longitudObjetivo, aptitudÓptima, geneSet, + mostrar): + random.seed() + mejorPadre = _generar_padre(longitudObjetivo, geneSet, obtener_aptitud) + mostrar(mejorPadre) + if mejorPadre.Aptitud >= aptitudÓptima: + return mejorPadre + while True: + niño = _mudar(mejorPadre, geneSet, obtener_aptitud) + if mejorPadre.Aptitud >= niño.Aptitud: + continue + mostrar(niño) + if niño.Aptitud >= aptitudÓptima: + return niño + mejorPadre = niño + + +class Cromosoma: + def __init__(self, genes, aptitud): + self.Genes = genes + self.Aptitud = aptitud + + +class Comparar: + @staticmethod + def ejecutar(función): + cronometrajes = [] + stdout = sys.stdout + for i in range(100): + sys.stdout = None + horaInicio = time.time() + función() + segundos = time.time() - horaInicio + sys.stdout = stdout + cronometrajes.append(segundos) + promedio = statistics.mean(cronometrajes) + if i < 10 or i % 10 == 9: + print("{} {:3.2f} {:3.2f}".format( + 1 + i, promedio, + statistics.stdev(cronometrajes, promedio) if i > 1 else 0)) diff --git a/es/ch02/oneMax.py b/es/ch02/oneMax.py new file mode 100644 index 0000000..5ff10d2 --- /dev/null +++ b/es/ch02/oneMax.py @@ -0,0 +1,58 @@ +# File: oneMax.py +# Del capítulo 2 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import unittest + +import genetic + + +def obtener_aptitud(genes): + return genes.count(1) + + +def mostrar(candidato, horaInicio): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + print("{}...{}\t{:3.2f}\t{}".format( + ''.join(map(str, candidato.Genes[:15])), + ''.join(map(str, candidato.Genes[-15:])), + candidato.Aptitud, + diferencia)) + + +class PruebasDeOneMax(unittest.TestCase): + def test(self, longitud=100): + geneSet = [0, 1] + horaInicio = datetime.datetime.now() + + def fnMostrar(candidato): + mostrar(candidato, horaInicio) + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes) + + aptitudÓptima = longitud + mejor = genetic.obtener_mejor(fnObtenerAptitud, longitud, + aptitudÓptima, geneSet, fnMostrar) + self.assertEqual(mejor.Aptitud, aptitudÓptima) + + def test_comparativa(self): + genetic.Comparar.ejecutar(lambda: self.test(4000)) + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch03/genetic.py b/es/ch03/genetic.py new file mode 100644 index 0000000..457274f --- /dev/null +++ b/es/ch03/genetic.py @@ -0,0 +1,95 @@ +# File: genetic.py +# Del capítulo 3 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import random +import statistics +import sys +import time + + +def _generar_padre(longitud, geneSet, obtener_aptitud): + genes = [] + while len(genes) < longitud: + tamañoMuestral = min(longitud - len(genes), len(geneSet)) + genes.extend(random.sample(geneSet, tamañoMuestral)) + aptitud = obtener_aptitud(genes) + return Cromosoma(genes, aptitud) + + +def _mudar(padre, geneSet, obtener_aptitud): + genesDelNiño = padre.Genes[:] + índice = random.randrange(0, len(padre.Genes)) + nuevoGen, alterno = random.sample(geneSet, 2) + genesDelNiño[índice] = alterno if nuevoGen == genesDelNiño[ + índice] else nuevoGen + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud) + + +def obtener_mejor(obtener_aptitud, longitudObjetivo, aptitudÓptima, geneSet, + mostrar): + random.seed() + + def fnMudar(padre): + return _mudar(padre, geneSet, obtener_aptitud) + + def fnGenerarPadre(): + return _generar_padre(longitudObjetivo, geneSet, obtener_aptitud) + + for mejora in _obtener_mejoras(fnMudar, fnGenerarPadre): + mostrar(mejora) + if not aptitudÓptima > mejora.Aptitud: + return mejora + + +def _obtener_mejoras(nuevo_niño, generar_padre): + mejorPadre = generar_padre() + yield mejorPadre + while True: + niño = nuevo_niño(mejorPadre) + if mejorPadre.Aptitud > niño.Aptitud: + continue + if not niño.Aptitud > mejorPadre.Aptitud: + mejorPadre = niño + continue + yield niño + mejorPadre = niño + + +class Cromosoma: + def __init__(self, genes, aptitud): + self.Genes = genes + self.Aptitud = aptitud + + +class Comparar: + @staticmethod + def ejecutar(función): + cronometrajes = [] + stdout = sys.stdout + for i in range(100): + sys.stdout = None + horaInicio = time.time() + función() + segundos = time.time() - horaInicio + sys.stdout = stdout + cronometrajes.append(segundos) + promedio = statistics.mean(cronometrajes) + if i < 10 or i % 10 == 9: + print("{} {:3.2f} {:3.2f}".format( + 1 + i, promedio, + statistics.stdev(cronometrajes, promedio) if i > 1 else 0)) diff --git "a/es/ch03/n\303\272merosOrdenados.py" "b/es/ch03/n\303\272merosOrdenados.py" new file mode 100644 index 0000000..3b57712 --- /dev/null +++ "b/es/ch03/n\303\272merosOrdenados.py" @@ -0,0 +1,86 @@ +# File: númerosOrdenados.py +# Del capítulo 3 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + + +import datetime +import unittest + +import genetic + + +def obtener_aptitud(genes): + aptitud = 1 + brecha = 0 + + for i in range(1, len(genes)): + if genes[i] > genes[i - 1]: + aptitud += 1 + else: + brecha += genes[i - 1] - genes[i] + return Aptitud(aptitud, brecha) + + +def mostrar(candidato, horaInicio): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + print("{}\t=> {}\t{}".format( + ', '.join(map(str, candidato.Genes)), + candidato.Aptitud, + diferencia)) + + +# `nosetests` no admite caracteres como ú en el nombre de la clase +class PruebasDeNumerosOrdenados(unittest.TestCase): + def test_ordenar_10_números(self): + self.ordenar_números(10) + + def ordenar_números(self, númerosTotales): + geneSet = [i for i in range(100)] + horaInicio = datetime.datetime.now() + + def fnMostrar(candidato): + mostrar(candidato, horaInicio) + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes) + + aptitudÓptima = Aptitud(númerosTotales, 0) + mejor = genetic.obtener_mejor(fnObtenerAptitud, númerosTotales, + aptitudÓptima, geneSet, fnMostrar) + self.assertTrue(not aptitudÓptima > mejor.Aptitud) + + def test_comparativa(self): + genetic.Comparar.ejecutar(lambda: self.ordenar_números(40)) + + +class Aptitud: + def __init__(self, númerosEnSecuencia, brechaTotal): + self.NúmerosEnSecuencia = númerosEnSecuencia + self.BrechaTotal = brechaTotal + + def __gt__(self, otro): + if self.NúmerosEnSecuencia != otro.NúmerosEnSecuencia: + return self.NúmerosEnSecuencia > otro.NúmerosEnSecuencia + return self.BrechaTotal < otro.BrechaTotal + + def __str__(self): + return "{} en secuencia, {} brecha".format( + self.NúmerosEnSecuencia, + self.BrechaTotal) + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch04/reinas.py b/es/ch04/reinas.py new file mode 100644 index 0000000..a05d99d --- /dev/null +++ b/es/ch04/reinas.py @@ -0,0 +1,104 @@ +# File: reinas.py +# Del capítulo 4 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import unittest + +import genetic + + +def obtener_aptitud(genes, tamaño): + tablero = Tablero(genes, tamaño) + filasConReinas = set() + columnasConReinas = set() + diagonalesNoresteConReinas = set() + diagonalesSudoesteConReinas = set() + for fila in range(tamaño): + for columna in range(tamaño): + if tablero.get(fila, columna) == 'R': + filasConReinas.add(fila) + columnasConReinas.add(columna) + diagonalesNoresteConReinas.add(fila + columna) + diagonalesSudoesteConReinas.add(tamaño - 1 - fila + columna) + total = (tamaño - len(filasConReinas) + + tamaño - len(columnasConReinas) + + tamaño - len(diagonalesNoresteConReinas) + + tamaño - len(diagonalesSudoesteConReinas)) + return Aptitud(total) + + +def mostrar(candidato, horaInicio, tamaño): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + tablero = Tablero(candidato.Genes, tamaño) + tablero.print() + print("{}\t- {}\t{}".format( + ' '.join(map(str, candidato.Genes)), + candidato.Aptitud, + diferencia)) + + +class PruebasDeReinas(unittest.TestCase): + def test(self, tamaño=8): + geneSet = [i for i in range(tamaño)] + horaInicio = datetime.datetime.now() + + def fnMostrar(candidato): + mostrar(candidato, horaInicio, tamaño) + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes, tamaño) + + aptitudÓptima = Aptitud(0) + mejor = genetic.obtener_mejor(fnObtenerAptitud, 2 * tamaño, + aptitudÓptima, geneSet, fnMostrar) + self.assertTrue(not aptitudÓptima > mejor.Aptitud) + + def test_comparativa(self): + genetic.Comparar.ejecutar(lambda: self.test(20)) + + +class Tablero: + def __init__(self, genes, tamaño): + tablero = [['.'] * tamaño for _ in range(tamaño)] + for índice in range(0, len(genes), 2): + fila = genes[índice] + columna = genes[índice + 1] + tablero[columna][fila] = 'R' + self._tablero = tablero + + def get(self, fila, columna): + return self._tablero[columna][fila] + + def print(self): + # 0,0 muestra en la esquina inferior izquierda + for i in reversed(range(len(self._tablero))): + print(' '.join(self._tablero[i])) + + +class Aptitud: + def __init__(self, total): + self.Total = total + + def __gt__(self, otro): + return self.Total < otro.Total + + def __str__(self): + return "{}".format(self.Total) + + +if __name__ == '__main__': + unittest.main() diff --git "a/es/ch05/coloraci\303\263n.py" "b/es/ch05/coloraci\303\263n.py" new file mode 100644 index 0000000..b077e1c --- /dev/null +++ "b/es/ch05/coloraci\303\263n.py" @@ -0,0 +1,140 @@ +# File: coloración.py +# Del capítulo 5 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import unittest + +import genetic + + +def cargar_datos(archivoLocal): + """ espera: T D1 [D2 ... DN] + donde T es el tipo de registro + y D1 .. DN son elementos de datos apropiados del tipo de registro + """ + reglas = set() + nodos = set() + with open(archivoLocal, mode='r') as fuente: + contenido = fuente.read().splitlines() + for fila in contenido: + if fila[0] == 'e': # e aa bb, aa y bb son identificadores de nodo + nodoIds = fila.split(' ')[1:3] + reglas.add(Regla(nodoIds[0], nodoIds[1])) + nodos.add(nodoIds[0]) + nodos.add(nodoIds[1]) + continue + if fila[0] == 'n': + # n aa ww, aa es un identificador de nodo, ww es un peso + nodoIds = fila.split(' ') + nodos.add(nodoIds[1]) + return reglas, nodos + + +def construir_reglas(artículos): + reglasAñadidas = {} + for país, adyacente in artículos.items(): + for paísAdyacente in adyacente: + if paísAdyacente == '': + continue + regla = Regla(país, paísAdyacente) + if regla in reglasAñadidas: + reglasAñadidas[regla] += 1 + else: + reglasAñadidas[regla] = 1 + for k, v in reglasAñadidas.items(): + if v != 2: + print("regla {} no es bidireccional".format(k)) + return reglasAñadidas.keys() + + +def obtener_aptitud(genes, reglas, búsquedaÍndicePaís): + reglasQuePasan = sum(1 for regla in reglas + if regla.EsVálida(genes, búsquedaÍndicePaís)) + return reglasQuePasan + + +def mostrar(candidato, horaInicio): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + print("{}\t{}\t{}".format( + ''.join(map(str, candidato.Genes)), + candidato.Aptitud, + diferencia)) + + +# `nosetests` no admite caracteres como ó y á en el nombre de la clase +class PruebasDeColoracionGrafica(unittest.TestCase): + def test_países(self): + self.color("países_españoles.col", + ["Naranja", "Amarillo", "Verde", "Rojo"]) + + def test_R100_1gb(self): + self.color("R100_1gb.col", + ["Dorado", "Naranja", "Amarillo", "Verde", "Rojo", "Morado"]) + + def test_comparativa(self): + genetic.Comparar.ejecutar(lambda: self.test_R100_1gb()) + + def color(self, file, colores): + reglas, nodos = cargar_datos(file) + valorÓptimo = len(reglas) + búsquedaDeColor = {color[0]: color for color in colores} + geneSet = list(búsquedaDeColor.keys()) + horaInicio = datetime.datetime.now() + búsquedaDeÍndiceDeNodo = {key: índice + for índice, key in enumerate(sorted(nodos))} + + def fnMostrar(candidato): + mostrar(candidato, horaInicio) + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes, reglas, búsquedaDeÍndiceDeNodo) + + mejor = genetic.obtener_mejor(fnObtenerAptitud, len(nodos), + valorÓptimo, geneSet, fnMostrar) + self.assertTrue(not valorÓptimo > mejor.Aptitud) + + llaves = sorted(nodos) + for índice in range(len(nodos)): + print( + llaves[índice] + " es " + búsquedaDeColor[mejor.Genes[índice]]) + + +class Regla: + def __init__(self, nodo, adyacente): + if nodo < adyacente: + nodo, adyacente = adyacente, nodo + self.Nodo = nodo + self.Adyacente = adyacente + + def __eq__(self, otro): + return self.Nodo == otro.Nodo and self.Adyacente == otro.Adyacente + + def __hash__(self): + return hash(self.Nodo) * 397 ^ hash(self.Adyacente) + + def __str__(self): + return self.Nodo + " -> " + self.Adyacente + + def EsVálida(self, genes, búsquedaDeÍndiceDeNodo): + índice = búsquedaDeÍndiceDeNodo[self.Nodo] + índiceDeNodosAdyacentes = búsquedaDeÍndiceDeNodo[self.Adyacente] + + return genes[índice] != genes[índiceDeNodosAdyacentes] + + +if __name__ == '__main__': + unittest.main() diff --git "a/es/ch05/pa\303\255ses_espa\303\261oles.col" "b/es/ch05/pa\303\255ses_espa\303\261oles.col" new file mode 100644 index 0000000..c252d04 --- /dev/null +++ "b/es/ch05/pa\303\255ses_espa\303\261oles.col" @@ -0,0 +1,46 @@ +p edge 21 40 +e AR BO +e AR CL +e AR PY +e AR UY +e BO AR +e BO CL +e BO PE +e BO PY +e CL AR +e CL BO +e CL PE +e CO EC +e CO PA +e CO PE +e CO VE +e CR NI +e CR PA +e EC CO +e EC PE +e GT HN +e GT MX +e GT SV +e HN GT +e HN NI +e HN SV +e MX GT +e NI CR +e NI HN +e PA CO +e PA CR +e PE BO +e PE CL +e PE CO +e PE EC +e PY AR +e PY BO +e SV GT +e SV HN +e UY AR +e VE CO +n CU 0 +n DO 0 +n ES 0 +n GQ 0 +n PR 0 diff --git "a/es/ch05/pa\303\255ses_espa\303\261oles.csv" "b/es/ch05/pa\303\255ses_espa\303\261oles.csv" new file mode 100644 index 0000000..0364df3 --- /dev/null +++ "b/es/ch05/pa\303\255ses_espa\303\261oles.csv" @@ -0,0 +1,21 @@ +AR,BO;CL;PY;UY +BO,AR;PY;CL;PE +CL,AR;BO;PE +CO,EC;PA;PE;VE +CR,NI;PA +CU, +DO, +EC,CO;PE +ES, +GQ, +GT,MX;HN;SV +HN,GT;NI;SV +MX,GT +NI,CR;HN +PA,CO;CR +PE,BO;CL;CO;EC +PR, +PY,AR;BO +SV,GT;HN +UY,AR +VE,CO \ No newline at end of file diff --git a/es/ch06/cartas.py b/es/ch06/cartas.py new file mode 100644 index 0000000..b222254 --- /dev/null +++ b/es/ch06/cartas.py @@ -0,0 +1,102 @@ +# File: cartas.py +# Del capítulo 6 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import functools +import operator +import random +import unittest + +import genetic + + +def obtener_aptitud(genes): + sumaDelGrupo1 = sum(genes[0:5]) + productoDelGrupo2 = functools.reduce(operator.mul, genes[5:10]) + duplicados = (len(genes) - len(set(genes))) + return Aptitud(sumaDelGrupo1, productoDelGrupo2, duplicados) + + +def mostrar(candidato, horaInicio): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + print("{} - {}\t{}\t{}".format( + ', '.join(map(str, candidato.Genes[0:5])), + ', '.join(map(str, candidato.Genes[5:10])), + candidato.Aptitud, + diferencia)) + + +def mudar(genes, geneSet): + if len(genes) == len(set(genes)): + cuenta = random.randint(1, 4) + while cuenta > 0: + cuenta -= 1 + índiceA, índiceB = random.sample(range(len(genes)), 2) + genes[índiceA], genes[índiceB] = genes[índiceB], genes[índiceA] + else: + índiceA = random.randrange(0, len(genes)) + índiceB = random.randrange(0, len(geneSet)) + genes[índiceA] = geneSet[índiceB] + + +class PruebasDeCartas(unittest.TestCase): + def test(self): + geneSet = [i + 1 for i in range(10)] + horaInicio = datetime.datetime.now() + + def fnMostrar(candidato): + mostrar(candidato, horaInicio) + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes) + + def fnMudar(genes): + mudar(genes, geneSet) + + aptitudÓptima = Aptitud(36, 360, 0) + mejor = genetic.obtener_mejor(fnObtenerAptitud, 10, aptitudÓptima, + geneSet, fnMostrar, + mutación_personalizada=fnMudar) + self.assertTrue(not aptitudÓptima > mejor.Aptitud) + + def test_comparativa(self): + genetic.Comparar.ejecutar(lambda: self.test()) + + +class Aptitud: + def __init__(self, sumaDelGrupo1, productoDelGrupo2, duplicados): + self.SumaDelGrupo1 = sumaDelGrupo1 + self.ProductoDelGrupo2 = productoDelGrupo2 + diferenciaSuma = abs(36 - sumaDelGrupo1) + diferenciaProducto = abs(360 - productoDelGrupo2) + self.DiferenciaTotal = diferenciaSuma + diferenciaProducto + self.Duplicados = duplicados + + def __gt__(self, otro): + if self.Duplicados != otro.Duplicados: + return self.Duplicados < otro.Duplicados + return self.DiferenciaTotal < otro.DiferenciaTotal + + def __str__(self): + return "sum: {} prod: {} dups: {}".format( + self.SumaDelGrupo1, + self.ProductoDelGrupo2, + self.Duplicados) + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch06/genetic.py b/es/ch06/genetic.py new file mode 100644 index 0000000..f1c934d --- /dev/null +++ b/es/ch06/genetic.py @@ -0,0 +1,105 @@ +# File: genetic.py +# Del capítulo 6 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import random +import statistics +import sys +import time + + +def _generar_padre(longitud, geneSet, obtener_aptitud): + genes = [] + while len(genes) < longitud: + tamañoMuestral = min(longitud - len(genes), len(geneSet)) + genes.extend(random.sample(geneSet, tamañoMuestral)) + aptitud = obtener_aptitud(genes) + return Cromosoma(genes, aptitud) + + +def _mudar(padre, geneSet, obtener_aptitud): + genesDelNiño = padre.Genes[:] + índice = random.randrange(0, len(padre.Genes)) + nuevoGen, alterno = random.sample(geneSet, 2) + genesDelNiño[índice] = alterno if nuevoGen == genesDelNiño[ + índice] else nuevoGen + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud) + + +def _mudar_personalizada(padre, mutación_personalizada, obtener_aptitud): + genesDelNiño = padre.Genes[:] + mutación_personalizada(genesDelNiño) + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud) + + +def obtener_mejor(obtener_aptitud, longitudObjetivo, aptitudÓptima, geneSet, + mostrar, mutación_personalizada=None): + if mutación_personalizada is None: + def fnMudar(padre): + return _mudar(padre, geneSet, obtener_aptitud) + else: + def fnMudar(padre): + return _mudar_personalizada(padre, mutación_personalizada, + obtener_aptitud) + + def fnGenerarPadre(): + return _generar_padre(longitudObjetivo, geneSet, obtener_aptitud) + + for mejora in _obtener_mejoras(fnMudar, fnGenerarPadre): + mostrar(mejora) + if not aptitudÓptima > mejora.Aptitud: + return mejora + + +def _obtener_mejoras(nuevo_niño, generar_padre): + mejorPadre = generar_padre() + yield mejorPadre + while True: + niño = nuevo_niño(mejorPadre) + if mejorPadre.Aptitud > niño.Aptitud: + continue + if not niño.Aptitud > mejorPadre.Aptitud: + mejorPadre = niño + continue + yield niño + mejorPadre = niño + + +class Cromosoma: + def __init__(self, genes, aptitud): + self.Genes = genes + self.Aptitud = aptitud + + +class Comparar: + @staticmethod + def ejecutar(función): + cronometrajes = [] + stdout = sys.stdout + for i in range(100): + sys.stdout = None + horaInicio = time.time() + función() + segundos = time.time() - horaInicio + sys.stdout = stdout + cronometrajes.append(segundos) + promedio = statistics.mean(cronometrajes) + if i < 10 or i % 10 == 9: + print("{} {:3.2f} {:3.2f}".format( + 1 + i, promedio, + statistics.stdev(cronometrajes, promedio) if i > 1 else 0)) diff --git a/es/ch07/caballos.py b/es/ch07/caballos.py new file mode 100644 index 0000000..c30641b --- /dev/null +++ b/es/ch07/caballos.py @@ -0,0 +1,200 @@ +# File: caballos.py +# Del capítulo 7 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import random +import unittest + +import genetic + + +def obtener_aptitud(genes, tableroAncho, tableroAltura): + atacado = set(pos + for kn in genes + for pos in obtener_ataques(kn, tableroAncho, + tableroAltura)) + return len(atacado) + + +def mostrar(candidato, horaInicio, tableroAncho, tableroAltura): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + tablero = Tablero(candidato.Genes, tableroAncho, tableroAltura) + tablero.print() + + print("{}\n\t{}\t{}".format( + ' '.join(map(str, candidato.Genes)), + candidato.Aptitud, + diferencia)) + + +def mudar(genes, tableroAncho, tableroAltura, todasPosiciones, + posicionesNoBordeadas): + cuenta = 2 if random.randint(0, 10) == 0 else 1 + while cuenta > 0: + cuenta -= 1 + posiciónACaballoÍndices = dict((p, []) for p in todasPosiciones) + for i, caballo in enumerate(genes): + for posición in obtener_ataques(caballo, tableroAncho, + tableroAltura): + posiciónACaballoÍndices[posición].append(i) + caballoÍndices = set(i for i in range(len(genes))) + noAtacados = [] + for kvp in posiciónACaballoÍndices.items(): + if len(kvp[1]) > 1: + continue + if len(kvp[1]) == 0: + noAtacados.append(kvp[0]) + continue + for p in kvp[1]: # longitud == 1 + if p in caballoÍndices: + caballoÍndices.remove(p) + + posicionesPotenciales = \ + [p for posiciones in + map(lambda x: obtener_ataques(x, tableroAncho, tableroAltura), + noAtacados) + for p in posiciones if p in posicionesNoBordeadas] \ + if len(noAtacados) > 0 else posicionesNoBordeadas + + índiceDeGen = random.randrange(0, len(genes)) \ + if len(caballoÍndices) == 0 \ + else random.choice([i for i in caballoÍndices]) + + posición = random.choice(posicionesPotenciales) + genes[índiceDeGen] = posición + + +def crear(fnObtenerPosiciónAleatoria, caballosEsperados): + genes = [fnObtenerPosiciónAleatoria() for _ in range(caballosEsperados)] + return genes + + +def obtener_ataques(ubicación, tableroAncho, tableroAltura): + return [i for i in set( + Posición(x + ubicación.X, y + ubicación.Y) + for x in [-2, -1, 1, 2] if 0 <= x + ubicación.X < tableroAncho + for y in [-2, -1, 1, 2] if 0 <= y + ubicación.Y < tableroAltura + and abs(y) != abs(x))] + + +class PruebasDeCaballos(unittest.TestCase): + def test_3x4(self): + anchura = 4 + altura = 3 + # 1,0 2,0 3,0 + # 0,2 1,2 2,0 + # 2 C C C . + # 1 . . . . + # 0 . C C C + # 0 1 2 3 + self.encontrarCaballoPosiciones(anchura, altura, 6) + + def test_8x8(self): + anchura = 8 + altura = 8 + self.encontrarCaballoPosiciones(anchura, altura, 14) + + def test_10x10(self): + anchura = 10 + altura = 10 + self.encontrarCaballoPosiciones(anchura, altura, 22) + + def test_12x12(self): + anchura = 13 + altura = 13 + self.encontrarCaballoPosiciones(anchura, altura, 28) + + def test_13x13(self): + anchura = 13 + altura = 13 + self.encontrarCaballoPosiciones(anchura, altura, 32) + + def test_comparativa(self): + genetic.Comparar.ejecutar(lambda: self.test_10x10()) + + def encontrarCaballoPosiciones(self, tableroAncho, tableroAltura, + caballosEsperados): + horaInicio = datetime.datetime.now() + + def fnMostrar(candidato): + mostrar(candidato, horaInicio, tableroAncho, tableroAltura) + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes, tableroAncho, tableroAltura) + + todasPosiciones = [Posición(x, y) + for y in range(tableroAltura) + for x in range(tableroAncho)] + + if tableroAncho < 6 or tableroAltura < 6: + posicionesNoBordeadas = todasPosiciones + else: + posicionesNoBordeadas = [i for i in todasPosiciones + if 0 < i.X < tableroAncho - 1 and + 0 < i.Y < tableroAltura - 1] + + def fnObtenerPosiciónAleatoria(): + return random.choice(posicionesNoBordeadas) + + def fnMudar(genes): + mudar(genes, tableroAncho, tableroAltura, todasPosiciones, + posicionesNoBordeadas) + + def fnCrear(): + return crear(fnObtenerPosiciónAleatoria, caballosEsperados) + + aptitudÓptima = tableroAncho * tableroAltura + mejor = genetic.obtener_mejor(fnObtenerAptitud, None, aptitudÓptima, + None, fnMostrar, fnMudar, fnCrear) + self.assertTrue(not aptitudÓptima > mejor.Aptitud) + + +class Posición: + def __init__(self, x, y): + self.X = x + self.Y = y + + def __str__(self): + return "{},{}".format(self.X, self.Y) + + def __eq__(self, otro): + return self.X == otro.X and self.Y == otro.Y + + def __hash__(self): + return self.X * 1000 + self.Y + + +class Tablero: + def __init__(self, posiciones, anchura, altura): + tablero = [['.'] * anchura for _ in range(altura)] + + for índice in range(len(posiciones)): + posiciónDeCaballo = posiciones[índice] + tablero[posiciónDeCaballo.Y][posiciónDeCaballo.X] = 'C' + self._tablero = tablero + self._anchura = anchura + self._altura = altura + + def print(self): + # 0,0 muestra en la esquina inferior izquierda + for i in reversed(range(self._altura)): + print(i, "\t", ' '.join(self._tablero[i])) + print(" \t", ' '.join(map(str, range(self._anchura)))) + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch07/genetic.py b/es/ch07/genetic.py new file mode 100644 index 0000000..da0332e --- /dev/null +++ b/es/ch07/genetic.py @@ -0,0 +1,111 @@ +# File: genetic.py +# Del capítulo 7 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import random +import statistics +import sys +import time + + +def _generar_padre(longitud, geneSet, obtener_aptitud): + genes = [] + while len(genes) < longitud: + tamañoMuestral = min(longitud - len(genes), len(geneSet)) + genes.extend(random.sample(geneSet, tamañoMuestral)) + aptitud = obtener_aptitud(genes) + return Cromosoma(genes, aptitud) + + +def _mudar(padre, geneSet, obtener_aptitud): + genesDelNiño = padre.Genes[:] + índice = random.randrange(0, len(padre.Genes)) + nuevoGen, alterno = random.sample(geneSet, 2) + genesDelNiño[índice] = alterno if nuevoGen == genesDelNiño[ + índice] else nuevoGen + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud) + + +def _mudar_personalizada(padre, mutación_personalizada, obtener_aptitud): + genesDelNiño = padre.Genes[:] + mutación_personalizada(genesDelNiño) + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud) + + +def obtener_mejor(obtener_aptitud, longitudObjetivo, aptitudÓptima, geneSet, + mostrar, mutación_personalizada=None, + creación_personalizada=None): + if mutación_personalizada is None: + def fnMudar(padre): + return _mudar(padre, geneSet, obtener_aptitud) + else: + def fnMudar(padre): + return _mudar_personalizada(padre, mutación_personalizada, + obtener_aptitud) + + if creación_personalizada is None: + def fnGenerarPadre(): + return _generar_padre(longitudObjetivo, geneSet, obtener_aptitud) + else: + def fnGenerarPadre(): + genes = creación_personalizada() + return Cromosoma(genes, obtener_aptitud(genes)) + + for mejora in _obtener_mejoras(fnMudar, fnGenerarPadre): + mostrar(mejora) + if not aptitudÓptima > mejora.Aptitud: + return mejora + + +def _obtener_mejoras(nuevo_niño, generar_padre): + mejorPadre = generar_padre() + yield mejorPadre + while True: + niño = nuevo_niño(mejorPadre) + if mejorPadre.Aptitud > niño.Aptitud: + continue + if not niño.Aptitud > mejorPadre.Aptitud: + mejorPadre = niño + continue + yield niño + mejorPadre = niño + + +class Cromosoma: + def __init__(self, genes, aptitud): + self.Genes = genes + self.Aptitud = aptitud + + +class Comparar: + @staticmethod + def ejecutar(función): + cronometrajes = [] + stdout = sys.stdout + for i in range(100): + sys.stdout = None + horaInicio = time.time() + función() + segundos = time.time() - horaInicio + sys.stdout = stdout + cronometrajes.append(segundos) + promedio = statistics.mean(cronometrajes) + if i < 10 or i % 10 == 9: + print("{} {:3.2f} {:3.2f}".format( + 1 + i, promedio, + statistics.stdev(cronometrajes, promedio) if i > 1 else 0)) diff --git "a/es/ch08/cuadradosM\303\241gicos.py" "b/es/ch08/cuadradosM\303\241gicos.py" new file mode 100644 index 0000000..0f00a93 --- /dev/null +++ "b/es/ch08/cuadradosM\303\241gicos.py" @@ -0,0 +1,129 @@ +# File: cuadradosMágicos.py +# Del capítulo 8 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import random +import unittest + +import genetic + + +def obtener_aptitud(genes, tamañoDiagonal, sumaEsperada): + filas, columnas, sumaDiagonalNoreste, sumaDiagonalSureste = \ + obtener_sums(genes, tamañoDiagonal) + + sumaDeDiferencias = sum(int(abs(s - sumaEsperada)) + for s in filas + columnas + + [sumaDiagonalSureste, sumaDiagonalNoreste] + if s != sumaEsperada) + + return Aptitud(sumaDeDiferencias) + + +def mostrar(candidato, tamañoDiagonal, horaInicio): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + + filas, columnas, sumaDiagonalNoreste, sumaDiagonalSureste = \ + obtener_sums(candidato.Genes, tamañoDiagonal) + + for númeroDeFila in range(tamañoDiagonal): + fila = candidato.Genes[ + númeroDeFila * tamañoDiagonal:(númeroDeFila + 1) * + tamañoDiagonal] + print("\t ", fila, "=", filas[númeroDeFila]) + print(sumaDiagonalNoreste, "\t", columnas, "\t", sumaDiagonalSureste) + print(" - - - - - - - - - - -", candidato.Aptitud, diferencia) + + +def obtener_sums(genes, tamañoDiagonal): + filas = [0 for _ in range(tamañoDiagonal)] + columnas = [0 for _ in range(tamañoDiagonal)] + sumaDiagonalSureste = 0 + sumaDiagonalNoreste = 0 + for fila in range(tamañoDiagonal): + for columna in range(tamañoDiagonal): + valor = genes[fila * tamañoDiagonal + columna] + filas[fila] += valor + columnas[columna] += valor + sumaDiagonalSureste += genes[fila * tamañoDiagonal + fila] + sumaDiagonalNoreste += genes[fila * tamañoDiagonal + + (tamañoDiagonal - 1 - fila)] + return filas, columnas, sumaDiagonalNoreste, sumaDiagonalSureste + + +def mudar(genes, índices): + índiceA, índiceB = random.sample(índices, 2) + genes[índiceA], genes[índiceB] = genes[índiceB], genes[índiceA] + + +# `nosetests` no admite caracteres como á en el nombre de la clase +class PruebasDeCuadradosMagicos(unittest.TestCase): + def test_tamaño_3(self): + self.generar(3, 50) + + def test_tamaño_4(self): + self.generar(4, 50) + + def test_tamaño_5(self): + self.generar(5, 500) + + def test_tamaño_10(self): + self.generar(10, 5000) + + def test_comparativa(self): + genetic.Comparar.ejecutar(self.test_tamaño_4) + + def generar(self, tamañoDiagonal, edadMáxima): + nCuadrado = tamañoDiagonal * tamañoDiagonal + geneSet = [i for i in range(1, nCuadrado + 1)] + sumaEsperada = tamañoDiagonal * (nCuadrado + 1) / 2 + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes, tamañoDiagonal, sumaEsperada) + + def fnMostrar(candidato): + mostrar(candidato, tamañoDiagonal, horaInicio) + + geneÍndices = [i for i in range(0, len(geneSet))] + + def fnMudar(genes): + mudar(genes, geneÍndices) + + def fnCreaciónPersonalizada(): + return random.sample(geneSet, len(geneSet)) + + valorÓptimo = Aptitud(0) + horaInicio = datetime.datetime.now() + mejor = genetic.obtener_mejor(fnObtenerAptitud, nCuadrado, valorÓptimo, + geneSet, fnMostrar, fnMudar, + fnCreaciónPersonalizada, edadMáxima) + self.assertTrue(not valorÓptimo > mejor.Aptitud) + + +class Aptitud: + def __init__(self, sumaDeDiferencias): + self.SumaDeDiferencias = sumaDeDiferencias + + def __gt__(self, otro): + return self.SumaDeDiferencias < otro.SumaDeDiferencias + + def __str__(self): + return "{}".format(self.SumaDeDiferencias) + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch08/genetic.py b/es/ch08/genetic.py new file mode 100644 index 0000000..a5a5c88 --- /dev/null +++ b/es/ch08/genetic.py @@ -0,0 +1,135 @@ +# File: genetic.py +# Del capítulo 8 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import random +import statistics +import sys +import time +from bisect import bisect_left +from math import exp + + +def _generar_padre(longitud, geneSet, obtener_aptitud): + genes = [] + while len(genes) < longitud: + tamañoMuestral = min(longitud - len(genes), len(geneSet)) + genes.extend(random.sample(geneSet, tamañoMuestral)) + aptitud = obtener_aptitud(genes) + return Cromosoma(genes, aptitud) + + +def _mudar(padre, geneSet, obtener_aptitud): + genesDelNiño = padre.Genes[:] + índice = random.randrange(0, len(padre.Genes)) + nuevoGen, alterno = random.sample(geneSet, 2) + genesDelNiño[índice] = alterno if nuevoGen == genesDelNiño[ + índice] else nuevoGen + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud) + + +def _mudar_personalizada(padre, mutación_personalizada, obtener_aptitud): + genesDelNiño = padre.Genes[:] + mutación_personalizada(genesDelNiño) + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud) + + +def obtener_mejor(obtener_aptitud, longitudObjetivo, aptitudÓptima, geneSet, + mostrar, mutación_personalizada=None, + creación_personalizada=None, edadMáxima=None): + if mutación_personalizada is None: + def fnMudar(padre): + return _mudar(padre, geneSet, obtener_aptitud) + else: + def fnMudar(padre): + return _mudar_personalizada(padre, mutación_personalizada, + obtener_aptitud) + + if creación_personalizada is None: + def fnGenerarPadre(): + return _generar_padre(longitudObjetivo, geneSet, obtener_aptitud) + else: + def fnGenerarPadre(): + genes = creación_personalizada() + return Cromosoma(genes, obtener_aptitud(genes)) + + for mejora in _obtener_mejoras(fnMudar, fnGenerarPadre, edadMáxima): + mostrar(mejora) + if not aptitudÓptima > mejora.Aptitud: + return mejora + + +def _obtener_mejoras(nuevo_niño, generar_padre, edadMáxima): + padre = mejorPadre = generar_padre() + yield mejorPadre + aptitudesHistóricas = [mejorPadre.Aptitud] + while True: + niño = nuevo_niño(padre) + if padre.Aptitud > niño.Aptitud: + if edadMáxima is None: + continue + padre.Edad += 1 + if edadMáxima > padre.Edad: + continue + índice = bisect_left(aptitudesHistóricas, niño.Aptitud, 0, + len(aptitudesHistóricas)) + diferencia = len(aptitudesHistóricas) - índice + proporciónSimilar = diferencia / len(aptitudesHistóricas) + if random.random() < exp(-proporciónSimilar): + padre = niño + continue + padre = mejorPadre + padre.Edad = 0 + continue + if not niño.Aptitud > padre.Aptitud: + # mismo aptitud + niño.Edad = padre.Edad + 1 + padre = niño + continue + padre = niño + padre.Edad = 0 + if niño.Aptitud > mejorPadre.Aptitud: + yield niño + mejorPadre = niño + aptitudesHistóricas.append(niño.Aptitud) + + +class Cromosoma: + def __init__(self, genes, aptitud): + self.Genes = genes + self.Aptitud = aptitud + self.Edad = 0 + + +class Comparar: + @staticmethod + def ejecutar(función): + cronometrajes = [] + stdout = sys.stdout + for i in range(100): + sys.stdout = None + horaInicio = time.time() + función() + segundos = time.time() - horaInicio + sys.stdout = stdout + cronometrajes.append(segundos) + promedio = statistics.mean(cronometrajes) + if i < 10 or i % 10 == 9: + print("{} {:3.2f} {:3.2f}".format( + 1 + i, promedio, + statistics.stdev(cronometrajes, promedio) if i > 1 else 0)) diff --git a/es/ch09/mochila.py b/es/ch09/mochila.py new file mode 100644 index 0000000..d6aec76 --- /dev/null +++ b/es/ch09/mochila.py @@ -0,0 +1,291 @@ +# File: mochila.py +# Del capítulo 9 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import random +import sys +import unittest + +import genetic + + +def obtener_aptitud(genes): + pesoTotal = 0 + volumenTotal = 0 + valorTotal = 0 + for ac in genes: + cuenta = ac.Cantidad + pesoTotal += ac.Artículo.Peso * cuenta + volumenTotal += ac.Artículo.Volumen * cuenta + valorTotal += ac.Artículo.Valor * cuenta + + return Aptitud(pesoTotal, volumenTotal, valorTotal) + + +def mostrar(candidato, horaInicio): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + genes = candidato.Genes[:] + genes.sort(key=lambda ac: ac.Cantidad, reverse=True) + + descripciones = [str(ac.Cantidad) + "x" + ac.Artículo.Nombre for ac in + genes] + if len(descripciones) == 0: + descripciones.append("Vacío") + print("{}\t{}\t{}".format( + ', '.join(descripciones), + candidato.Aptitud, + diferencia)) + + +def cantidad_máxima(artículo, pesoMáximo, volumenMáximo): + return min(int(pesoMáximo / artículo.Peso) + if artículo.Peso > 0 else sys.maxsize, + int(volumenMáximo / artículo.Volumen) + if artículo.Volumen > 0 else sys.maxsize) + + +def crear(artículos, pesoMáximo, volumenMáximo): + genes = [] + pesoRestante, volumenRestante = pesoMáximo, volumenMáximo + for i in range(random.randrange(1, len(artículos))): + nuevoGen = añadir(genes, artículos, pesoRestante, volumenRestante) + if nuevoGen is not None: + genes.append(nuevoGen) + pesoRestante -= nuevoGen.Cantidad * nuevoGen.Artículo.Peso + volumenRestante -= nuevoGen.Cantidad * nuevoGen.Artículo.Volumen + return genes + + +def añadir(genes, artículos, pesoMáximo, volumenMáximo): + artículosUsados = {ac.Artículo for ac in genes} + artículo = random.choice(artículos) + while artículo in artículosUsados: + artículo = random.choice(artículos) + + cantidadMáxima = cantidad_máxima(artículo, pesoMáximo, volumenMáximo) + return ArtículoCantidad(artículo, + cantidadMáxima) if cantidadMáxima > 0 else None + + +def mudar(genes, artículos, pesoMáximo, volumenMáximo, ventana): + ventana.deslizar() + aptitud = obtener_aptitud(genes) + pesoRestante = pesoMáximo - aptitud.PesoTotal + volumenRestante = volumenMáximo - aptitud.VolumenTotal + + eliminando = len(genes) > 1 and random.randint(0, 10) == 0 + if eliminando: + índice = random.randrange(0, len(genes)) + ac = genes[índice] + artículo = ac.Artículo + pesoRestante += artículo.Peso * ac.Cantidad + volumenRestante += artículo.Volumen * ac.Cantidad + del genes[índice] + + añadiendo = (pesoRestante > 0 or volumenRestante > 0) and \ + (len(genes) == 0 or (len(genes) < len(artículos) and + random.randint(0, 100) == 0)) + + if añadiendo: + nuevoGen = añadir(genes, artículos, pesoRestante, volumenRestante) + if nuevoGen is not None: + genes.append(nuevoGen) + return + + índice = random.randrange(0, len(genes)) + ac = genes[índice] + artículo = ac.Artículo + pesoRestante += artículo.Peso * ac.Cantidad + volumenRestante += artículo.Volumen * ac.Cantidad + + artículoACambiar = len(genes) < len(artículos) and \ + random.randint(0, 4) == 0 + if artículoACambiar: + artículoÍndice = artículos.index(ac.Artículo) + principio = max(1, artículoÍndice - ventana.Tamaño) + fin = min(len(artículos) - 1, artículoÍndice + ventana.Tamaño) + artículo = artículos[random.randint(principio, fin)] + cantidadMáxima = cantidad_máxima(artículo, pesoRestante, volumenRestante) + if cantidadMáxima > 0: + genes[índice] = ArtículoCantidad(artículo, cantidadMáxima + if ventana.Tamaño > 1 else random.randint(1, cantidadMáxima)) + else: + del genes[índice] + + +class PruebasDeMochila(unittest.TestCase): + def test_galletas(self): + artículos = [ + Recurso("Harina", 1680, 0.265, .41), + Recurso("Mantequilla", 1440, 0.5, .13), + Recurso("Azúcar", 1840, 0.441, .29) + ] + pesoMáximo = 10 + volumenMáximo = 4 + óptimo = obtener_aptitud( + [ArtículoCantidad(artículos[0], 1), + ArtículoCantidad(artículos[1], 14), + ArtículoCantidad(artículos[2], 6)]) + self.rellenar_mochila(artículos, pesoMáximo, volumenMáximo, óptimo) + + def test_exnsd16(self): + informaciónDelProblema = cargar_datos("exnsd16.ukp") + artículos = informaciónDelProblema.Recursos + pesoMáximo = informaciónDelProblema.PesoMáximo + volumenMáximo = 0 + óptimo = obtener_aptitud(informaciónDelProblema.Solución) + self.rellenar_mochila(artículos, pesoMáximo, volumenMáximo, óptimo) + + def test_comparativa(self): + genetic.Comparar.ejecutar(lambda: self.test_exnsd16()) + + def rellenar_mochila(self, artículos, pesoMáximo, volumenMáximo, + aptitudÓptima): + horaInicio = datetime.datetime.now() + ventana = Ventana(1, + max(1, int(len(artículos) / 3)), + int(len(artículos) / 2)) + + artículosOrdenados = sorted(artículos, + key=lambda artículo: artículo.Valor) + + def fnMostrar(candidato): + mostrar(candidato, horaInicio) + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes) + + def fnCrear(): + return crear(artículos, pesoMáximo, volumenMáximo) + + def fnMudar(genes): + mudar(genes, artículosOrdenados, pesoMáximo, volumenMáximo, + ventana) + + mejor = genetic.obtener_mejor(fnObtenerAptitud, None, aptitudÓptima, + None, fnMostrar, fnMudar, fnCrear, + edadMáxima=50) + self.assertTrue(not aptitudÓptima > mejor.Aptitud) + + +def cargar_datos(archivoLocal): + with open(archivoLocal, mode='r') as fuente: + filas = fuente.read().splitlines() + datos = DatosDelProblema() + f = encontrar_restricción + + for fila in filas: + f = f(fila.strip(), datos) + if f is None: + break + return datos + + +def encontrar_restricción(fila, datos): + partes = fila.split(' ') + if partes[0] != "c:": + return encontrar_restricción + datos.PesoMáximo = int(partes[1]) + return buscar_inicio_de_datos + + +def buscar_inicio_de_datos(fila, datos): + if fila != "begin data": + return buscar_inicio_de_datos + return leer_recurso_o_encontrar_final_de_datos + + +def leer_recurso_o_encontrar_final_de_datos(fila, datos): + if fila == "end data": + return encontrar_inicio_de_la_solución + partes = fila.split('\t') + recurso = Recurso("R" + str(1 + len(datos.Recursos)), int(partes[1]), + int(partes[0]), 0) + datos.Recursos.append(recurso) + return leer_recurso_o_encontrar_final_de_datos + + +def encontrar_inicio_de_la_solución(fila, datos): + if fila == "sol:": + return leer_solución_recurso_o_encontrar_final_de_solución + return encontrar_inicio_de_la_solución + + +def leer_solución_recurso_o_encontrar_final_de_solución(fila, datos): + if fila == "": + return None + partes = [p for p in fila.split('\t') if p != ""] + recursoÍndice = int(partes[0]) - 1 # hacer que sea basado en 0 + recursoCantidad = int(partes[1]) + datos.Solución.append( + ArtículoCantidad(datos.Recursos[recursoÍndice], recursoCantidad)) + return leer_solución_recurso_o_encontrar_final_de_solución + + +class Recurso: + def __init__(self, nombre, valor, peso, volumen): + self.Nombre = nombre + self.Valor = valor + self.Peso = peso + self.Volumen = volumen + + +class ArtículoCantidad: + def __init__(self, artículo, cantidad): + self.Artículo = artículo + self.Cantidad = cantidad + + def __eq__(self, otro): + return self.Artículo == otro.Artículo and \ + self.Cantidad == otro.Cantidad + + +class Aptitud: + def __init__(self, pesoTotal, volumenTotal, valorTotal): + self.PesoTotal = pesoTotal + self.VolumenTotal = volumenTotal + self.ValorTotal = valorTotal + + def __gt__(self, otro): + return self.ValorTotal > otro.ValorTotal + + def __str__(self): + return "peso: {:0.2f} vol: {:0.2f} valor: {}".format( + self.PesoTotal, + self.VolumenTotal, + self.ValorTotal) + + +class DatosDelProblema: + def __init__(self): + self.Recursos = [] + self.PesoMáximo = 0 + self.Solución = [] + + +class Ventana: + def __init__(self, mínimo, máximo, tamaño): + self.Min = mínimo + self.Max = máximo + self.Tamaño = tamaño + + def deslizar(self): + self.Tamaño = self.Tamaño - 1 if self.Tamaño > self.Min else self.Max + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch10/ecuacionesLineales.py b/es/ch10/ecuacionesLineales.py new file mode 100644 index 0000000..5f0730e --- /dev/null +++ b/es/ch10/ecuacionesLineales.py @@ -0,0 +1,196 @@ +# File: ecuacionesLineales.py +# Del capítulo 10 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +from fractions import Fraction +import random +import unittest + +import genetic + + +def obtener_aptitud(genes, ecuaciones): + aptitud = Aptitud(sum(abs(e(genes)) for e in ecuaciones)) + return aptitud + + +def mostrar(candidato, horaInicio, fnGenesAEntradas): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + símbolos = "xyza" + resultado = ', '.join("{} = {}".format(s, v) + for s, v in + zip(símbolos, fnGenesAEntradas(candidato.Genes))) + print("{}\t{}\t{}".format( + resultado, + candidato.Aptitud, + diferencia)) + + +def mudar(genes, geneSetOrdenado, ventana, geneÍndices): + índices = random.sample(geneÍndices, random.randint(1, len(genes))) \ + if random.randint(0, 10) == 0 else [random.choice(geneÍndices)] + ventana.deslizar() + while len(índices) > 0: + índice = índices.pop() + geneSetÍndice = geneSetOrdenado.index(genes[índice]) + principio = max(0, geneSetÍndice - ventana.Tamaño) + fin = min(len(geneSetOrdenado) - 1, geneSetÍndice + ventana.Tamaño) + geneSetÍndice = random.randint(principio, fin) + genes[índice] = geneSetOrdenado[geneSetÍndice] + + +class PruebasDeEcuacionesLineales(unittest.TestCase): + def test_2_desconocidos(self): + geneSet = [i for i in range(-5, 5) if i != 0] + + def fnGenesAEntradas(genes): + return genes[0], genes[1] + + def e1(genes): + x, y = fnGenesAEntradas(genes) + return x + 2 * y - 4 + + def e2(genes): + x, y = fnGenesAEntradas(genes) + return 4 * x + 4 * y - 12 + + ecuaciones = [e1, e2] + self.resolver_desconocidos(2, geneSet, ecuaciones, fnGenesAEntradas) + + def test_3_desconocidos(self): + valores = [i for i in range(-5, 5) if i != 0] + geneSet = [i for i in set( + Fraction(d, e) + for d in valores + for e in valores if e != 0)] + + def fnGenesAEntradas(genes): + return genes + + def e1(genes): + x, y, z = genes + return 6 * x - 2 * y + 8 * z - 20 + + def e2(genes): + x, y, z = genes + return y + 8 * x * z + 1 + + def e3(genes): + x, y, z = genes + return 2 * z * Fraction(6, x) \ + + 3 * Fraction(y, 2) - 6 + + ecuaciones = [e1, e2, e3] + self.resolver_desconocidos(3, geneSet, ecuaciones, fnGenesAEntradas) + + def test_4_desconocidos(self): + valores = [i for i in range(-13, 13) if i != 0] + geneSet = [i for i in set( + Fraction(d, e) + for d in valores + for e in valores if e != 0)] + + def fnGenesAEntradas(genes): + return genes + + def e1(genes): + x, y, z, a = genes + return Fraction(1, 15) * x \ + - 2 * y \ + - 15 * z \ + - Fraction(4, 5) * a \ + - 3 + + def e2(genes): + x, y, z, a = genes + return -Fraction(5, 2) * x \ + - Fraction(9, 4) * y \ + + 12 * z \ + - a \ + - 17 + + def e3(genes): + x, y, z, a = genes + return -13 * x \ + + Fraction(3, 10) * y \ + - 6 * z \ + - Fraction(2, 5) * a \ + - 17 + + def e4(genes): + x, y, z, a = genes + return Fraction(1, 2) * x \ + + 2 * y \ + + Fraction(7, 4) * z \ + + Fraction(4, 3) * a \ + + 9 + + ecuaciones = [e1, e2, e3, e4] + self.resolver_desconocidos(4, geneSet, ecuaciones, fnGenesAEntradas) + + def test_comparativa(self): + genetic.Comparar.ejecutar(lambda: self.test_4_desconocidos()) + + def resolver_desconocidos(self, numUnknowns, geneSet, ecuaciones, + fnGenesAEntradas): + horaInicio = datetime.datetime.now() + edadMáxima = 50 + ventana = Ventana(max(1, int(len(geneSet) / (2 * edadMáxima))), + max(1, int(len(geneSet) / 3)), + int(len(geneSet) / 2)) + geneÍndices = [i for i in range(numUnknowns)] + geneSetOrdenado = sorted(geneSet) + + def fnMostrar(candidato): + mostrar(candidato, horaInicio, fnGenesAEntradas) + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes, ecuaciones) + + def fnMudar(genes): + mudar(genes, geneSetOrdenado, ventana, geneÍndices) + + aptitudÓptima = Aptitud(0) + mejor = genetic.obtener_mejor(fnObtenerAptitud, numUnknowns, + aptitudÓptima, geneSet, fnMostrar, + fnMudar, edadMáxima=edadMáxima) + self.assertTrue(not aptitudÓptima > mejor.Aptitud) + + +class Aptitud: + def __init__(self, diferenciaTotal): + self.DiferenciaTotal = diferenciaTotal + + def __gt__(self, otro): + return self.DiferenciaTotal < otro.DiferenciaTotal + + def __str__(self): + return "dif: {:0.2f}".format(float(self.DiferenciaTotal)) + + +class Ventana: + def __init__(self, mínimo, máximo, tamaño): + self.Min = mínimo + self.Max = máximo + self.Tamaño = tamaño + + def deslizar(self): + self.Tamaño = self.Tamaño - 1 if self.Tamaño > self.Min else self.Max + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch11/sudoku.py b/es/ch11/sudoku.py new file mode 100644 index 0000000..7130e22 --- /dev/null +++ b/es/ch11/sudoku.py @@ -0,0 +1,171 @@ +# File: sudoku.py +# Del capítulo 11 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + + +import datetime +import random +import unittest + +import genetic + + +def obtener_aptitud(genes, reglasDeValidación): + try: + primeraReglaQueFalla = next(regla for regla in reglasDeValidación + if genes[regla.Índice] == genes[ + regla.OtroÍndice]) + except StopIteration: + aptitud = 100 + else: + aptitud = (1 + índice_fila(primeraReglaQueFalla.OtroÍndice)) * 10 \ + + (1 + índice_columna(primeraReglaQueFalla.OtroÍndice)) + return aptitud + + +def mostrar(candidato, horaInicio): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + + for filaId in range(9): + fila = ' | '.join( + ' '.join(str(i) + for i in + candidato.Genes[filaId * 9 + i:filaId * 9 + i + 3]) + for i in [0, 3, 6]) + print("", fila) + if filaId < 8 and filaId % 3 == 2: + print(" ----- + ----- + -----") + print(" - = - - = - - = - {}\t{}\n" + .format(candidato.Aptitud, diferencia)) + + +def mudar(genes, reglasDeValidación): + reglaSeleccionada = next(regla for regla in reglasDeValidación + if genes[regla.Índice] == genes[regla.OtroÍndice]) + if reglaSeleccionada is None: + return + + if índice_fila(reglaSeleccionada.OtroÍndice) % 3 == 2 \ + and random.randint(0, 10) == 0: + secciónPrincipio = sección_principio(reglaSeleccionada.Índice) + actual = reglaSeleccionada.OtroÍndice + while reglaSeleccionada.OtroÍndice == actual: + barajar_en_su_lugar(genes, secciónPrincipio, 80) + reglaSeleccionada = next(regla for regla in reglasDeValidación + if genes[regla.Índice] == genes[ + regla.OtroÍndice]) + return + fila = índice_fila(reglaSeleccionada.OtroÍndice) + principio = fila * 9 + índiceA = reglaSeleccionada.OtroÍndice + índiceB = random.randrange(principio, len(genes)) + genes[índiceA], genes[índiceB] = genes[índiceB], genes[índiceA] + + +def barajar_en_su_lugar(genes, primero, último): + while primero < último: + índice = random.randint(primero, último) + genes[primero], genes[índice] = genes[índice], genes[primero] + primero += 1 + + +class PruebasDeSudoku(unittest.TestCase): + def test(self): + geneSet = [i for i in range(1, 9 + 1)] + horaInicio = datetime.datetime.now() + valorÓptimo = 100 + + def fnMostrar(candidato): + mostrar(candidato, horaInicio) + + reglasDeValidación = construir_las_reglas_de_validación() + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes, reglasDeValidación) + + def fnCrear(): + return random.sample(geneSet * 9, 81) + + def fnMudar(genes): + mudar(genes, reglasDeValidación) + + mejor = genetic.obtener_mejor(fnObtenerAptitud, None, valorÓptimo, + None, fnMostrar, fnMudar, fnCrear, + edadMáxima=50) + self.assertEqual(mejor.Aptitud, valorÓptimo) + + def test_comparativa(self): + genetic.Comparar.ejecutar(lambda: self.test()) + + +def construir_las_reglas_de_validación(): + reglas = [] + for índice in range(80): + suFila = índice_fila(índice) + suColumna = índice_columna(índice) + suSección = fila_columna_sección(suFila, suColumna) + + for índice2 in range(índice + 1, 81): + otroFila = índice_fila(índice2) + otroColumna = índice_columna(índice2) + otroSección = fila_columna_sección(otroFila, otroColumna) + if suFila == otroFila or \ + suColumna == otroColumna or \ + suSección == otroSección: + reglas.append(Regla(índice, índice2)) + + reglas.sort(key=lambda x: x.OtroÍndice * 100 + x.Índice) + return reglas + + +def índice_fila(índice): + return int(índice / 9) + + +def índice_columna(índice): + return int(índice % 9) + + +def fila_columna_sección(fila, columna): + return int(fila / 3) * 3 + int(columna / 3) + + +def índice_sección(índice): + return fila_columna_sección(índice_fila(índice), índice_columna(índice)) + + +def sección_principio(índice): + return int((índice_fila(índice) % 9) / 3) * 27 + int( + índice_columna(índice) / 3) * 3 + + +class Regla: + def __init__(self, eso, otro): + if eso > otro: + eso, otro = otro, eso + self.Índice = eso + self.OtroÍndice = otro + + def __eq__(self, otro): + return self.Índice == otro.Índice and \ + self.OtroÍndice == otro.OtroÍndice + + def __hash__(self): + return self.Índice * 100 + self.OtroÍndice + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch12/genetic.py b/es/ch12/genetic.py new file mode 100644 index 0000000..7b71058 --- /dev/null +++ b/es/ch12/genetic.py @@ -0,0 +1,192 @@ +# File: genetic.py +# Del capítulo 12 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import random +import statistics +import sys +import time +from bisect import bisect_left +from enum import Enum +from math import exp + + +def _generar_padre(longitud, geneSet, obtener_aptitud): + genes = [] + while len(genes) < longitud: + tamañoMuestral = min(longitud - len(genes), len(geneSet)) + genes.extend(random.sample(geneSet, tamañoMuestral)) + aptitud = obtener_aptitud(genes) + return Cromosoma(genes, aptitud, Estrategias.Creación) + + +def _mudar(padre, geneSet, obtener_aptitud): + genesDelNiño = padre.Genes[:] + índice = random.randrange(0, len(padre.Genes)) + nuevoGen, alterno = random.sample(geneSet, 2) + genesDelNiño[índice] = alterno if nuevoGen == genesDelNiño[ + índice] else nuevoGen + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Mutación) + + +def _mudar_personalizada(padre, mutación_personalizada, obtener_aptitud): + genesDelNiño = padre.Genes[:] + mutación_personalizada(genesDelNiño) + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Mutación) + + +def _intercambiar(genesDePadre, índice, padres, obtener_aptitud, intercambiar, + mudar, generar_padre): + índiceDeDonante = random.randrange(0, len(padres)) + if índiceDeDonante == índice: + índiceDeDonante = (índiceDeDonante + 1) % len(padres) + genesDelNiño = intercambiar(genesDePadre, padres[índiceDeDonante].Genes) + if genesDelNiño is None: + # padre y donante son indistinguibles + padres[índiceDeDonante] = generar_padre() + return mudar(padres[índice]) + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Intercambio) + + +def obtener_mejor(obtener_aptitud, longitudObjetivo, aptitudÓptima, geneSet, + mostrar, mutación_personalizada=None, + creación_personalizada=None, edadMáxima=None, + tamañoDePiscina=1, intercambiar=None): + if mutación_personalizada is None: + def fnMudar(padre): + return _mudar(padre, geneSet, obtener_aptitud) + else: + def fnMudar(padre): + return _mudar_personalizada(padre, mutación_personalizada, + obtener_aptitud) + + if creación_personalizada is None: + def fnGenerarPadre(): + return _generar_padre(longitudObjetivo, geneSet, obtener_aptitud) + else: + def fnGenerarPadre(): + genes = creación_personalizada() + return Cromosoma(genes, obtener_aptitud(genes), + Estrategias.Creación) + + búsquedaDeEstrategia = { + Estrategias.Creación: lambda p, i, o: fnGenerarPadre(), + Estrategias.Mutación: lambda p, i, o: fnMudar(p), + Estrategias.Intercambio: lambda p, i, o: + _intercambiar(p.Genes, i, o, obtener_aptitud, intercambiar, fnMudar, + fnGenerarPadre) + } + + estrategiasUsadas = [búsquedaDeEstrategia[Estrategias.Mutación]] + if intercambiar is not None: + estrategiasUsadas.append(búsquedaDeEstrategia[Estrategias.Intercambio]) + + def fnNuevoNiño(padre, índice, padres): + return random.choice(estrategiasUsadas)(padre, índice, padres) + else: + def fnNuevoNiño(padre, índice, padres): + return fnMudar(padre) + + for mejora in _obtener_mejoras(fnNuevoNiño, fnGenerarPadre, edadMáxima, + tamañoDePiscina): + mostrar(mejora) + f = búsquedaDeEstrategia[mejora.Estrategia] + estrategiasUsadas.append(f) + if not aptitudÓptima > mejora.Aptitud: + return mejora + + +def _obtener_mejoras(nuevo_niño, generar_padre, edadMáxima, tamañoDePiscina): + mejorPadre = generar_padre() + yield mejorPadre + padres = [mejorPadre] + aptitudesHistóricas = [mejorPadre.Aptitud] + for _ in range(tamañoDePiscina - 1): + padre = generar_padre() + if padre.Aptitud > mejorPadre.Aptitud: + yield padre + mejorPadre = padre + aptitudesHistóricas.append(padre.Aptitud) + padres.append(padre) + índiceDelÚltimoPadre = tamañoDePiscina - 1 + pÍndice = 1 + while True: + pÍndice = pÍndice - 1 if pÍndice > 0 else índiceDelÚltimoPadre + padre = padres[pÍndice] + niño = nuevo_niño(padre, pÍndice, padres) + if padre.Aptitud > niño.Aptitud: + if edadMáxima is None: + continue + padre.Edad += 1 + if edadMáxima > padre.Edad: + continue + índice = bisect_left(aptitudesHistóricas, niño.Aptitud, 0, + len(aptitudesHistóricas)) + diferencia = len(aptitudesHistóricas) - índice + proporciónSimilar = diferencia / len(aptitudesHistóricas) + if random.random() < exp(-proporciónSimilar): + padres[pÍndice] = niño + continue + padres[pÍndice] = mejorPadre + padre.Edad = 0 + continue + if not niño.Aptitud > padre.Aptitud: + # mismo aptitud + niño.Edad = padre.Edad + 1 + padres[pÍndice] = niño + continue + padres[pÍndice] = niño + padre.Edad = 0 + if niño.Aptitud > mejorPadre.Aptitud: + yield niño + mejorPadre = niño + aptitudesHistóricas.append(niño.Aptitud) + + +class Cromosoma: + def __init__(self, genes, aptitud, estrategia): + self.Genes = genes + self.Aptitud = aptitud + self.Estrategia = estrategia + self.Edad = 0 + + +class Estrategias(Enum): + Creación = 0, + Mutación = 1, + Intercambio = 2 + + +class Comparar: + @staticmethod + def ejecutar(función): + cronometrajes = [] + stdout = sys.stdout + for i in range(100): + sys.stdout = None + horaInicio = time.time() + función() + segundos = time.time() - horaInicio + sys.stdout = stdout + cronometrajes.append(segundos) + promedio = statistics.mean(cronometrajes) + if i < 10 or i % 10 == 9: + print("{} {:3.2f} {:3.2f}".format( + 1 + i, promedio, + statistics.stdev(cronometrajes, promedio) if i > 1 else 0)) diff --git a/es/ch12/vendedor.py b/es/ch12/vendedor.py new file mode 100644 index 0000000..65ce9cc --- /dev/null +++ b/es/ch12/vendedor.py @@ -0,0 +1,211 @@ +# File: vendedor.py +# Del capítulo 12 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import math +import random +import unittest +from itertools import chain + +import genetic + + +def obtener_aptitud(genes, búsquedaDeUbicación): + aptitud = obtener_distancia(búsquedaDeUbicación[genes[0]], + búsquedaDeUbicación[genes[-1]]) + + for i in range(len(genes) - 1): + principio = búsquedaDeUbicación[genes[i]] + end = búsquedaDeUbicación[genes[i + 1]] + aptitud += obtener_distancia(principio, end) + + return Aptitud(round(aptitud, 2)) + + +def mostrar(candidato, horaInicio): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + print("{}\t{}\t{}\t{}".format( + ' '.join(map(str, candidato.Genes)), + candidato.Aptitud, + candidato.Estrategia.name, + diferencia)) + + +def obtener_distancia(ubicaciónA, ubicaciónB): + ladoA = ubicaciónA[0] - ubicaciónB[0] + ladoB = ubicaciónA[1] - ubicaciónB[1] + ladoC = math.sqrt(ladoA * ladoA + ladoB * ladoB) + return ladoC + + +def mudar(genes, fnObtenerAptitud): + cuenta = random.randint(2, len(genes)) + aptitudInicial = fnObtenerAptitud(genes) + while cuenta > 0: + cuenta -= 1 + índiceA, índiceB = random.sample(range(len(genes)), 2) + genes[índiceA], genes[índiceB] = genes[índiceB], genes[índiceA] + aptitud = fnObtenerAptitud(genes) + if aptitud > aptitudInicial: + return + + +def intercambiar(genesDePadre, donanteGenes, fnObtenerAptitud): + pares = {Par(donanteGenes[0], donanteGenes[-1]): 0} + + for i in range(len(donanteGenes) - 1): + pares[Par(donanteGenes[i], donanteGenes[i + 1])] = 0 + + genesTemporales = genesDePadre[:] + if Par(genesDePadre[0], genesDePadre[-1]) in pares: + # encontrar una discontinuidad + encontró = False + for i in range(len(genesDePadre) - 1): + if Par(genesDePadre[i], genesDePadre[i + 1]) in pares: + continue + genesTemporales = genesDePadre[i + 1:] + genesDePadre[:i + 1] + encontró = True + break + if not encontró: + return None + + series = [[genesTemporales[0]]] + for i in range(len(genesTemporales) - 1): + if Par(genesTemporales[i], genesTemporales[i + 1]) in pares: + series[-1].append(genesTemporales[i + 1]) + continue + series.append([genesTemporales[i + 1]]) + + aptitudInicial = fnObtenerAptitud(genesDePadre) + cuenta = random.randint(2, 20) + índicesDeSerie = range(len(series)) + while cuenta > 0: + cuenta -= 1 + for i in índicesDeSerie: + if len(series[i]) == 1: + continue + if random.randint(0, len(series)) == 0: + series[i] = [n for n in reversed(series[i])] + + índiceA, índiceB = random.sample(índicesDeSerie, 2) + series[índiceA], series[índiceB] = series[índiceB], series[índiceA] + genesDelNiño = list(chain.from_iterable(series)) + if fnObtenerAptitud(genesDelNiño) > aptitudInicial: + return genesDelNiño + return genesDelNiño + + +class PruebasDeVendedorAmbulante(unittest.TestCase): + def test_8_reinas(self): + búsquedaDeUbicación = { + 'A': [4, 7], + 'B': [2, 6], + 'C': [0, 5], + 'D': [1, 3], + 'E': [3, 0], + 'F': [5, 1], + 'G': [7, 2], + 'H': [6, 4] + } + secuenciaÓptima = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] + self.resolver(búsquedaDeUbicación, secuenciaÓptima) + + def test_ulysses16(self): + búsquedaDeUbicación = cargar_datos("ulysses16.tsp") + secuenciaÓptima = [14, 13, 12, 16, 1, 3, 2, 4, + 8, 15, 5, 11, 9, 10, 7, 6] + self.resolver(búsquedaDeUbicación, secuenciaÓptima) + + def test_comparativa(self): + genetic.Comparar.ejecutar(lambda: self.test_ulysses16()) + + def resolver(self, búsquedaDeUbicación, secuenciaÓptima): + geneSet = [i for i in búsquedaDeUbicación.keys()] + + def fnCrear(): + return random.sample(geneSet, len(geneSet)) + + def fnMostrar(candidato): + mostrar(candidato, horaInicio) + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes, búsquedaDeUbicación) + + def fnMudar(genes): + mudar(genes, fnObtenerAptitud) + + def fnIntercambio(padre, donante): + return intercambiar(padre, donante, fnObtenerAptitud) + + aptitudÓptima = fnObtenerAptitud(secuenciaÓptima) + horaInicio = datetime.datetime.now() + mejor = genetic.obtener_mejor(fnObtenerAptitud, None, aptitudÓptima, + None, fnMostrar, fnMudar, fnCrear, + edadMáxima=500, tamañoDePiscina=25, + intercambiar=fnIntercambio) + self.assertTrue(not aptitudÓptima > mejor.Aptitud) + + +def cargar_datos(archivoLocal): + """ espera que: + Sección de HEADER antes de la sección de DATA, todos los filas + comienzan en columna 0 + Los elementos de la sección de DATA tienen un espacio en columna 0 + 1 23.45 67.89 + último fila de archivo es: " EOF" + """ + with open(archivoLocal, mode='r') as fuente: + contenido = fuente.read().splitlines() + búsquedaDeUbicación = {} + for fila in contenido: + if fila[0] != ' ': # HEADER + continue + if fila == " EOF": + break + + id, x, y = fila.split(' ')[1:4] + búsquedaDeUbicación[int(id)] = [float(x), float(y)] + return búsquedaDeUbicación + + +class Aptitud: + def __init__(self, distanciaTotal): + self.DistanciaTotal = distanciaTotal + + def __gt__(self, otro): + return self.DistanciaTotal < otro.DistanciaTotal + + def __str__(self): + return "{:0.2f}".format(self.DistanciaTotal) + + +class Par: + def __init__(self, nodo, adyacente): + if nodo < adyacente: + nodo, adyacente = adyacente, nodo + self.Nodo = nodo + self.Adyacente = adyacente + + def __eq__(self, otro): + return self.Nodo == otro.Nodo and self.Adyacente == otro.Adyacente + + def __hash__(self): + return hash(self.Nodo) * 397 ^ hash(self.Adyacente) + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch13/genetic.py b/es/ch13/genetic.py new file mode 100644 index 0000000..f952e8d --- /dev/null +++ b/es/ch13/genetic.py @@ -0,0 +1,204 @@ +# File: genetic.py +# Del capítulo 13 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import random +import statistics +import sys +import time +from bisect import bisect_left +from enum import Enum +from math import exp + + +def _generar_padre(longitud, geneSet, obtener_aptitud): + genes = [] + while len(genes) < longitud: + tamañoMuestral = min(longitud - len(genes), len(geneSet)) + genes.extend(random.sample(geneSet, tamañoMuestral)) + aptitud = obtener_aptitud(genes) + return Cromosoma(genes, aptitud, Estrategias.Creación) + + +def _mudar(padre, geneSet, obtener_aptitud): + genesDelNiño = padre.Genes[:] + índice = random.randrange(0, len(padre.Genes)) + nuevoGen, alterno = random.sample(geneSet, 2) + genesDelNiño[índice] = alterno if nuevoGen == genesDelNiño[ + índice] else nuevoGen + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Mutación) + + +def _mudar_personalizada(padre, mutación_personalizada, obtener_aptitud): + genesDelNiño = padre.Genes[:] + mutación_personalizada(genesDelNiño) + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Mutación) + + +def _intercambiar(genesDePadre, índice, padres, obtener_aptitud, intercambiar, + mudar, generar_padre): + índiceDeDonante = random.randrange(0, len(padres)) + if índiceDeDonante == índice: + índiceDeDonante = (índiceDeDonante + 1) % len(padres) + genesDelNiño = intercambiar(genesDePadre, padres[índiceDeDonante].Genes) + if genesDelNiño is None: + # padre y donante son indistinguibles + padres[índiceDeDonante] = generar_padre() + return mudar(padres[índice]) + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Intercambio) + + +def obtener_mejor(obtener_aptitud, longitudObjetivo, aptitudÓptima, geneSet, + mostrar, mutación_personalizada=None, + creación_personalizada=None, edadMáxima=None, + tamañoDePiscina=1, intercambiar=None, segundosMáximos=None): + if mutación_personalizada is None: + def fnMudar(padre): + return _mudar(padre, geneSet, obtener_aptitud) + else: + def fnMudar(padre): + return _mudar_personalizada(padre, mutación_personalizada, + obtener_aptitud) + + if creación_personalizada is None: + def fnGenerarPadre(): + return _generar_padre(longitudObjetivo, geneSet, obtener_aptitud) + else: + def fnGenerarPadre(): + genes = creación_personalizada() + return Cromosoma(genes, obtener_aptitud(genes), + Estrategias.Creación) + + búsquedaDeEstrategia = { + Estrategias.Creación: lambda p, i, o: fnGenerarPadre(), + Estrategias.Mutación: lambda p, i, o: fnMudar(p), + Estrategias.Intercambio: lambda p, i, o: + _intercambiar(p.Genes, i, o, obtener_aptitud, intercambiar, fnMudar, + fnGenerarPadre) + } + + estrategiasUsadas = [búsquedaDeEstrategia[Estrategias.Mutación]] + if intercambiar is not None: + estrategiasUsadas.append(búsquedaDeEstrategia[Estrategias.Intercambio]) + + def fnNuevoNiño(padre, índice, padres): + return random.choice(estrategiasUsadas)(padre, índice, padres) + else: + def fnNuevoNiño(padre, índice, padres): + return fnMudar(padre) + + for caducado, mejora in \ + _obtener_mejoras(fnNuevoNiño, fnGenerarPadre, edadMáxima, + tamañoDePiscina, segundosMáximos): + if caducado: + return mejora + mostrar(mejora) + f = búsquedaDeEstrategia[mejora.Estrategia] + estrategiasUsadas.append(f) + if not aptitudÓptima > mejora.Aptitud: + return mejora + + +def _obtener_mejoras(nuevo_niño, generar_padre, edadMáxima, tamañoDePiscina, + segundosMáximos): + horaInicio = time.time() + mejorPadre = generar_padre() + yield segundosMáximos is not None and time.time() \ + - horaInicio > segundosMáximos, mejorPadre + padres = [mejorPadre] + aptitudesHistóricas = [mejorPadre.Aptitud] + for _ in range(tamañoDePiscina - 1): + padre = generar_padre() + if segundosMáximos is not None and time.time() - horaInicio > \ + segundosMáximos: + yield True, padre + if padre.Aptitud > mejorPadre.Aptitud: + yield False, padre + mejorPadre = padre + aptitudesHistóricas.append(padre.Aptitud) + padres.append(padre) + índiceDelÚltimoPadre = tamañoDePiscina - 1 + pÍndice = 1 + while True: + if segundosMáximos is not None and time.time() - horaInicio > \ + segundosMáximos: + yield True, mejorPadre + pÍndice = pÍndice - 1 if pÍndice > 0 else índiceDelÚltimoPadre + padre = padres[pÍndice] + niño = nuevo_niño(padre, pÍndice, padres) + if padre.Aptitud > niño.Aptitud: + if edadMáxima is None: + continue + padre.Edad += 1 + if edadMáxima > padre.Edad: + continue + índice = bisect_left(aptitudesHistóricas, niño.Aptitud, 0, + len(aptitudesHistóricas)) + diferencia = len(aptitudesHistóricas) - índice + proporciónSimilar = diferencia / len(aptitudesHistóricas) + if random.random() < exp(-proporciónSimilar): + padres[pÍndice] = niño + continue + padres[pÍndice] = mejorPadre + padre.Edad = 0 + continue + if not niño.Aptitud > padre.Aptitud: + # mismo aptitud + niño.Edad = padre.Edad + 1 + padres[pÍndice] = niño + continue + padres[pÍndice] = niño + padre.Edad = 0 + if niño.Aptitud > mejorPadre.Aptitud: + yield False, niño + mejorPadre = niño + aptitudesHistóricas.append(niño.Aptitud) + + +class Cromosoma: + def __init__(self, genes, aptitud, estrategia): + self.Genes = genes + self.Aptitud = aptitud + self.Estrategia = estrategia + self.Edad = 0 + + +class Estrategias(Enum): + Creación = 0, + Mutación = 1, + Intercambio = 2 + + +class Comparar: + @staticmethod + def ejecutar(función): + cronometrajes = [] + stdout = sys.stdout + for i in range(100): + sys.stdout = None + horaInicio = time.time() + función() + segundos = time.time() - horaInicio + sys.stdout = stdout + cronometrajes.append(segundos) + promedio = statistics.mean(cronometrajes) + if i < 10 or i % 10 == 9: + print("{} {:3.2f} {:3.2f}".format( + 1 + i, promedio, + statistics.stdev(cronometrajes, promedio) if i > 1 else 0)) diff --git a/es/ch13/pi.py b/es/ch13/pi.py new file mode 100644 index 0000000..ae0e0ba --- /dev/null +++ b/es/ch13/pi.py @@ -0,0 +1,147 @@ +# File: pi.py +# Del capítulo 13 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import math +import random +import sys +import time +import unittest + +import genetic + + +def obtener_aptitud(genes, valoresDeBits): + denominator = obtener_denominador(genes, valoresDeBits) + if denominator == 0: + return 0 + + relación = obtener_numerador(genes, valoresDeBits) / denominator + return math.pi - math.fabs(math.pi - relación) + + +def mostrar(candidato, horaInicio, valoresDeBits): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + numerador = obtener_numerador(candidato.Genes, valoresDeBits) + denominator = obtener_denominador(candidato.Genes, valoresDeBits) + print("{}/{}\t{}\t{}".format( + numerador, + denominator, + candidato.Aptitud, diferencia)) + + +def bits_a_entero(bits, valoresDeBits): + resultado = 0 + for i, bit in enumerate(bits): + if bit == 0: + continue + resultado += valoresDeBits[i] + return resultado + + +def obtener_numerador(genes, valoresDeBits): + return 1 + bits_a_entero(genes[:len(valoresDeBits)], valoresDeBits) + + +def obtener_denominador(genes, valoresDeBits): + return bits_a_entero(genes[len(valoresDeBits):], valoresDeBits) + + +def mudar(genes, numBits): + índiceDelNumerador, índiceDelDenominador \ + = random.randrange(0, numBits), random.randrange(numBits, + len(genes)) + genes[índiceDelNumerador] = 1 - genes[índiceDelNumerador] + genes[índiceDelDenominador] = 1 - genes[índiceDelDenominador] + + +class PruebasDePi(unittest.TestCase): + def test(self, valoresDeBits=[512, 256, 128, 64, 32, 16, 8, 4, 2, 1], + segundosMáximos=None): + geneSet = [i for i in range(2)] + horaInicio = datetime.datetime.now() + + def fnMostrar(candidato): + mostrar(candidato, horaInicio, valoresDeBits) + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes, valoresDeBits) + + aptitudÓptima = 3.14159 + + def fnMudar(genes): + mudar(genes, len(valoresDeBits)) + + longitud = 2 * len(valoresDeBits) + mejor = genetic.obtener_mejor(fnObtenerAptitud, longitud, + aptitudÓptima, geneSet, fnMostrar, + fnMudar, edadMáxima=250, + segundosMáximos=segundosMáximos) + return mejor.Aptitud >= aptitudÓptima + + def test_optimize(self): + geneSet = [i for i in range(1, 512 + 1)] + longitud = 10 + segundosMáximos = 2 + + def fnObtenerAptitud(genes): + horaInicio = time.time() + cuenta = 0 + stdout = sys.stdout + sys.stdout = None + while time.time() - horaInicio < segundosMáximos: + if self.test(genes, segundosMáximos): + cuenta += 1 + sys.stdout = stdout + distancia = abs(sum(genes) - 1023) + fracción = 1 / distancia if distancia > 0 else distancia + cuenta += round(fracción, 4) + return cuenta + + def fnMostrar(cromosoma): + print("{}\t{}".format(cromosoma.Genes, cromosoma.Aptitud)) + + initial = [512, 256, 128, 64, 32, 16, 8, 4, 2, 1] + print("initial:", initial, fnObtenerAptitud(initial)) + + aptitudÓptima = 10 * segundosMáximos + genetic.obtener_mejor(fnObtenerAptitud, longitud, aptitudÓptima, + geneSet, fnMostrar, segundosMáximos=600) + + def test_comparativa(self): + genetic.Comparar.ejecutar( + lambda: self.test([98, 334, 38, 339, 117, + 39, 145, 123, 40, 129])) + + def test_encontrar_10_mejores_aproximaciones(self): + mejor = {} + for numerador in range(1, 1024): + for denominator in range(1, 1024): + relación = numerador / denominator + piDist = math.pi - abs(math.pi - relación) + if piDist not in mejor or mejor[piDist][0] > numerador: + mejor[piDist] = [numerador, denominator] + + mejoresAproximaciones = list(reversed(sorted(mejor.keys()))) + for i in range(10): + relación = mejoresAproximaciones[i] + nd = mejor[relación] + print("%i / %i\t%f" % (nd[0], nd[1], relación)) + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch14/ecuaciones.py b/es/ch14/ecuaciones.py new file mode 100644 index 0000000..3466cc0 --- /dev/null +++ b/es/ch14/ecuaciones.py @@ -0,0 +1,177 @@ +# File: ecuaciones.py +# Del capítulo 14 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import random +import unittest + +import genetic + + +def evaluar(genes, operacionesPriorizadas): + ecuación = genes[:] + for operationSet in operacionesPriorizadas: + iOffset = 0 + for i in range(1, len(ecuación), 2): + i += iOffset + opSímbolo = ecuación[i] + if opSímbolo in operationSet: + operandoIzquierdo = ecuación[i - 1] + operandoDerecho = ecuación[i + 1] + ecuación[i - 1] = operationSet[opSímbolo](operandoIzquierdo, + operandoDerecho) + del ecuación[i + 1] + del ecuación[i] + iOffset += -2 + return ecuación[0] + + +def añadir(a, b): + return a + b + + +def sustraer(a, b): + return a - b + + +def multiplicar(a, b): + return a * b + + +def obtener_aptitud(genes, totalEsperado, fnEvaluar): + resultado = fnEvaluar(genes) + + if resultado != totalEsperado: + aptitud = totalEsperado - abs(resultado - totalEsperado) + else: + aptitud = 1000 - len(genes) + + return aptitud + + +def mostrar(candidato, horaInicio): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + print("{}\t{}\t{}".format( + (' '.join(map(str, [i for i in candidato.Genes]))), + candidato.Aptitud, + diferencia)) + + +def crear(números, operaciones, númerosMin, númerosMax): + genes = [random.choice(números)] + cuenta = random.randint(númerosMin, 1 + númerosMax) + while cuenta > 1: + cuenta -= 1 + genes.append(random.choice(operaciones)) + genes.append(random.choice(números)) + return genes + + +def mudar(genes, números, operaciones, númerosMin, númerosMax, + fnObtenerAptitud): + cuenta = random.randint(1, 10) + aptitudInicial = fnObtenerAptitud(genes) + while cuenta > 0: + cuenta -= 1 + if fnObtenerAptitud(genes) > aptitudInicial: + return + cuentaDeNúmeros = (1 + len(genes)) / 2 + añadiendo = cuentaDeNúmeros < númerosMax and \ + random.randint(0, 100) == 0 + if añadiendo: + genes.append(random.choice(operaciones)) + genes.append(random.choice(números)) + continue + + eliminando = cuentaDeNúmeros > númerosMin and \ + random.randint(0, 20) == 0 + if eliminando: + índice = random.randrange(0, len(genes) - 1) + del genes[índice] + del genes[índice] + continue + + índice = random.randrange(0, len(genes)) + genes[índice] = random.choice(operaciones) \ + if (índice & 1) == 1 else random.choice(números) + + +class PruebasDeEcuaciones(unittest.TestCase): + def test_adición(self): + operaciones = ['+', '-'] + operacionesPriorizadas = [{'+': añadir, + '-': sustraer}] + soluciónDeLongitudÓptima = [7, '+', 7, '+', 7, '+', 7, '+', 7, '-', 6] + self.resolver(operaciones, operacionesPriorizadas, + soluciónDeLongitudÓptima) + + def test_multiplicación(self): + operaciones = ['+', '-', '*'] + operacionesPriorizadas = [{'*': multiplicar}, + {'+': añadir, + '-': sustraer}] + soluciónDeLongitudÓptima = [6, '*', 3, '*', 3, '*', 6, '-', 7] + self.resolver(operaciones, operacionesPriorizadas, + soluciónDeLongitudÓptima) + + def test_exponente(self): + operaciones = ['^', '+', '-', '*'] + operacionesPriorizadas = [{'^': lambda a, b: a ** b}, + {'*': multiplicar}, + {'+': añadir, + '-': sustraer}] + soluciónDeLongitudÓptima = [6, '^', 3, '*', 2, '-', 5] + self.resolver(operaciones, operacionesPriorizadas, + soluciónDeLongitudÓptima) + + def resolver(self, operaciones, operacionesPriorizadas, + soluciónDeLongitudÓptima): + números = [1, 2, 3, 4, 5, 6, 7] + totalEsperado = evaluar(soluciónDeLongitudÓptima, + operacionesPriorizadas) + númerosMin = (1 + len(soluciónDeLongitudÓptima)) / 2 + númerosMax = 6 * númerosMin + horaInicio = datetime.datetime.now() + + def fnMostrar(candidato): + mostrar(candidato, horaInicio) + + def fnEvaluar(genes): + return evaluar(genes, operacionesPriorizadas) + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes, totalEsperado, fnEvaluar) + + def fnCrear(): + return crear(números, operaciones, númerosMin, númerosMax) + + def fnMudar(niño): + mudar(niño, números, operaciones, númerosMin, númerosMax, + fnObtenerAptitud) + + aptitudÓptima = fnObtenerAptitud(soluciónDeLongitudÓptima) + mejor = genetic.obtener_mejor(fnObtenerAptitud, None, aptitudÓptima, + None, fnMostrar, fnMudar, fnCrear, + edadMáxima=50) + self.assertTrue(not aptitudÓptima > mejor.Aptitud) + + def test_comparativa(self): + genetic.Comparar.ejecutar(self.test_exponente) + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch15/cortadora.py b/es/ch15/cortadora.py new file mode 100644 index 0000000..1c8070b --- /dev/null +++ b/es/ch15/cortadora.py @@ -0,0 +1,167 @@ +# File: cortadora.py +# Del capítulo 15 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +from enum import Enum + + +class ContenidoDelCampo(Enum): + Hierba = ' #' + Cortado = ' .' + Cortador = 'C' + + def __str__(self): + return self.value + + +class Dirección: + def __init__(self, índice, xOffset, yOffset, símbolo): + self.Índice = índice + self.XOffset = xOffset + self.YOffset = yOffset + self.Símbolo = símbolo + + def mover_de(self, ubicación, distancia=1): + return Ubicación(ubicación.X + distancia * self.XOffset, + ubicación.Y + distancia * self.YOffset) + + +class Direcciones(Enum): + Norte = Dirección(0, 0, -1, '^') + Este = Dirección(1, 1, 0, '>') + Sur = Dirección(2, 0, 1, 'v') + Oeste = Dirección(3, -1, 0, '<') + + @staticmethod + def obtener_dirección_después_de_girar_a_la_izquierda_90_grados(dirección): + nuevoÍndice = dirección.Índice - 1 \ + if dirección.Índice > 0 \ + else len(Direcciones) - 1 + nuevaDirección = next(i for i in Direcciones + if i.value.Índice == nuevoÍndice) + return nuevaDirección.value + + @staticmethod + def obtener_dirección_después_de_girar_a_la_derecha_90_grados(dirección): + nuevoÍndice = dirección.Índice + 1 \ + if dirección.Índice < len(Direcciones) - 1 \ + else 0 + nuevaDirección = next(i for i in Direcciones + if i.value.Índice == nuevoÍndice) + return nuevaDirección.value + + +class Ubicación: + def __init__(self, x, y): + self.X, self.Y = x, y + + def mover(self, xOffset, yOffset): + return Ubicación(self.X + xOffset, self.Y + yOffset) + + +class Cortadora: + def __init__(self, ubicación, dirección): + self.Ubicación = ubicación + self.Dirección = dirección + self.CuentaDePasos = 0 + + def girar_a_la_izquierda(self): + self.CuentaDePasos += 1 + self.Dirección = Direcciones \ + .obtener_dirección_después_de_girar_a_la_izquierda_90_grados( + self.Dirección) + + def corta(self, campo): + nuevaUbicación = self.Dirección.mover_de(self.Ubicación) + nuevaUbicación, esVálida = campo.arreglar_ubicación(nuevaUbicación) + if esVálida: + self.Ubicación = nuevaUbicación + self.CuentaDePasos += 1 + campo.ajuste(self.Ubicación, + self.CuentaDePasos if self.CuentaDePasos > 9 + else " {}".format(self.CuentaDePasos)) + + def salta(self, campo, adelante, derecha): + nuevaUbicación = self.Dirección.mover_de(self.Ubicación, adelante) + derechaDirección = Direcciones \ + .obtener_dirección_después_de_girar_a_la_derecha_90_grados( + self.Dirección) + nuevaUbicación = derechaDirección.mover_de(nuevaUbicación, derecha) + nuevaUbicación, esVálida = campo.arreglar_ubicación(nuevaUbicación) + if esVálida: + self.Ubicación = nuevaUbicación + self.CuentaDePasos += 1 + campo.ajuste(self.Ubicación, self.CuentaDePasos + if self.CuentaDePasos > 9 + else " {}".format(self.CuentaDePasos)) + + +class Campo: + def __init__(self, anchura, altura, contenidoInicial): + self.Campo = [[contenidoInicial] * anchura for _ in range(altura)] + self.Anchura = anchura + self.Altura = altura + + def ajuste(self, ubicación, símbolo): + self.Campo[ubicación.Y][ubicación.X] = símbolo + + def cuente_cortada(self): + return sum(1 for fila in range(self.Altura) + for columna in range(self.Anchura) + if self.Campo[fila][columna] != ContenidoDelCampo.Hierba) + + def mostrar(self, cortadora): + for índiceDeFilas in range(self.Altura): + if índiceDeFilas != cortadora.Ubicación.Y: + fila = ' '.join(map(str, self.Campo[índiceDeFilas])) + else: + r = self.Campo[índiceDeFilas][:] + r[cortadora.Ubicación.X] = "{}{}".format( + ContenidoDelCampo.Cortador, cortadora.Dirección.Símbolo) + fila = ' '.join(map(str, r)) + print(fila) + + +class CampoValidando(Campo): + def __init__(self, anchura, altura, contenidoInicial): + super().__init__(anchura, altura, contenidoInicial) + + def arreglar_ubicación(self, ubicación): + if ubicación.X >= self.Anchura or \ + ubicación.X < 0 or \ + ubicación.Y >= self.Altura or \ + ubicación.Y < 0: + return None, False + return ubicación, True + + +class CampoToroidal(Campo): + def __init__(self, anchura, altura, contenidoInicial): + super().__init__(anchura, altura, contenidoInicial) + + def arreglar_ubicación(self, ubicación): + nuevaUbicación = Ubicación(ubicación.X, ubicación.Y) + if nuevaUbicación.X < 0: + nuevaUbicación.X += self.Anchura + elif nuevaUbicación.X >= self.Anchura: + nuevaUbicación.X %= self.Anchura + + if nuevaUbicación.Y < 0: + nuevaUbicación.Y += self.Altura + elif nuevaUbicación.Y >= self.Altura: + nuevaUbicación.Y %= self.Altura + + return nuevaUbicación, True diff --git a/es/ch15/genetic.py b/es/ch15/genetic.py new file mode 100644 index 0000000..6a00549 --- /dev/null +++ b/es/ch15/genetic.py @@ -0,0 +1,204 @@ +# File: genetic.py +# Del capítulo 13 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import random +import statistics +import sys +import time +from bisect import bisect_left +from enum import Enum +from math import exp + + +def _generar_padre(longitud, geneSet, obtener_aptitud): + genes = [] + while len(genes) < longitud: + tamañoMuestral = min(longitud - len(genes), len(geneSet)) + genes.extend(random.sample(geneSet, tamañoMuestral)) + aptitud = obtener_aptitud(genes) + return Cromosoma(genes, aptitud, Estrategias.Creación) + + +def _mudar(padre, geneSet, obtener_aptitud): + genesDelNiño = padre.Genes[:] + índice = random.randrange(0, len(padre.Genes)) + nuevoGen, alterno = random.sample(geneSet, 2) + genesDelNiño[índice] = alterno if nuevoGen == genesDelNiño[ + índice] else nuevoGen + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Mutación) + + +def _mudar_personalizada(padre, mutación_personalizada, obtener_aptitud): + genesDelNiño = padre.Genes[:] + mutación_personalizada(genesDelNiño) + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Mutación) + + +def _intercambiar(genesDePadre, índice, padres, obtener_aptitud, intercambiar, + mudar, generar_padre): + índiceDeDonante = random.randrange(0, len(padres)) + if índiceDeDonante == índice: + índiceDeDonante = (índiceDeDonante + 1) % len(padres) + genesDelNiño = intercambiar(genesDePadre, padres[índiceDeDonante].Genes) + if genesDelNiño is None: + # padre y donante son indistinguibles + padres[índiceDeDonante] = generar_padre() + return mudar(padres[índice]) + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Intercambio) + + +def obtener_mejor(obtener_aptitud, longitudObjetivo, aptitudÓptima, geneSet, + mostrar, mutación_personalizada=None, + creación_personalizada=None, edadMáxima=None, + tamañoDePiscina=1, intercambiar=None, segundosMáximos=None): + if mutación_personalizada is None: + def fnMudar(padre): + return _mudar(padre, geneSet, obtener_aptitud) + else: + def fnMudar(padre): + return _mudar_personalizada(padre, mutación_personalizada, + obtener_aptitud) + + if creación_personalizada is None: + def fnGenerarPadre(): + return _generar_padre(longitudObjetivo, geneSet, obtener_aptitud) + else: + def fnGenerarPadre(): + genes = creación_personalizada() + return Cromosoma(genes, obtener_aptitud(genes), + Estrategias.Creación) + + búsquedaDeEstrategia = { + Estrategias.Creación: lambda p, i, o: fnGenerarPadre(), + Estrategias.Mutación: lambda p, i, o: fnMudar(p), + Estrategias.Intercambio: lambda p, i, o: + _intercambiar(p.Genes, i, o, obtener_aptitud, intercambiar, fnMudar, + fnGenerarPadre) + } + + estrategiasUsadas = [búsquedaDeEstrategia[Estrategias.Mutación]] + if intercambiar is not None: + estrategiasUsadas.append(búsquedaDeEstrategia[Estrategias.Intercambio]) + + def fnNuevoNiño(padre, índice, padres): + return random.choice(estrategiasUsadas)(padre, índice, padres) + else: + def fnNuevoNiño(padre, índice, padres): + return fnMudar(padre) + + for caducado, mejora in _obtener_mejoras(fnNuevoNiño, fnGenerarPadre, + edadMáxima, tamañoDePiscina, + segundosMáximos): + if caducado: + return mejora + mostrar(mejora) + f = búsquedaDeEstrategia[mejora.Estrategia] + estrategiasUsadas.append(f) + if not aptitudÓptima > mejora.Aptitud: + return mejora + + +def _obtener_mejoras(nuevo_niño, generar_padre, edadMáxima, tamañoDePiscina, + segundosMáximos): + horaInicio = time.time() + mejorPadre = generar_padre() + yield segundosMáximos is not None and time.time() \ + - horaInicio > segundosMáximos, mejorPadre + padres = [mejorPadre] + aptitudesHistóricas = [mejorPadre.Aptitud] + for _ in range(tamañoDePiscina - 1): + padre = generar_padre() + if segundosMáximos is not None and time.time() - horaInicio > \ + segundosMáximos: + yield True, padre + if padre.Aptitud > mejorPadre.Aptitud: + yield False, padre + mejorPadre = padre + aptitudesHistóricas.append(padre.Aptitud) + padres.append(padre) + índiceDelÚltimoPadre = tamañoDePiscina - 1 + pÍndice = 1 + while True: + if segundosMáximos is not None and time.time() - horaInicio > \ + segundosMáximos: + yield True, mejorPadre + pÍndice = pÍndice - 1 if pÍndice > 0 else índiceDelÚltimoPadre + padre = padres[pÍndice] + niño = nuevo_niño(padre, pÍndice, padres) + if padre.Aptitud > niño.Aptitud: + if edadMáxima is None: + continue + padre.Edad += 1 + if edadMáxima > padre.Edad: + continue + índice = bisect_left(aptitudesHistóricas, niño.Aptitud, 0, + len(aptitudesHistóricas)) + diferencia = len(aptitudesHistóricas) - índice + proporciónSimilar = diferencia / len(aptitudesHistóricas) + if random.random() < exp(-proporciónSimilar): + padres[pÍndice] = niño + continue + padres[pÍndice] = mejorPadre + padre.Edad = 0 + continue + if not niño.Aptitud > padre.Aptitud: + # mismo aptitud + niño.Edad = padre.Edad + 1 + padres[pÍndice] = niño + continue + padres[pÍndice] = niño + padre.Edad = 0 + if niño.Aptitud > mejorPadre.Aptitud: + yield False, niño + mejorPadre = niño + aptitudesHistóricas.append(niño.Aptitud) + + +class Cromosoma: + def __init__(self, genes, aptitud, estrategia): + self.Genes = genes + self.Aptitud = aptitud + self.Estrategia = estrategia + self.Edad = 0 + + +class Estrategias(Enum): + Creación = 0, + Mutación = 1, + Intercambio = 2 + + +class Comparar: + @staticmethod + def ejecutar(función): + cronometrajes = [] + stdout = sys.stdout + for i in range(100): + sys.stdout = None + horaInicio = time.time() + función() + segundos = time.time() - horaInicio + sys.stdout = stdout + cronometrajes.append(segundos) + promedio = statistics.mean(cronometrajes) + if i < 10 or i % 10 == 9: + print("{} {:3.2f} {:3.2f}".format( + 1 + i, promedio, + statistics.stdev(cronometrajes, promedio) if i > 1 else 0)) diff --git a/es/ch15/pruebas.py b/es/ch15/pruebas.py new file mode 100644 index 0000000..27c2851 --- /dev/null +++ b/es/ch15/pruebas.py @@ -0,0 +1,425 @@ +# File: pruebas.py +# Del capítulo 15 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import random +import unittest + +import genetic +from cortadora import * + + +def obtener_aptitud(genes, fnEvaluar): + campo, cortadora, _ = fnEvaluar(genes) + return Aptitud(campo.cuente_cortada(), len(genes), cortadora.CuentaDePasos) + + +def mostrar(candidato, horaInicio, fnEvaluar): + campo, cortadora, programa = fnEvaluar(candidato.Genes) + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + campo.mostrar(cortadora) + print("{}\t{}".format( + candidato.Aptitud, + diferencia)) + programa.print() + + +def mudar(genes, geneSet, mínGenes, máxGenes, fnObtenerAptitud, rondasMáximas): + cuenta = random.randint(1, rondasMáximas) + aptitudInicial = fnObtenerAptitud(genes) + while cuenta > 0: + cuenta -= 1 + if fnObtenerAptitud(genes) > aptitudInicial: + return + añadiendo = len(genes) == 0 or \ + (len(genes) < máxGenes and random.randint(0, 5) == 0) + if añadiendo: + genes.append(random.choice(geneSet)()) + continue + + eliminando = len(genes) > mínGenes and random.randint(0, 50) == 0 + if eliminando: + índice = random.randrange(0, len(genes)) + del genes[índice] + continue + + índice = random.randrange(0, len(genes)) + genes[índice] = random.choice(geneSet)() + + +def crear(geneSet, mínGenes, máxGenes): + cantidad = random.randint(mínGenes, máxGenes) + genes = [random.choice(geneSet)() for _ in range(1, cantidad)] + return genes + + +def intercambiar(padre, otroPadre): + genesDelNiño = padre[:] + if len(padre) <= 2 or len(otroPadre) < 2: + return genesDelNiño + longitud = random.randint(1, len(padre) - 2) + principio = random.randrange(0, len(padre) - longitud) + genesDelNiño[principio:principio + longitud] = \ + otroPadre[principio:principio + longitud] + return genesDelNiño + + +class PruebasDeCortadora(unittest.TestCase): + def test_corta_gira(self): + anchura = altura = 8 + geneSet = [lambda: Corta(), + lambda: Gira()] + mínGenes = anchura * altura + máxGenes = int(1.5 * mínGenes) + rondasMáximasDeMutación = 3 + númeroEsperadoDeInstrucciones = 78 + + def fnCrearCampo(): + return CampoToroidal(anchura, altura, + ContenidoDelCampo.Hierba) + + self.ejecutar_con(geneSet, anchura, altura, mínGenes, máxGenes, + númeroEsperadoDeInstrucciones, + rondasMáximasDeMutación, fnCrearCampo, + númeroEsperadoDeInstrucciones) + + def test_corta_gira_salta(self): + anchura = altura = 8 + geneSet = [lambda: Corta(), + lambda: Gira(), + lambda: Salta(random.randint(0, min(anchura, altura)), + random.randint(0, min(anchura, altura)))] + mínGenes = anchura * altura + máxGenes = int(1.5 * mínGenes) + rondasMáximasDeMutación = 1 + númeroEsperadoDeInstrucciones = 64 + + def fnCrearCampo(): + return CampoToroidal(anchura, altura, + ContenidoDelCampo.Hierba) + + self.ejecutar_con(geneSet, anchura, altura, mínGenes, máxGenes, + númeroEsperadoDeInstrucciones, + rondasMáximasDeMutación, fnCrearCampo, + númeroEsperadoDeInstrucciones) + + def test_corta_gira_salta_validando(self): + anchura = altura = 8 + geneSet = [lambda: Corta(), + lambda: Gira(), + lambda: Salta(random.randint(0, min(anchura, altura)), + random.randint(0, min(anchura, altura)))] + mínGenes = anchura * altura + máxGenes = int(1.5 * mínGenes) + rondasMáximasDeMutación = 3 + númeroEsperadoDeInstrucciones = 79 + + def fnCrearCampo(): + return CampoValidando(anchura, altura, + ContenidoDelCampo.Hierba) + + self.ejecutar_con(geneSet, anchura, altura, mínGenes, máxGenes, + númeroEsperadoDeInstrucciones, + rondasMáximasDeMutación, fnCrearCampo, + númeroEsperadoDeInstrucciones) + + def test_corta_gira_repite(self): + anchura = altura = 8 + geneSet = [lambda: Corta(), + lambda: Gira(), + lambda: Repite(random.randint(0, 8), + random.randint(0, 8))] + mínGenes = 3 + máxGenes = 20 + rondasMáximasDeMutación = 3 + númeroEsperadoDeInstrucciones = 9 + númeroEsperadoDePasos = 88 + + def fnCrearCampo(): + return CampoToroidal(anchura, altura, + ContenidoDelCampo.Hierba) + + self.ejecutar_con(geneSet, anchura, altura, mínGenes, máxGenes, + númeroEsperadoDeInstrucciones, + rondasMáximasDeMutación, fnCrearCampo, + númeroEsperadoDePasos) + + def test_corta_gira_salta_func(self): + anchura = altura = 8 + geneSet = [lambda: Corta(), + lambda: Gira(), + lambda: Salta(random.randint(0, min(anchura, altura)), + random.randint(0, min(anchura, altura))), + lambda: Func()] + mínGenes = 3 + máxGenes = 20 + rondasMáximasDeMutación = 3 + númeroEsperadoDeInstrucciones = 17 + númeroEsperadoDePasos = 64 + + def fnCrearCampo(): + return CampoToroidal(anchura, altura, + ContenidoDelCampo.Hierba) + + self.ejecutar_con(geneSet, anchura, altura, mínGenes, máxGenes, + númeroEsperadoDeInstrucciones, + rondasMáximasDeMutación, fnCrearCampo, + númeroEsperadoDePasos) + + def test_corta_gira_salta_llama(self): + anchura = altura = 8 + geneSet = [lambda: Corta(), + lambda: Gira(), + lambda: Salta(random.randint(0, min(anchura, altura)), + random.randint(0, min(anchura, altura))), + lambda: Func(expectLlama=True), + lambda: Llama(random.randint(0, 5))] + mínGenes = 3 + máxGenes = 20 + rondasMáximasDeMutación = 3 + númeroEsperadoDeInstrucciones = 18 + númeroEsperadoDePasos = 65 + + def fnCrearCampo(): + return CampoToroidal(anchura, altura, + ContenidoDelCampo.Hierba) + + self.ejecutar_con(geneSet, anchura, altura, mínGenes, máxGenes, + númeroEsperadoDeInstrucciones, + rondasMáximasDeMutación, fnCrearCampo, + númeroEsperadoDePasos) + + def ejecutar_con(self, geneSet, anchura, altura, mínGenes, máxGenes, + númeroEsperadoDeInstrucciones, rondasMáximasDeMutación, + fnCrearCampo, númeroEsperadoDePasos): + ubicaciónInicialDelCortador = Ubicación(int(anchura / 2), + int(altura / 2)) + direcciónInicialDelCortador = Direcciones.Sur.value + + def fnCrear(): + return crear(geneSet, 1, altura) + + def fnEvaluar(instrucciones): + programa = Programa(instrucciones) + cortadora = Cortadora(ubicaciónInicialDelCortador, + direcciónInicialDelCortador) + campo = fnCrearCampo() + try: + programa.evaluar(cortadora, campo) + except RecursionError: + pass + return campo, cortadora, programa + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes, fnEvaluar) + + horaInicio = datetime.datetime.now() + + def fnMostrar(candidato): + mostrar(candidato, horaInicio, fnEvaluar) + + def fnMudar(niño): + mudar(niño, geneSet, mínGenes, máxGenes, fnObtenerAptitud, + rondasMáximasDeMutación) + + aptitudÓptima = Aptitud(anchura * altura, + númeroEsperadoDeInstrucciones, + númeroEsperadoDePasos) + + mejor = genetic.obtener_mejor(fnObtenerAptitud, None, aptitudÓptima, + None, fnMostrar, fnMudar, fnCrear, + edadMáxima=None, tamañoDePiscina=10, + intercambiar=intercambiar) + + self.assertTrue(not mejor.Aptitud > aptitudÓptima) + + +class Corta: + def __init__(self): + pass + + @staticmethod + def ejecutar(cortadora, campo): + cortadora.corta(campo) + + def __str__(self): + return "corta" + + +class Gira: + def __init__(self): + pass + + @staticmethod + def ejecutar(cortadora, campo): + cortadora.girar_a_la_izquierda() + + def __str__(self): + return "gira" + + +class Salta: + def __init__(self, adelante, derecha): + self.Adelante = adelante + self.Derecha = derecha + + def ejecutar(self, cortadora, campo): + cortadora.salta(campo, self.Adelante, self.Derecha) + + def __str__(self): + return "salta({},{})".format(self.Adelante, self.Derecha) + + +class Repite: + def __init__(self, númeroDeOperaciones, veces): + self.NúmeroDeOperaciones = númeroDeOperaciones + self.Veces = veces + self.Instrucciones = [] + + def ejecutar(self, cortadora, campo): + for i in range(self.Veces): + for instrucción in self.Instrucciones: + instrucción.ejecutar(cortadora, campo) + + def __str__(self): + return "repite({},{})".format( + ' '.join(map(str, self.Instrucciones)) + if len(self.Instrucciones) > 0 + else self.NúmeroDeOperaciones, + self.Veces) + + +class Func: + def __init__(self, expectLlama=False): + self.Instrucciones = [] + self.ExpectLlama = expectLlama + self.Id = None + + def ejecutar(self, cortadora, campo): + for instrucción in self.Instrucciones: + instrucción.ejecutar(cortadora, campo) + + def __str__(self): + return "func{1}: {0}".format( + ' '.join(map(str, self.Instrucciones)), + self.Id if self.Id is not None else '') + + +class Llama: + def __init__(self, funcId=None): + self.FuncId = funcId + self.Funcs = None + + def ejecutar(self, cortadora, campo): + funcId = 0 if self.FuncId is None else self.FuncId + if len(self.Funcs) > funcId: + self.Funcs[funcId].ejecutar(cortadora, campo) + + def __str__(self): + return "llama-{}".format( + self.FuncId + if self.FuncId is not None + else 'func') + + +class Programa: + def __init__(self, genes): + temp = genes[:] + funcs = [] + + for índice in reversed(range(len(temp))): + if type(temp[índice]) is Repite: + principio = índice + 1 + fin = min(índice + temp[índice].NúmeroDeOperaciones + 1, + len(temp)) + temp[índice].Instrucciones = temp[principio:fin] + del temp[principio:fin] + continue + + if type(temp[índice]) is Llama: + temp[índice].Funcs = funcs + if type(temp[índice]) is Func: + if len(funcs) > 0 and not temp[índice].ExpectLlama: + temp[índice] = Llama() + temp[índice].Funcs = funcs + continue + principio = índice + 1 + fin = len(temp) + func = Func() + if temp[índice].ExpectLlama: + func.Id = len(funcs) + func.Instrucciones = [i for i in temp[principio:fin] + if type(i) is not Repite or + type(i) is Repite and len( + i.Instrucciones) > 0] + funcs.append(func) + del temp[índice:fin] + + for func in funcs: + for índice in reversed(range(len(func.Instrucciones))): + if type(func.Instrucciones[índice]) is Llama: + func_id = func.Instrucciones[índice].FuncId + if func_id is None: + continue + if func_id >= len(funcs) or \ + len(funcs[func_id].Instrucciones) == 0: + del func.Instrucciones[índice] + + for índice in reversed(range(len(temp))): + if type(temp[índice]) is Llama: + func_id = temp[índice].FuncId + if func_id is None: + continue + if func_id >= len(funcs) or \ + len(funcs[func_id].Instrucciones) == 0: + del temp[índice] + self.Principal = temp + self.Funcs = funcs + + def evaluar(self, cortadora, campo): + for i, instrucción in enumerate(self.Principal): + instrucción.ejecutar(cortadora, campo) + + def print(self): + if self.Funcs is not None: + for func in self.Funcs: + if func.Id is not None and len(func.Instrucciones) == 0: + continue + print(func) + print(' '.join(map(str, self.Principal))) + + +class Aptitud: + def __init__(self, totalCortada, instruccionesTotales, cuentaDePasos): + self.TotalCortada = totalCortada + self.InstruccionesTotales = instruccionesTotales + self.CuentaDePasos = cuentaDePasos + + def __gt__(self, otro): + if self.TotalCortada != otro.TotalCortada: + return self.TotalCortada > otro.TotalCortada + if self.CuentaDePasos != otro.CuentaDePasos: + return self.CuentaDePasos < otro.CuentaDePasos + return self.InstruccionesTotales < otro.InstruccionesTotales + + def __str__(self): + return "{} segados con {} instrucciones y {} pasos".format( + self.TotalCortada, self.InstruccionesTotales, self.CuentaDePasos) + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch16/circuitos.py b/es/ch16/circuitos.py new file mode 100644 index 0000000..de105b0 --- /dev/null +++ b/es/ch16/circuitos.py @@ -0,0 +1,100 @@ +# File: circuitos.py +# Del capítulo 16 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +class Not: + def __init__(self, entrada): + self._entrada = entrada + + def obtener_salida(self): + if self._entrada is None: + return None + valor = self._entrada.obtener_salida() + if valor is None: + return None + return not valor + + def __str__(self): + if self._entrada is None: + return "Not(?)" + return "Not({})".format(self._entrada) + + @staticmethod + def recuento_de_entradas(): + return 1 + + +class PuertaCon2Entradas: + def __init__(self, entradaA, entradaB, etiqueta, fnPrueba): + self._entradaA = entradaA + self._entradaB = entradaB + self._etiqueta = etiqueta + self._fnPrueba = fnPrueba + + def obtener_salida(self): + if self._entradaA is None or self._entradaB is None: + return None + aValor = self._entradaA.obtener_salida() + if aValor is None: + return None + bValor = self._entradaB.obtener_salida() + if bValor is None: + return None + return self._fnPrueba(aValor, bValor) + + def __str__(self): + if self._entradaA is None or self._entradaB is None: + return "{}(?)".format(self._etiqueta) + return "{}({} {})".format(self._etiqueta, self._entradaA, + self._entradaB) + + @staticmethod + def recuento_de_entradas(): + return 2 + + +class And(PuertaCon2Entradas): + def __init__(self, entradaA, entradaB): + super().__init__(entradaA, entradaB, type(self).__name__, + lambda a, b: a and b) + + +class Or(PuertaCon2Entradas): + def __init__(self, entradaA, entradaB): + super().__init__(entradaA, entradaB, type(self).__name__, + lambda a, b: a or b) + + +class Xor(PuertaCon2Entradas): + def __init__(self, entradaA, entradaB): + super().__init__(entradaA, entradaB, type(self).__name__, + lambda a, b: a != b) + + +class Fuente: + def __init__(self, fuenteId, contenedorDeFuentes): + self._fuenteId = fuenteId + self._contenedorDeFuentes = contenedorDeFuentes + + def obtener_salida(self): + return self._contenedorDeFuentes[self._fuenteId] + + def __str__(self): + return self._fuenteId + + @staticmethod + def recuento_de_entradas(): + return 0 diff --git a/es/ch16/genetic.py b/es/ch16/genetic.py new file mode 100644 index 0000000..b114b25 --- /dev/null +++ b/es/ch16/genetic.py @@ -0,0 +1,223 @@ +# File: genetic.py +# Del capítulo 16 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import random +import statistics +import sys +import time +from bisect import bisect_left +from enum import Enum +from math import exp + + +def _generar_padre(longitud, geneSet, obtener_aptitud): + genes = [] + while len(genes) < longitud: + tamañoMuestral = min(longitud - len(genes), len(geneSet)) + genes.extend(random.sample(geneSet, tamañoMuestral)) + aptitud = obtener_aptitud(genes) + return Cromosoma(genes, aptitud, Estrategias.Creación) + + +def _mudar(padre, geneSet, obtener_aptitud): + genesDelNiño = padre.Genes[:] + índice = random.randrange(0, len(padre.Genes)) + nuevoGen, alterno = random.sample(geneSet, 2) + genesDelNiño[índice] = alterno if nuevoGen == genesDelNiño[ + índice] else nuevoGen + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Mutación) + + +def _mudar_personalizada(padre, mutación_personalizada, obtener_aptitud): + genesDelNiño = padre.Genes[:] + mutación_personalizada(genesDelNiño) + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Mutación) + + +def _intercambiar(genesDePadre, índice, padres, obtener_aptitud, intercambiar, + mudar, generar_padre): + índiceDeDonante = random.randrange(0, len(padres)) + if índiceDeDonante == índice: + índiceDeDonante = (índiceDeDonante + 1) % len(padres) + genesDelNiño = intercambiar(genesDePadre, padres[índiceDeDonante].Genes) + if genesDelNiño is None: + # padre y donante son indistinguibles + padres[índiceDeDonante] = generar_padre() + return mudar(padres[índice]) + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Intercambio) + + +def obtener_mejor(obtener_aptitud, longitudObjetivo, aptitudÓptima, geneSet, + mostrar, mutación_personalizada=None, + creación_personalizada=None, edadMáxima=None, + tamañoDePiscina=1, intercambiar=None, segundosMáximos=None): + if mutación_personalizada is None: + def fnMudar(padre): + return _mudar(padre, geneSet, obtener_aptitud) + else: + def fnMudar(padre): + return _mudar_personalizada(padre, mutación_personalizada, + obtener_aptitud) + + if creación_personalizada is None: + def fnGenerarPadre(): + return _generar_padre(longitudObjetivo, geneSet, obtener_aptitud) + else: + def fnGenerarPadre(): + genes = creación_personalizada() + return Cromosoma(genes, obtener_aptitud(genes), + Estrategias.Creación) + + búsquedaDeEstrategia = { + Estrategias.Creación: lambda p, i, o: fnGenerarPadre(), + Estrategias.Mutación: lambda p, i, o: fnMudar(p), + Estrategias.Intercambio: lambda p, i, o: + _intercambiar(p.Genes, i, o, obtener_aptitud, intercambiar, fnMudar, + fnGenerarPadre) + } + + estrategiasUsadas = [búsquedaDeEstrategia[Estrategias.Mutación]] + if intercambiar is not None: + estrategiasUsadas.append(búsquedaDeEstrategia[Estrategias.Intercambio]) + + def fnNuevoNiño(padre, índice, padres): + return random.choice(estrategiasUsadas)(padre, índice, padres) + else: + def fnNuevoNiño(padre, índice, padres): + return fnMudar(padre) + + for caducado, mejora in _obtener_mejoras(fnNuevoNiño, fnGenerarPadre, + edadMáxima, tamañoDePiscina, + segundosMáximos): + if caducado: + return mejora + mostrar(mejora) + f = búsquedaDeEstrategia[mejora.Estrategia] + estrategiasUsadas.append(f) + if not aptitudÓptima > mejora.Aptitud: + return mejora + + +def _obtener_mejoras(nuevo_niño, generar_padre, edadMáxima, tamañoDePiscina, + segundosMáximos): + horaInicio = time.time() + mejorPadre = generar_padre() + yield segundosMáximos is not None and time.time() \ + - horaInicio > segundosMáximos, mejorPadre + padres = [mejorPadre] + aptitudesHistóricas = [mejorPadre.Aptitud] + for _ in range(tamañoDePiscina - 1): + padre = generar_padre() + if segundosMáximos is not None and time.time() - horaInicio > \ + segundosMáximos: + yield True, padre + if padre.Aptitud > mejorPadre.Aptitud: + yield False, padre + mejorPadre = padre + aptitudesHistóricas.append(padre.Aptitud) + padres.append(padre) + índiceDelÚltimoPadre = tamañoDePiscina - 1 + pÍndice = 1 + while True: + if segundosMáximos is not None and time.time() - horaInicio > \ + segundosMáximos: + yield True, mejorPadre + pÍndice = pÍndice - 1 if pÍndice > 0 else índiceDelÚltimoPadre + padre = padres[pÍndice] + niño = nuevo_niño(padre, pÍndice, padres) + if padre.Aptitud > niño.Aptitud: + if edadMáxima is None: + continue + padre.Edad += 1 + if edadMáxima > padre.Edad: + continue + índice = bisect_left(aptitudesHistóricas, niño.Aptitud, 0, + len(aptitudesHistóricas)) + diferencia = len(aptitudesHistóricas) - índice + proporciónSimilar = diferencia / len(aptitudesHistóricas) + if random.random() < exp(-proporciónSimilar): + padres[pÍndice] = niño + continue + padres[pÍndice] = mejorPadre + padre.Edad = 0 + continue + if not niño.Aptitud > padre.Aptitud: + # mismo aptitud + niño.Edad = padre.Edad + 1 + padres[pÍndice] = niño + continue + padres[pÍndice] = niño + padre.Edad = 0 + if niño.Aptitud > mejorPadre.Aptitud: + yield False, niño + mejorPadre = niño + aptitudesHistóricas.append(niño.Aptitud) + + +def ascenso_de_la_colina(funciónDeOptimización, es_mejora, es_óptimo, + obtener_valor_de_característica_siguiente, mostrar, + valorInicialDeCaracterística): + mejor = funciónDeOptimización(valorInicialDeCaracterística) + stdout = sys.stdout + sys.stdout = None + while not es_óptimo(mejor): + valorDeCaracterística = obtener_valor_de_característica_siguiente( + mejor) + niño = funciónDeOptimización(valorDeCaracterística) + if es_mejora(mejor, niño): + mejor = niño + sys.stdout = stdout + mostrar(mejor, valorDeCaracterística) + sys.stdout = None + sys.stdout = stdout + return mejor + + +class Cromosoma: + def __init__(self, genes, aptitud, estrategia): + self.Genes = genes + self.Aptitud = aptitud + self.Estrategia = estrategia + self.Edad = 0 + + +class Estrategias(Enum): + Creación = 0, + Mutación = 1, + Intercambio = 2 + + +class Comparar: + @staticmethod + def ejecutar(función): + cronometrajes = [] + stdout = sys.stdout + for i in range(100): + sys.stdout = None + horaInicio = time.time() + función() + segundos = time.time() - horaInicio + sys.stdout = stdout + cronometrajes.append(segundos) + promedio = statistics.mean(cronometrajes) + if i < 10 or i % 10 == 9: + print("{} {:3.2f} {:3.2f}".format( + 1 + i, promedio, + statistics.stdev(cronometrajes, promedio) if i > 1 else 0)) diff --git a/es/ch16/pruebas.py b/es/ch16/pruebas.py new file mode 100644 index 0000000..fd3540a --- /dev/null +++ b/es/ch16/pruebas.py @@ -0,0 +1,237 @@ +# File: pruebas.py +# Del capítulo 16 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import random +import unittest + +import circuitos +import genetic + + +def obtener_aptitud(genes, reglas, entradas): + circuito = nodos_a_circuito(genes)[0] + etiquetasDeFuente = "ABCD" + reglasExitosas = 0 + for regla in reglas: + entradas.clear() + entradas.update(zip(etiquetasDeFuente, regla[0])) + if circuito.obtener_salida() == regla[1]: + reglasExitosas += 1 + return reglasExitosas + + +def mostrar(candidato, horaInicio): + circuito = nodos_a_circuito(candidato.Genes)[0] + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + print("{}\t{}\t{}".format( + circuito, + candidato.Aptitud, + diferencia)) + + +def crear_gen(índice, puertas, fuentes): + if índice < len(fuentes): + tipoDePuerta = fuentes[índice] + else: + tipoDePuerta = random.choice(puertas) + índiceA = índiceB = None + if tipoDePuerta[1].recuento_de_entradas() > 0: + índiceA = random.randint(0, índice) + if tipoDePuerta[1].recuento_de_entradas() > 1: + índiceB = random.randint(0, índice) \ + if índice > 1 and índice >= len(fuentes) else 0 + if índiceB == índiceA: + índiceB = random.randint(0, índice) + return Nodo(tipoDePuerta[0], índiceA, índiceB) + + +def mudar(genesDelNiño, fnCrearGen, fnObtenerAptitud, fuenteCount): + cuenta = random.randint(1, 5) + aptitudInicial = fnObtenerAptitud(genesDelNiño) + while cuenta > 0: + cuenta -= 1 + índicesUsados = [i for i in nodos_a_circuito(genesDelNiño)[1] + if i >= fuenteCount] + if len(índicesUsados) == 0: + return + índice = random.choice(índicesUsados) + genesDelNiño[índice] = fnCrearGen(índice) + if fnObtenerAptitud(genesDelNiño) > aptitudInicial: + return + + +class PruebasDeCircuitos(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.entradas = dict() + cls.puertas = [[circuitos.And, circuitos.And], + [lambda i1, i2: circuitos.Not(i1), circuitos.Not]] + cls.fuentes = [ + [lambda i1, i2: circuitos.Fuente('A', cls.entradas), + circuitos.Fuente], + [lambda i1, i2: circuitos.Fuente('B', cls.entradas), + circuitos.Fuente]] + + def test_generar_OR(self): + reglas = [[[False, False], False], + [[False, True], True], + [[True, False], True], + [[True, True], True]] + + longitudÓptima = 6 + self.encontrar_circuito(reglas, longitudÓptima) + + def test_generar_XOR(self): + reglas = [[[False, False], False], + [[False, True], True], + [[True, False], True], + [[True, True], False]] + self.encontrar_circuito(reglas, 9) + + def test_generar_AxBxC(self): + reglas = [[[False, False, False], False], + [[False, False, True], True], + [[False, True, False], True], + [[False, True, True], False], + [[True, False, False], True], + [[True, False, True], False], + [[True, True, False], False], + [[True, True, True], True]] + self.fuentes.append( + [lambda l, r: circuitos.Fuente('C', self.entradas), + circuitos.Fuente]) + self.puertas.append([circuitos.Or, circuitos.Or]) + self.encontrar_circuito(reglas, 12) + + def obtener_reglas_para_sumador_de_2_bits_bit(self, bit): + reglas = [[[0, 0, 0, 0], [0, 0, 0]], # 0 + 0 = 0 + [[0, 0, 0, 1], [0, 0, 1]], # 0 + 1 = 1 + [[0, 0, 1, 0], [0, 1, 0]], # 0 + 2 = 2 + [[0, 0, 1, 1], [0, 1, 1]], # 0 + 3 = 3 + [[0, 1, 0, 0], [0, 0, 1]], # 1 + 0 = 1 + [[0, 1, 0, 1], [0, 1, 0]], # 1 + 1 = 2 + [[0, 1, 1, 0], [0, 1, 1]], # 1 + 2 = 3 + [[0, 1, 1, 1], [1, 0, 0]], # 1 + 3 = 4 + [[1, 0, 0, 0], [0, 1, 0]], # 2 + 0 = 2 + [[1, 0, 0, 1], [0, 1, 1]], # 2 + 1 = 3 + [[1, 0, 1, 0], [1, 0, 0]], # 2 + 2 = 4 + [[1, 0, 1, 1], [1, 0, 1]], # 2 + 3 = 5 + [[1, 1, 0, 0], [0, 1, 1]], # 3 + 0 = 3 + [[1, 1, 0, 1], [1, 0, 0]], # 3 + 1 = 4 + [[1, 1, 1, 0], [1, 0, 1]], # 3 + 2 = 5 + [[1, 1, 1, 1], [1, 1, 0]]] # 3 + 3 = 6 + reglasDeBitN = [[regla[0], regla[1][2 - bit]] for regla in reglas] + self.puertas.append([circuitos.Or, circuitos.Or]) + self.puertas.append([circuitos.Xor, circuitos.Xor]) + self.fuentes.append( + [lambda l, r: circuitos.Fuente('C', self.entradas), + circuitos.Fuente]) + self.fuentes.append( + [lambda l, r: circuitos.Fuente('D', self.entradas), + circuitos.Fuente]) + return reglasDeBitN + + def test_sumador_de_2_bits_bit_1(self): + reglas = self.obtener_reglas_para_sumador_de_2_bits_bit(0) + self.encontrar_circuito(reglas, 3) + + def test_sumador_de_2_bits_bit_2(self): + reglas = self.obtener_reglas_para_sumador_de_2_bits_bit(1) + self.encontrar_circuito(reglas, 7) + + def test_sumador_de_2_bits_bit_4s(self): + reglas = self.obtener_reglas_para_sumador_de_2_bits_bit(2) + self.encontrar_circuito(reglas, 9) + + def encontrar_circuito(self, reglas, longitudEsperada): + horaInicio = datetime.datetime.now() + + def fnMostrar(candidato, longitud=None): + if longitud is not None: + print("-- nodos distintos en el circuito:", + len(nodos_a_circuito(candidato.Genes)[1])) + mostrar(candidato, horaInicio) + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes, reglas, self.entradas) + + def fnCrearGen(índice): + return crear_gen(índice, self.puertas, self.fuentes) + + def fnMudar(genes): + mudar(genes, fnCrearGen, fnObtenerAptitud, len(self.fuentes)) + + longitudMáxima = 50 + + def fnCrear(): + return [fnCrearGen(i) for i in range(longitudMáxima)] + + def fnFunciónDeOptimización(longitudVariable): + nonlocal longitudMáxima + longitudMáxima = longitudVariable + return genetic.obtener_mejor( + fnObtenerAptitud, None, len(reglas), None, fnMostrar, + fnMudar, fnCrear, tamañoDePiscina=3, segundosMáximos=30) + + def fnEsUnaMejora(mejorActual, niño): + return niño.Aptitud == len(reglas) and \ + len(nodos_a_circuito(niño.Genes)[1]) < \ + len(nodos_a_circuito(mejorActual.Genes)[1]) + + def fnEsÓptimo(niño): + return niño.Aptitud == len(reglas) and \ + len(nodos_a_circuito(niño.Genes)[1]) <= longitudEsperada + + def fnObtenerValorDeCaracterísticaSiguiente(mejorActual): + return len(nodos_a_circuito(mejorActual.Genes)[1]) + + mejor = genetic.ascenso_de_la_colina( + fnFunciónDeOptimización, fnEsUnaMejora, fnEsÓptimo, + fnObtenerValorDeCaracterísticaSiguiente, fnMostrar, + longitudMáxima) + self.assertTrue(mejor.Aptitud == len(reglas)) + self.assertFalse(len(nodos_a_circuito(mejor.Genes)[1]) + > longitudEsperada) + + +def nodos_a_circuito(genes): + circuito = [] + índicesUsados = [] + for i, nodo in enumerate(genes): + usados = {i} + entradaA = entradaB = None + if nodo.ÍndiceA is not None and i > nodo.ÍndiceA: + entradaA = circuito[nodo.ÍndiceA] + usados.update(índicesUsados[nodo.ÍndiceA]) + if nodo.ÍndiceB is not None and i > nodo.ÍndiceB: + entradaB = circuito[nodo.ÍndiceB] + usados.update(índicesUsados[nodo.ÍndiceB]) + circuito.append(nodo.CrearPuerta(entradaA, entradaB)) + índicesUsados.append(usados) + return circuito[-1], índicesUsados[-1] + + +class Nodo: + def __init__(self, crearPuerta, índiceA=None, índiceB=None): + self.CrearPuerta = crearPuerta + self.ÍndiceA = índiceA + self.ÍndiceB = índiceB + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch17/regex.py b/es/ch17/regex.py new file mode 100644 index 0000000..481ea85 --- /dev/null +++ b/es/ch17/regex.py @@ -0,0 +1,391 @@ +# File: regex.py +# Del capítulo 17 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import random +import re +import unittest +from functools import partial + +import genetic + +metasDeRepetición = {'?', '*', '+', '{2}', '{2,}'} +metasDeInicio = {'|', '(', '['} +metasFinales = {')', ']'} +todosMetacaracteres = metasDeRepetición | metasDeInicio | metasFinales + +erroresEnRegexes = {} + + +def reparar_regex(genes): + resultado = [] + finales = [] + f = reparar_o_ignorar_metas_de_repetición + for símbolo in genes: + f = f(símbolo, resultado, finales) + if ']' in finales and resultado[-1] == '[': + del resultado[-1] + resultado.extend(reversed(finales)) + return ''.join(resultado) + + +def reparar_o_ignorar_metas_de_repetición(símbolo, resultado, finales): + if símbolo in metasDeRepetición or símbolo in metasFinales: + return reparar_o_ignorar_metas_de_repetición + if símbolo == '(': + finales.append(')') + resultado.append(símbolo) + if símbolo == '[': + finales.append(']') + return reparar_in_character_set + return manejar_metas_de_repetición_que_siguen_metas_de_repetición_o_inicio + + +def manejar_metas_de_repetición_que_siguen_metas_de_repetición_o_inicio( + símbolo, resultado, finales): + último = resultado[-1] + if símbolo not in metasDeRepetición: + if símbolo == '[': + resultado.append(símbolo) + finales.append(']') + return reparar_in_character_set + if símbolo == '(': + finales.append(')') + elif símbolo == ')': + coincidencia = ''.join(finales).rfind(')') + if coincidencia != -1: + del finales[coincidencia] + else: + resultado[0:0] = ['('] + resultado.append(símbolo) + elif último in metasDeInicio: + pass + elif símbolo == '?' and último == '?' and len(resultado) > 2 \ + and resultado[-2] in metasDeRepetición: + pass + elif último in metasDeRepetición: + pass + else: + resultado.append(símbolo) + return manejar_metas_de_repetición_que_siguen_metas_de_repetición_o_inicio + + +def reparar_in_character_set(símbolo, resultado, finales): + if símbolo == ']': + if resultado[-1] == '[': + del resultado[-1] + resultado.append(símbolo) + coincidencia = ''.join(finales).rfind(']') + if coincidencia != -1: + del finales[coincidencia] + return manejar_metas_de_repetición_que_siguen_metas_de_repetición_o_inicio + elif símbolo == '[': + pass + else: + resultado.append(símbolo) + return reparar_in_character_set + + +def obtener_aptitud(genes, deseadas, noDeseadas): + patrón = reparar_regex(genes) + longitud = len(patrón) + + try: + re.compile(patrón) + except re.error as e: + llave = str(e) + llave = llave[:llave.index("at position")] + info = [str(e), + "genes = ['{}']".format("', '".join(genes)), + "regex: " + patrón] + if llave not in erroresEnRegexes or len(info[1]) < len( + erroresEnRegexes[llave][1]): + erroresEnRegexes[llave] = info + return Aptitud(0, len(deseadas), len(noDeseadas), longitud) + + númeroDeDeseadosQueCoincidieron = sum( + 1 for i in deseadas if re.fullmatch(patrón, i)) + númeroDeNoDeseadosQueCoincidieron = sum( + 1 for i in noDeseadas if re.fullmatch(patrón, i)) + return Aptitud(númeroDeDeseadosQueCoincidieron, len(deseadas), + númeroDeNoDeseadosQueCoincidieron, longitud) + + +def mostrar(candidato, horaInicio): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + print("{}\t{}\t{}".format( + reparar_regex(candidato.Genes), candidato.Aptitud, diferencia)) + + +def mudar_añadir(genes, geneSet): + índice = random.randrange(0, len(genes) + 1) if len(genes) > 0 else 0 + genes[índice:índice] = [random.choice(geneSet)] + return True + + +def mudar_remover(genes): + if len(genes) < 1: + return False + del genes[random.randrange(0, len(genes))] + if len(genes) > 1 and random.randint(0, 1) == 1: + del genes[random.randrange(0, len(genes))] + return True + + +def mudar_reemplazar(genes, geneSet): + if len(genes) < 1: + return False + índice = random.randrange(0, len(genes)) + genes[índice] = random.choice(geneSet) + return True + + +def mudar_intercambiar(genes): + if len(genes) < 2: + return False + índiceA, índiceB = random.sample(range(len(genes)), 2) + genes[índiceA], genes[índiceB] = genes[índiceB], genes[índiceA] + return True + + +def mudar_mover(genes): + if len(genes) < 3: + return False + principio = random.choice(range(len(genes))) + fin = principio + random.randint(1, 2) + aMover = genes[principio:fin] + genes[principio:fin] = [] + índice = random.choice(range(len(genes))) + genes[índice:índice] = aMover + return True + + +def mudar_a_conjunto_de_caracteres(genes): + if len(genes) < 3: + return False + o = [i for i in range(1, len(genes) - 1) + if genes[i] == '|' and + genes[i - 1] not in todosMetacaracteres and + genes[i + 1] not in todosMetacaracteres] + if len(o) == 0: + return False + corta = [i for i in o + if sum(len(w) for w in genes[i - 1:i + 2:2]) > + len(set(c for w in genes[i - 1:i + 2:2] for c in w))] + if len(corta) == 0: + return False + índice = random.choice(o) + distinto = set(c for w in genes[índice - 1:índice + 2:2] for c in w) + secuencia = ['['] + [i for i in distinto] + [']'] + genes[índice - 1:índice + 2] = secuencia + return True + + +def mudar_a_conjunto_de_caracteres_izquierda(genes, deseadas): + if len(genes) < 4: + return False + o = [i for i in range(-1, len(genes) - 3) + if (i == -1 or genes[i] in metasDeInicio) and + len(genes[i + 1]) == 2 and + genes[i + 1] in deseadas and + (len(genes) == i + 1 or genes[i + 2] == '|' or + genes[i + 2] in metasFinales)] + if len(o) == 0: + return False + búsqueda = {} + for i in o: + búsqueda.setdefault(genes[i + 1][0], []).append(i) + mín2 = [i for i in búsqueda.values() if len(i) > 1] + if len(mín2) == 0: + return False + choice = random.choice(mín2) + caracteres = ['|', genes[choice[0] + 1][0], '['] + caracteres.extend([genes[i + 1][1] for i in choice]) + caracteres.append(']') + for i in reversed(choice): + if i >= 0: + genes[i:i + 2] = [] + genes.extend(caracteres) + return True + + +def mudar_add_deseadas(genes, deseadas): + índice = random.randrange(0, len(genes) + 1) if len(genes) > 0 else 0 + genes[índice:índice] = ['|'] + [random.choice(deseadas)] + return True + + +def mudar(genes, fnObtenerAptitud, operadoresDeMutación, recuentoDeMutaciones): + aptitudInicial = fnObtenerAptitud(genes) + cuenta = random.choice(recuentoDeMutaciones) + for i in range(1, cuenta + 2): + duplo = operadoresDeMutación[:] + func = random.choice(duplo) + while not func(genes): + duplo.remove(func) + func = random.choice(duplo) + if fnObtenerAptitud(genes) > aptitudInicial: + recuentoDeMutaciones.append(i) + return + + +class PruebasDeRegex(unittest.TestCase): + def test_dos_dígitos(self): + deseadas = {"01", "11", "10"} + noDeseadas = {"00", ""} + self.encontrar_regex(deseadas, noDeseadas, 7) + + def test_grupos(self): + deseadas = {"01", "0101", "010101"} + noDeseadas = {"0011", ""} + self.encontrar_regex(deseadas, noDeseadas, 5) + + def test_códigos_de_estado(self): + Aptitud.UseRegexLongitud = True + deseadas = {"NE", "NV", "NH", "NJ", "NM", "NY", "NC", "ND"} + noDeseadas = {"N" + l for l in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + if "N" + l not in deseadas} + operadoresPersonalizados = [ + partial(mudar_a_conjunto_de_caracteres_izquierda, + deseadas=deseadas), + ] + self.encontrar_regex(deseadas, noDeseadas, 11, + operadoresPersonalizados) + + def test_longitud_par(self): + deseadas = {"00", "01", "10", "11", "0000", "0001", "0010", "0011", + "0100", "0101", "0110", "0111", "1000", "1001", "1010", + "1011", "1100", "1101", "1110", "1111"} + noDeseadas = {"0", "1", "000", "001", "010", "011", "100", "101", + "110", "111", ""} + operadoresPersonalizados = [ + mudar_a_conjunto_de_caracteres, + ] + self.encontrar_regex(deseadas, noDeseadas, 10, + operadoresPersonalizados) + + def test_50_códigos_de_estado(self): + Aptitud.UseRegexLongitud = True + deseadas = {"AL", "AK", "AZ", "AR", "CA", + "CO", "CT", "DE", "FL", "GA", + "HI", "ID", "IL", "IN", "IA", + "KS", "KY", "LA", "ME", "MD", + "MA", "MI", "MN", "MS", "MO", + "MT", "NE", "NV", "NH", "NJ", + "NM", "NY", "NC", "ND", "OH", + "OK", "OR", "PA", "RI", "SC", + "SD", "TN", "TX", "UT", "VT", + "VA", "WA", "WV", "WI", "WY"} + noDeseadas = {a + b for a in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + for b in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + if a + b not in deseadas} | \ + set(i for i in "ABCDEFGHIJKLMNOPQRSTUVWXYZ") + operadoresPersonalizados = [ + partial(mudar_a_conjunto_de_caracteres_izquierda, + deseadas=deseadas), + mudar_a_conjunto_de_caracteres, + partial(mudar_add_deseadas, deseadas=[i for i in deseadas]), + ] + self.encontrar_regex(deseadas, noDeseadas, 120, + operadoresPersonalizados) + + def encontrar_regex(self, deseadas, noDeseadas, longitudEsperada, + operadoresPersonalizados=None): + horaInicio = datetime.datetime.now() + genesDeTexto = deseadas | set(c for w in deseadas for c in w) + geneSet = [i for i in todosMetacaracteres | genesDeTexto] + + def fnMostrar(candidato): + mostrar(candidato, horaInicio) + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes, deseadas, noDeseadas) + + recuentoDeMutaciones = [1] + + operadoresDeMutación = [ + partial(mudar_añadir, geneSet=geneSet), + partial(mudar_reemplazar, geneSet=geneSet), + mudar_remover, + mudar_intercambiar, + mudar_mover, + ] + if operadoresPersonalizados is not None: + operadoresDeMutación.extend(operadoresPersonalizados) + + def fnMudar(genes): + mudar(genes, fnObtenerAptitud, operadoresDeMutación, + recuentoDeMutaciones) + + aptitudÓptima = Aptitud(len(deseadas), len(deseadas), 0, + longitudEsperada) + + mejor = genetic.obtener_mejor( + fnObtenerAptitud, max(len(i) for i in genesDeTexto), + aptitudÓptima, geneSet, fnMostrar, fnMudar, tamañoDePiscina=10) + self.assertTrue(not aptitudÓptima > mejor.Aptitud) + + for info in erroresEnRegexes.values(): + print("") + print(info[0]) + print(info[1]) + print(info[2]) + + def test_comparativa(self): + genetic.Comparar.ejecutar(self.test_dos_dígitos) + + +class Aptitud: + UseRegexLongitud = False + + def __init__(self, númeroDeDeseadosQueCoincidieron, totalDeseado, + númeroDeNoDeseadosQueCoincidieron, longitud): + self.NúmeroDeDeseadosQueCoincidieron = númeroDeDeseadosQueCoincidieron + self._totalDeseado = totalDeseado + self.NúmeroDeNoDeseadosQueCoincidieron = \ + númeroDeNoDeseadosQueCoincidieron + self.Longitud = longitud + + def __gt__(self, otro): + conjunto = (self._totalDeseado - + self.NúmeroDeDeseadosQueCoincidieron) + \ + self.NúmeroDeNoDeseadosQueCoincidieron + otroConjunto = (self._totalDeseado - + otro.NúmeroDeDeseadosQueCoincidieron) + \ + otro.NúmeroDeNoDeseadosQueCoincidieron + if conjunto != otroConjunto: + return conjunto < otroConjunto + éxito = conjunto == 0 + otroÉxito = otroConjunto == 0 + if éxito != otroÉxito: + return éxito + if not éxito: + return self.Longitud <= otro.Longitud if \ + Aptitud.UseRegexLongitud else False + return self.Longitud < otro.Longitud + + def __str__(self): + return "coincide con {} deseadas y {} no deseadas, lon {}".format( + "todas" if self._totalDeseado == + self.NúmeroDeDeseadosQueCoincidieron else + self.NúmeroDeDeseadosQueCoincidieron, + self.NúmeroDeNoDeseadosQueCoincidieron, + self.Longitud) + + +if __name__ == '__main__': + unittest.main() diff --git a/es/ch18/genetic.py b/es/ch18/genetic.py new file mode 100644 index 0000000..dffb546 --- /dev/null +++ b/es/ch18/genetic.py @@ -0,0 +1,273 @@ +# File: genetic.py +# Del capítulo 18 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import random +import statistics +import sys +import time +from bisect import bisect_left +from enum import Enum +from enum import IntEnum +from math import exp + + +def _generar_padre(longitud, geneSet, obtener_aptitud): + genes = [] + while len(genes) < longitud: + tamañoMuestral = min(longitud - len(genes), len(geneSet)) + genes.extend(random.sample(geneSet, tamañoMuestral)) + aptitud = obtener_aptitud(genes) + return Cromosoma(genes, aptitud, Estrategias.Creación) + + +def _mudar(padre, geneSet, obtener_aptitud): + genesDelNiño = padre.Genes[:] + índice = random.randrange(0, len(padre.Genes)) + nuevoGen, alterno = random.sample(geneSet, 2) + genesDelNiño[índice] = alterno if nuevoGen == genesDelNiño[ + índice] else nuevoGen + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Mutación) + + +def _mudar_personalizada(padre, mutación_personalizada, obtener_aptitud): + genesDelNiño = padre.Genes[:] + mutación_personalizada(genesDelNiño) + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Mutación) + + +def _intercambiar(genesDePadre, índice, padres, obtener_aptitud, intercambiar, + mudar, generar_padre): + índiceDeDonante = random.randrange(0, len(padres)) + if índiceDeDonante == índice: + índiceDeDonante = (índiceDeDonante + 1) % len(padres) + genesDelNiño = intercambiar(genesDePadre, padres[índiceDeDonante].Genes) + if genesDelNiño is None: + # padre y donante son indistinguibles + padres[índiceDeDonante] = generar_padre() + return mudar(padres[índice]) + aptitud = obtener_aptitud(genesDelNiño) + return Cromosoma(genesDelNiño, aptitud, Estrategias.Intercambio) + + +def obtener_mejor(obtener_aptitud, longitudObjetivo, aptitudÓptima, geneSet, + mostrar, mutación_personalizada=None, + creación_personalizada=None, edadMáxima=None, + tamañoDePiscina=1, intercambiar=None, segundosMáximos=None): + if mutación_personalizada is None: + def fnMudar(padre): + return _mudar(padre, geneSet, obtener_aptitud) + else: + def fnMudar(padre): + return _mudar_personalizada(padre, mutación_personalizada, + obtener_aptitud) + + if creación_personalizada is None: + def fnGenerarPadre(): + return _generar_padre(longitudObjetivo, geneSet, obtener_aptitud) + else: + def fnGenerarPadre(): + genes = creación_personalizada() + return Cromosoma(genes, obtener_aptitud(genes), + Estrategias.Creación) + + búsquedaDeEstrategia = { + Estrategias.Creación: lambda p, i, o: fnGenerarPadre(), + Estrategias.Mutación: lambda p, i, o: fnMudar(p), + Estrategias.Intercambio: lambda p, i, o: + _intercambiar(p.Genes, i, o, obtener_aptitud, intercambiar, fnMudar, + fnGenerarPadre) + } + + estrategiasUsadas = [búsquedaDeEstrategia[Estrategias.Mutación]] + if intercambiar is not None: + estrategiasUsadas.append(búsquedaDeEstrategia[Estrategias.Intercambio]) + + def fnNuevoNiño(padre, índice, padres): + return random.choice(estrategiasUsadas)(padre, índice, padres) + else: + def fnNuevoNiño(padre, índice, padres): + return fnMudar(padre) + + for caducado, mejora in _obtener_mejoras( + fnNuevoNiño, fnGenerarPadre, edadMáxima, tamañoDePiscina, + segundosMáximos): + if caducado: + return mejora + mostrar(mejora) + f = búsquedaDeEstrategia[mejora.Estrategia] + estrategiasUsadas.append(f) + if not aptitudÓptima > mejora.Aptitud: + return mejora + + +def _obtener_mejoras(nuevo_niño, generar_padre, edadMáxima, tamañoDePiscina, + segundosMáximos): + horaInicio = time.time() + mejorPadre = generar_padre() + yield segundosMáximos is not None and time.time() - \ + horaInicio > segundosMáximos, mejorPadre + padres = [mejorPadre] + aptitudesHistóricas = [mejorPadre.Aptitud] + for _ in range(tamañoDePiscina - 1): + padre = generar_padre() + if segundosMáximos is not None and time.time() - horaInicio > \ + segundosMáximos: + yield True, padre + if padre.Aptitud > mejorPadre.Aptitud: + yield False, padre + mejorPadre = padre + aptitudesHistóricas.append(padre.Aptitud) + padres.append(padre) + índiceDelÚltimoPadre = tamañoDePiscina - 1 + pÍndice = 1 + while True: + if segundosMáximos is not None and time.time() - horaInicio > \ + segundosMáximos: + yield True, mejorPadre + pÍndice = pÍndice - 1 if pÍndice > 0 else índiceDelÚltimoPadre + padre = padres[pÍndice] + niño = nuevo_niño(padre, pÍndice, padres) + if padre.Aptitud > niño.Aptitud: + if edadMáxima is None: + continue + padre.Edad += 1 + if edadMáxima > padre.Edad: + continue + índice = bisect_left(aptitudesHistóricas, niño.Aptitud, 0, + len(aptitudesHistóricas)) + diferencia = len(aptitudesHistóricas) - índice + proporciónSimilar = diferencia / len(aptitudesHistóricas) + if random.random() < exp(-proporciónSimilar): + padres[pÍndice] = niño + continue + padres[pÍndice] = mejorPadre + padre.Edad = 0 + continue + if not niño.Aptitud > padre.Aptitud: + # mismo aptitud + niño.Edad = padre.Edad + 1 + padres[pÍndice] = niño + continue + padres[pÍndice] = niño + padre.Edad = 0 + if niño.Aptitud > mejorPadre.Aptitud: + yield False, niño + mejorPadre = niño + aptitudesHistóricas.append(niño.Aptitud) + + +def ascenso_de_la_colina(funciónDeOptimización, es_mejora, es_óptimo, + obtener_valor_de_característica_siguiente, mostrar, + valorInicialDeCaracterística): + mejor = funciónDeOptimización(valorInicialDeCaracterística) + stdout = sys.stdout + sys.stdout = None + while not es_óptimo(mejor): + valorDeCaracterística = obtener_valor_de_característica_siguiente( + mejor) + niño = funciónDeOptimización(valorDeCaracterística) + if es_mejora(mejor, niño): + mejor = niño + sys.stdout = stdout + mostrar(mejor, valorDeCaracterística) + sys.stdout = None + sys.stdout = stdout + return mejor + + +def torneo(generar_padre, intercambiar, competir, mostrar, clave_de_orden, + númeroDePadres=10, generaciones_máximas=100): + piscina = [[generar_padre(), [0, 0, 0]] for _ in + range(1 + númeroDePadres * númeroDePadres)] + mejor, mejorPuntuación = piscina[0] + + def obtenerClaveDeOrden(x): + return clave_de_orden(x[0], x[1][ResultadoDeCompetición.Ganado], + x[1][ResultadoDeCompetición.Empatado], + x[1][ResultadoDeCompetición.Perdido]) + + generación = 0 + while generación < generaciones_máximas: + generación += 1 + for i in range(0, len(piscina)): + for j in range(0, len(piscina)): + if i == j: + continue + jugadorA, puntuaciónA = piscina[i] + jugadorB, puntuaciónB = piscina[j] + resultado = competir(jugadorA, jugadorB) + puntuaciónA[resultado] += 1 + puntuaciónB[2 - resultado] += 1 + + piscina.sort(key=obtenerClaveDeOrden, reverse=True) + if obtenerClaveDeOrden(piscina[0]) > obtenerClaveDeOrden( + [mejor, mejorPuntuación]): + mejor, mejorPuntuación = piscina[0] + mostrar(mejor, mejorPuntuación[ResultadoDeCompetición.Ganado], + mejorPuntuación[ResultadoDeCompetición.Empatado], + mejorPuntuación[ResultadoDeCompetición.Perdido], + generación) + + padres = [piscina[i][0] for i in range(númeroDePadres)] + piscina = [[intercambiar(padres[i], padres[j]), [0, 0, 0]] + for i in range(len(padres)) + for j in range(len(padres)) + if i != j] + piscina.extend([padre, [0, 0, 0]] for padre in padres) + piscina.append([generar_padre(), [0, 0, 0]]) + return mejor + + +class ResultadoDeCompetición(IntEnum): + Perdido = 0, + Empatado = 1, + Ganado = 2, + + +class Cromosoma: + def __init__(self, genes, aptitud, estrategia): + self.Genes = genes + self.Aptitud = aptitud + self.Estrategia = estrategia + self.Edad = 0 + + +class Estrategias(Enum): + Creación = 0, + Mutación = 1, + Intercambio = 2 + + +class Comparar: + @staticmethod + def ejecutar(función): + cronometrajes = [] + stdout = sys.stdout + for i in range(100): + sys.stdout = None + horaInicio = time.time() + función() + segundos = time.time() - horaInicio + sys.stdout = stdout + cronometrajes.append(segundos) + promedio = statistics.mean(cronometrajes) + if i < 10 or i % 10 == 9: + print("{} {:3.2f} {:3.2f}".format( + 1 + i, promedio, + statistics.stdev(cronometrajes, promedio) if i > 1 else 0)) diff --git a/es/ch18/ticTacToe.py b/es/ch18/ticTacToe.py new file mode 100644 index 0000000..1c9e36b --- /dev/null +++ b/es/ch18/ticTacToe.py @@ -0,0 +1,736 @@ +# File: ticTacToe.py +# Del capítulo 18 de _Algoritmos Genéticos con Python_ +# +# Author: Clinton Sheppard +# Copyright (c) 2017 Clinton Sheppard +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import datetime +import random +import unittest +from functools import partial + +import genetic + + +def obtener_aptitud(genes): + copiaLocal = genes[:] + aptitud = obtener_aptitud_para_juegos(copiaLocal) + aptitud.ConteoDeGenes = len(genes) + return aptitud + + +índicesDeCuadrados = [1, 2, 3, 4, 5, 6, 7, 8, 9] + + +def jugar1en1(xGenes, oGenes): + tablero = dict((i, Cuadrado(i, TipoDeContenido.Vacío)) for i in range(1, 9 + 1)) + vacíos = [v for v in tablero.values() if v.Contenido == TipoDeContenido.Vacío] + datosDeRonda = [[xGenes, TipoDeContenido.Mia, genetic.ResultadoDeCompetición.Perdido, + genetic.ResultadoDeCompetición.Ganado], + [oGenes, TipoDeContenido.Oponente, genetic.ResultadoDeCompetición.Ganado, + genetic.ResultadoDeCompetición.Perdido]] + índiceDelJugador = 0 + + while len(vacíos) > 0: + datosDelJugador = datosDeRonda[índiceDelJugador] + índiceDelJugador = 1 - índiceDelJugador + genes, pieza, perdió, ganó = datosDelJugador + + índiceDeReglaYMovimiento = obtener_mover(genes, tablero, vacíos) + if índiceDeReglaYMovimiento is None: # no pudo encontrar un movimiento + return perdió + + índice = índiceDeReglaYMovimiento[0] + tablero[índice] = Cuadrado(índice, pieza) + + sóloElMovimientoMásReciente = [tablero[índice]] + if len(FiltroDeContenidoDeFila(pieza, 3).obtener_coincidencias(tablero, sóloElMovimientoMásReciente)) > 0 or \ + len(FiltroDeContenidoDeColumna(pieza, 3).obtener_coincidencias(tablero, sóloElMovimientoMásReciente)) > 0 or \ + len(DiagonalContenidoFilter(pieza, 3).obtener_coincidencias(tablero, sóloElMovimientoMásReciente)) > 0: + return ganó + vacíos = [v for v in tablero.values() if v.Contenido == TipoDeContenido.Vacío] + return genetic.ResultadoDeCompetición.Empatado + + +def obtener_aptitud_para_juegos(genes): + def obtenerCadenaDelTablero(b): + return ''.join(map(lambda i: + '.' if b[i].Contenido == TipoDeContenido.Vacío + else 'x' if b[i].Contenido == TipoDeContenido.Mia + else 'o', índicesDeCuadrados)) + + tablero = dict((i, Cuadrado(i, TipoDeContenido.Vacío)) for i in range(1, 9 + 1)) + + cola = [tablero] + for cuadrado in tablero.values(): + copiaDelCandidato = tablero.copy() + copiaDelCandidato[cuadrado.Índice] = Cuadrado(cuadrado.Índice, TipoDeContenido.Oponente) + cola.append(copiaDelCandidato) + + reglasGanadoras = {} + ganados = empates = perdidos = 0 + + while len(cola) > 0: + tablero = cola.pop() + cadenaDelTablero = obtenerCadenaDelTablero(tablero) + vacíos = [v for v in tablero.values() if v.Contenido == TipoDeContenido.Vacío] + + if len(vacíos) == 0: + empates += 1 + continue + + candidatoÍndiceAndReglaÍndice = obtener_mover(genes, tablero, vacíos) + + if candidatoÍndiceAndReglaÍndice is None: # no pudo encontrar un movimiento + # hay vacíos pero no encontró un movimiento + perdidos += 1 + # ir al siguiente tablero + continue + + # encontró al menos un movimiento + índice = candidatoÍndiceAndReglaÍndice[0] + tablero[índice] = Cuadrado(índice, TipoDeContenido.Mia) + # newTableroString = obtenerCadenaDelTablero(tablero) + + # Si ahora tenemos tres de mis piezas en cualquier fila, columna o diagonal, ganamos + sóloElMovimientoMásReciente = [tablero[índice]] + if len(tengoTresEnUnaFila.obtener_coincidencias(tablero, sóloElMovimientoMásReciente)) > 0 or \ + len(tengoTresEnUnaColumna.obtener_coincidencias(tablero, sóloElMovimientoMásReciente)) > 0 or \ + len(tengoTresEnDiagonal.obtener_coincidencias(tablero, sóloElMovimientoMásReciente)) > 0: + reglaId = candidatoÍndiceAndReglaÍndice[1] + if reglaId not in reglasGanadoras: + reglasGanadoras[reglaId] = list() + reglasGanadoras[reglaId].append(cadenaDelTablero) + ganados += 1 + # ir al siguiente tablero + continue + + # perdemos si vacíos tienen dos piezas opositoras en una fila, columna o diagonal + vacíos = [v for v in tablero.values() if v.Contenido == TipoDeContenido.Vacío] + if len(oponenteTieneDosEnUnaFila.obtener_coincidencias(tablero, vacíos)) > 0: + perdidos += 1 + # ir al siguiente tablero + continue + + # poner en cola todas las posibles respuestas de los oponentes + for cuadrado in vacíos: + copiaDelCandidato = tablero.copy() + copiaDelCandidato[cuadrado.Índice] = Cuadrado(cuadrado.Índice, + TipoDeContenido.Oponente) + cola.append(copiaDelCandidato) + + return Aptitud(ganados, empates, perdidos, len(genes)) + + +def obtener_mover(reglaSet, tablero, vacíos, índiceDePrimeraRegla=0): + copiaDeReglas = reglaSet[:] + + for reglaÍndice in range(índiceDePrimeraRegla, len(copiaDeReglas)): + gene = copiaDeReglas[reglaÍndice] + coincidencias = gene.obtener_coincidencias(tablero, vacíos) + if len(coincidencias) == 0: + continue + if len(coincidencias) == 1: + return [list(coincidencias)[0], reglaÍndice] + if len(vacíos) > len(coincidencias): + vacíos = [e for e in vacíos if e.Índice in coincidencias] + + return None + + +def mostrar(candidato, horaInicio): + diferencia = (datetime.datetime.now() - horaInicio).total_seconds() + copiaLocal = candidato.Genes[:] + for i in reversed(range(len(copiaLocal))): + copiaLocal[i] = str(copiaLocal[i]) + + print("\t{}\n{}\n{}".format( + '\n\t'.join([d for d in copiaLocal]), + candidato.Aptitud, + diferencia)) + + +def mudar_añadir(genes, geneSet): + índice = random.randrange(0, len(genes) + 1) if len(genes) > 0 else 0 + genes[índice:índice] = [random.choice(geneSet)] + return True + + +def mudar_remover(genes): + if len(genes) < 1: + return False + del genes[random.randrange(0, len(genes))] + if len(genes) > 1 and random.randint(0, 1) == 1: + del genes[random.randrange(0, len(genes))] + return True + + +def mudar_reemplazar(genes, geneSet): + if len(genes) < 1: + return False + índice = random.randrange(0, len(genes)) + genes[índice] = random.choice(geneSet) + return True + + +def mudar_intercambiar_adyacente(genes): + if len(genes) < 2: + return False + índice = random.choice(range(len(genes) - 1)) + genes[índice], genes[índice + 1] = genes[índice + 1], genes[índice] + return True + + +def mudar_mover(genes): + if len(genes) < 3: + return False + principio = random.choice(range(len(genes))) + fin = principio + random.randint(1, 2) + aMover = genes[principio:fin] + genes[principio:fin] = [] + índice = random.choice(range(len(genes))) + genes[índice:índice] = aMover + return True + + +def mudar(genes, fnObtenerAptitud, operadoresDeMutación, recuentoDeMutaciones): + aptitudInicial = fnObtenerAptitud(genes) + cuenta = random.choice(recuentoDeMutaciones) + for i in range(1, cuenta + 2): + duplo = operadoresDeMutación[:] + func = random.choice(duplo) + while not func(genes): + duplo.remove(func) + func = random.choice(duplo) + if fnObtenerAptitud(genes) > aptitudInicial: + recuentoDeMutaciones.append(i) + return + + +def crear_geneSet(): + opciones = [[TipoDeContenido.Oponente, [0, 1, 2]], + [TipoDeContenido.Mia, [0, 1, 2]]] + geneSet = [ + ReglaMetadatos(FiltroDeContenidoDeFila, opciones), + ReglaMetadatos(lambda contenidoEsperado, cuenta: FiltroDeFilaSuperior(), opciones), + ReglaMetadatos(lambda contenidoEsperado, cuenta: FiltroDeFilaDelMedio(), + opciones), + ReglaMetadatos(lambda contenidoEsperado, cuenta: FiltroDeFilaInferior(), + opciones), + ReglaMetadatos(FiltroDeContenidoDeColumna, opciones), + ReglaMetadatos(lambda contenidoEsperado, cuenta: FiltroDeColumnaIzquierda(), + opciones), + ReglaMetadatos(lambda contenidoEsperado, cuenta: FiltroDeColumnaMedia(), + opciones), + ReglaMetadatos(lambda contenidoEsperado, cuenta: FiltroDeColumnaDerecha(), + opciones), + ReglaMetadatos(DiagonalContenidoFilter, opciones), + ReglaMetadatos(lambda contenidoEsperado, cuenta: FiltroDeUbicaciónDiagonal(), + opciones), + ReglaMetadatos(lambda contenidoEsperado, cuenta: FiltroDeEsquina()), + ReglaMetadatos(lambda contenidoEsperado, cuenta: FiltroDeLado()), + ReglaMetadatos(lambda contenidoEsperado, cuenta: FiltroCentral()), + ReglaMetadatos(lambda contenidoEsperado, cuenta: + FiltroDeOpuestosDeFila(contenidoEsperado), opciones, + necesitaContenidoEspecífico=True), + ReglaMetadatos(lambda contenidoEsperado, cuenta: FiltroDeOpuestosDeColumna( + contenidoEsperado), opciones, necesitaContenidoEspecífico=True), + ReglaMetadatos(lambda contenidoEsperado, cuenta: FiltroDeOpuestosDeDiagonal( + contenidoEsperado), opciones, necesitaContenidoEspecífico=True), + ] + + genes = list() + for gene in geneSet: + genes.extend(gene.crear_reglas()) + + print("creado " + str(len(genes)) + " genes") + return genes + + +class TicTacToeTests(unittest.TestCase): + def test_conocimiento_perfecto(self): + mínGenes = 10 + máxGenes = 20 + geneSet = crear_geneSet() + horaInicio = datetime.datetime.now() + + def fnMostrar(candidato): + mostrar(candidato, horaInicio) + + def fnObtenerAptitud(genes): + return obtener_aptitud(genes) + + recuentoDeMutaciones = [1] + + operadoresDeMutación = [ + partial(mudar_añadir, geneSet=geneSet), + partial(mudar_reemplazar, geneSet=geneSet), + mudar_remover, + mudar_intercambiar_adyacente, + mudar_mover, + ] + + def fnMudar(genes): + mudar(genes, fnObtenerAptitud, operadoresDeMutación, recuentoDeMutaciones) + + def fnIntercambio(padre, donante): + niño = padre[0:int(len(padre) / 2)] + \ + donante[int(len(donante) / 2):] + fnMudar(niño) + return niño + + def fnCrear(): + return random.sample(geneSet, random.randrange(mínGenes, máxGenes)) + + aptitudÓptima = Aptitud(620, 120, 0, 11) + mejor = genetic.obtener_mejor(fnObtenerAptitud, mínGenes, aptitudÓptima, None, + fnMostrar, fnMudar, fnCrear, edadMáxima=500, + tamañoDePiscina=20, intercambiar=fnIntercambio) + self.assertTrue(not aptitudÓptima > mejor.Aptitud) + + def test_tornament(self): + mínGenes = 10 + máxGenes = 20 + geneSet = crear_geneSet() + horaInicio = datetime.datetime.now() + + def fnMostrar(genes, ganados, empates, perdidos, generación): + print("-- generación {} --".format(generación)) + mostrar(genetic.Cromosoma(genes, + Aptitud(ganados, empates, perdidos, len(genes)), + None), horaInicio) + + recuentoDeMutaciones = [1] + + operadoresDeMutación = [ + partial(mudar_añadir, geneSet=geneSet), + partial(mudar_reemplazar, geneSet=geneSet), + mudar_remover, + mudar_intercambiar_adyacente, + mudar_mover, + ] + + def fnMudar(genes): + mudar(genes, lambda x: 0, operadoresDeMutación, recuentoDeMutaciones) + + def fnIntercambio(padre, donante): + niño = padre[0:int(len(padre) / 2)] + \ + donante[int(len(donante) / 2):] + fnMudar(niño) + return niño + + def fnCrear(): + return random.sample(geneSet, random.randrange(mínGenes, máxGenes)) + + def fnClaveDeOrden(genes, ganados, empates, perdidos): + return -1000 * perdidos - empates + 1 / len(genes) + + genetic.torneo(fnCrear, fnIntercambio, jugar1en1, fnMostrar, + fnClaveDeOrden, 13) + + +class TipoDeContenido: + Vacío = 'VACÍO' + Mia = 'MIA' + Oponente = 'OPONENTE' + + +class Cuadrado: + def __init__(self, índice, contenido=TipoDeContenido.Vacío): + self.Contenido = contenido + self.Índice = índice + self.Diagonales = [] + # diseño del tablero es + # 1 2 3 + # 4 5 6 + # 7 8 9 + self.EsCentro = False + self.EsEsquina = False + self.EsLado = False + self.EsFilaSuperior = False + self.EsFilaDelMedio = False + self.EsFilaInferior = False + self.EsColumnaIzquierda = False + self.EsColumnaEnMedio = False + self.EsColumnaDerecha = False + self.Fila = None + self.Columna = None + self.OpuestoDeDiagonal = None + self.OpuestoDeFila = None + self.OpuestoDeColumna = None + + if índice == 1 or índice == 2 or índice == 3: + self.EsFilaSuperior = True + self.Fila = [1, 2, 3] + elif índice == 4 or índice == 5 or índice == 6: + self.EsFilaDelMedio = True + self.Fila = [4, 5, 6] + elif índice == 7 or índice == 8 or índice == 9: + self.EsFilaInferior = True + self.Fila = [7, 8, 9] + + if índice % 3 == 1: + self.Columna = [1, 4, 7] + self.EsColumnaIzquierda = True + elif índice % 3 == 2: + self.Columna = [2, 5, 8] + self.EsColumnaEnMedio = True + elif índice % 3 == 0: + self.Columna = [3, 6, 9] + self.EsColumnaDerecha = True + + if índice == 5: + self.EsCentro = True + else: + if índice == 1 or índice == 3 or índice == 7 or índice == 9: + self.EsEsquina = True + elif índice == 2 or índice == 4 or índice == 6 or índice == 8: + self.EsLado = True + + if índice == 1: + self.OpuestoDeFila = 3 + self.OpuestoDeColumna = 7 + self.OpuestoDeDiagonal = 9 + elif índice == 2: + self.OpuestoDeColumna = 8 + elif índice == 3: + self.OpuestoDeFila = 1 + self.OpuestoDeColumna = 9 + self.OpuestoDeDiagonal = 7 + elif índice == 4: + self.OpuestoDeFila = 6 + elif índice == 6: + self.OpuestoDeFila = 4 + elif índice == 7: + self.OpuestoDeFila = 9 + self.OpuestoDeColumna = 1 + self.OpuestoDeDiagonal = 3 + elif índice == 8: + self.OpuestoDeColumna = 2 + else: # índice == 9 + self.OpuestoDeFila = 7 + self.OpuestoDeColumna = 3 + self.OpuestoDeDiagonal = 1 + + if índice == 1 or self.OpuestoDeDiagonal == 1 or self.EsCentro: + self.Diagonales.append([1, 5, 9]) + if índice == 3 or self.OpuestoDeDiagonal == 3 or self.EsCentro: + self.Diagonales.append([7, 5, 3]) + + +class Regla: + def __init__(self, prefijoDeDescripción, contenidoEsperado=None, cuenta=None): + self.PrefijoDeDescripción = prefijoDeDescripción + self.ContenidoEsperado = contenidoEsperado + self.Cuenta = cuenta + + def __str__(self): + resultado = self.PrefijoDeDescripción + " " + if self.Cuenta is not None: + resultado += str(self.Cuenta) + " " + if self.ContenidoEsperado is not None: + resultado += self.ContenidoEsperado + " " + return resultado + + +class ReglaMetadatos: + def __init__(self, crear, opciones=None, necesitaContenidoEspecífico=True, + necesitaCuentaEspecífica=True): + if opciones is None: + necesitaContenidoEspecífico = False + necesitaCuentaEspecífica = False + if necesitaCuentaEspecífica and not necesitaContenidoEspecífico: + raise ValueError('necesitaCuentaEspecífica solo es válida si necesitaContenidoEspecífico es verdadera') + self.crear = crear + self.opciones = opciones + self.necesitaContenidoEspecífico = necesitaContenidoEspecífico + self.necesitaCuentaEspecífica = necesitaCuentaEspecífica + + def crear_reglas(self): + opción = None + cuenta = None + + visto = set() + if self.necesitaContenidoEspecífico: + reglas = list() + + for opciónInfo in self.opciones: + opción = opciónInfo[0] + if self.necesitaCuentaEspecífica: + cuentaDeOpciones = opciónInfo[1] + + for cuenta in cuentaDeOpciones: + gene = self.crear(opción, cuenta) + if str(gene) not in visto: + visto.add(str(gene)) + reglas.append(gene) + else: + gene = self.crear(opción, None) + if str(gene) not in visto: + visto.add(str(gene)) + reglas.append(gene) + return reglas + else: + return [self.crear(opción, cuenta)] + + +class ContenidoFilter(Regla): + def __init__(self, descripción, contenidoEsperado, cuentaEsperada, + obtenerValorDelCuadrado): + super().__init__(descripción, contenidoEsperado, cuentaEsperada) + self.obtenerValorDelCuadrado = obtenerValorDelCuadrado + + def obtener_coincidencias(self, tablero, cuadrados): + resultado = set() + for cuadrado in cuadrados: + m = list(map(lambda i: tablero[i].Contenido, + self.obtenerValorDelCuadrado(cuadrado))) + if m.count(self.ContenidoEsperado) == self.Cuenta: + resultado.add(cuadrado.Índice) + return resultado + + +class FiltroDeContenidoDeFila(ContenidoFilter): + def __init__(self, contenidoEsperado, cuentaEsperada): + super().__init__("su FILA tiene", contenidoEsperado, cuentaEsperada, + lambda s: s.Fila) + + +class FiltroDeContenidoDeColumna(ContenidoFilter): + def __init__(self, contenidoEsperado, cuentaEsperada): + super().__init__("su COLUMNA tiene", contenidoEsperado, cuentaEsperada, + lambda s: s.Columna) + + +class FiltroDeUbicación(Regla): + def __init__(self, ubicaciónEsperada, descripciónDelContenedor, func): + super().__init__( + "es en " + descripciónDelContenedor + (" " if len(descripciónDelContenedor) > 0 else "") + ubicaciónEsperada) + self.func = func + + def obtener_coincidencias(self, tablero, cuadrados): + resultado = set() + for cuadrado in cuadrados: + if self.func(cuadrado): + resultado.add(cuadrado.Índice) + return resultado + + +class RowFiltroDeUbicación(FiltroDeUbicación): + def __init__(self, ubicaciónEsperada, func): + super().__init__(ubicaciónEsperada, "FILA", func) + + +class ColumnFiltroDeUbicación(FiltroDeUbicación): + def __init__(self, ubicaciónEsperada, func): + super().__init__(ubicaciónEsperada, "COLUMNA", func) + + +class FiltroDeFilaSuperior(RowFiltroDeUbicación): + def __init__(self): + super().__init__("SUPERIOR", lambda cuadrado: cuadrado.EsFilaSuperior) + + +class FiltroDeFilaDelMedio(RowFiltroDeUbicación): + def __init__(self): + super().__init__("MEDIO", lambda cuadrado: cuadrado.EsFilaDelMedio) + + +class FiltroDeFilaInferior(RowFiltroDeUbicación): + def __init__(self): + super().__init__("INFERIOR", lambda cuadrado: cuadrado.EsFilaInferior) + + +class FiltroDeColumnaIzquierda(ColumnFiltroDeUbicación): + def __init__(self): + super().__init__("IZQUIERDA", lambda cuadrado: cuadrado.EsColumnaIzquierda) + + +class FiltroDeColumnaMedia(ColumnFiltroDeUbicación): + def __init__(self): + super().__init__("MEDIO", lambda cuadrado: cuadrado.EsColumnaEnMedio) + + +class FiltroDeColumnaDerecha(ColumnFiltroDeUbicación): + def __init__(self): + super().__init__("DERECHO", lambda cuadrado: cuadrado.EsColumnaDerecha) + + +class FiltroDeUbicaciónDiagonal(FiltroDeUbicación): + def __init__(self): + super().__init__("DIAGONAL", "", + lambda cuadrado: not (cuadrado.EsFilaDelMedio or + cuadrado.EsColumnaEnMedio) or + cuadrado.EsCentro) + + +class DiagonalContenidoFilter(Regla): + def __init__(self, contenidoEsperado, cuenta): + super().__init__("su DIAGONAL tiene", contenidoEsperado, cuenta) + + def obtener_coincidencias(self, tablero, cuadrados): + resultado = set() + for cuadrado in cuadrados: + for diagonal in cuadrado.Diagonales: + m = list(map(lambda i: tablero[i].Contenido, diagonal)) + if m.count(self.ContenidoEsperado) == self.Cuenta: + resultado.add(cuadrado.Índice) + break + return resultado + + +class FiltroDeVictorias(Regla): + def __init__(self, contenido): + super().__init__("GANAR" if contenido == TipoDeContenido + .Mia else "bloquear OPONENTE de GANAR") + self.reglaDeFila = FiltroDeContenidoDeFila(contenido, 2) + self.reglaDeColumna = FiltroDeContenidoDeColumna(contenido, 2) + self.reglaDeDiagonal = DiagonalContenidoFilter(contenido, 2) + + def obtener_coincidencias(self, tablero, cuadrados): + enDiagonal = self.reglaDeDiagonal.obtener_coincidencias(tablero, cuadrados) + if len(enDiagonal) > 0: + return enDiagonal + enFila = self.reglaDeFila.obtener_coincidencias(tablero, cuadrados) + if len(enFila) > 0: + return enFila + enColumna = self.reglaDeColumna.obtener_coincidencias(tablero, cuadrados) + return enColumna + + +class FiltroDeOpuestosDeDiagonal(Regla): + def __init__(self, contenidoEsperado): + super().__init__("OPUESTO en DIAGONAL es", contenidoEsperado) + + def obtener_coincidencias(self, tablero, cuadrados): + resultado = set() + for cuadrado in cuadrados: + if cuadrado.OpuestoDeDiagonal is None: + continue + if tablero[cuadrado.OpuestoDeDiagonal].Contenido == self.ContenidoEsperado: + resultado.add(cuadrado.Índice) + return resultado + + +class FiltroDeOpuestosDeFila(Regla): + def __init__(self, contenidoEsperado): + super().__init__("OPUESTO en FILA es", contenidoEsperado) + + def obtener_coincidencias(self, tablero, cuadrados): + resultado = set() + for cuadrado in cuadrados: + if cuadrado.OpuestoDeFila is None: + continue + if tablero[cuadrado.OpuestoDeFila].Contenido == self.ContenidoEsperado: + resultado.add(cuadrado.Índice) + return resultado + + +class FiltroDeOpuestosDeColumna(Regla): + def __init__(self, contenidoEsperado): + super().__init__("OPUESTO en COLUMNA es", contenidoEsperado) + + def obtener_coincidencias(self, tablero, cuadrados): + resultado = set() + for cuadrado in cuadrados: + if cuadrado.OpuestoDeColumna is None: + continue + if tablero[cuadrado.OpuestoDeColumna].Contenido == self.ContenidoEsperado: + resultado.add(cuadrado.Índice) + return resultado + + +class FiltroCentral(Regla): + def __init__(self): + super().__init__("es en CENTRO") + + @staticmethod + def obtener_coincidencias(tablero, cuadrados): + resultado = set() + for cuadrado in cuadrados: + if cuadrado.EsCentro: + resultado.add(cuadrado.Índice) + return resultado + + +class FiltroDeEsquina(Regla): + def __init__(self): + super().__init__("es una ESQUINA") + + @staticmethod + def obtener_coincidencias(tablero, cuadrados): + resultado = set() + for cuadrado in cuadrados: + if cuadrado.EsEsquina: + resultado.add(cuadrado.Índice) + return resultado + + +class FiltroDeLado(Regla): + def __init__(self): + super().__init__("es LADO") + + @staticmethod + def obtener_coincidencias(tablero, cuadrados): + resultado = set() + for cuadrado in cuadrados: + if cuadrado.EsLado: + resultado.add(cuadrado.Índice) + return resultado + + +tengoTresEnUnaFila = FiltroDeContenidoDeFila(TipoDeContenido.Mia, 3) +tengoTresEnUnaColumna = FiltroDeContenidoDeColumna(TipoDeContenido.Mia, 3) +tengoTresEnDiagonal = DiagonalContenidoFilter(TipoDeContenido.Mia, 3) +oponenteTieneDosEnUnaFila = FiltroDeVictorias(TipoDeContenido.Oponente) + + +class Aptitud: + def __init__(self, ganados, empates, perdidos, conteoDeGenes): + self.Ganados = ganados + self.Empatados = empates + self.Perdidos = perdidos + conteoDeJuegos = ganados + empates + perdidos + porcentajeGanados = 100 * round(ganados / conteoDeJuegos, 3) + porcentajePerdidos = 100 * round(perdidos / conteoDeJuegos, 3) + porcentajeEmpates = 100 * round(empates / conteoDeJuegos, 3) + self.PorcentajeEmpates = porcentajeEmpates + self.PorcentajeGanados = porcentajeGanados + self.PorcentajePerdidos = porcentajePerdidos + self.ConteoDeGenes = conteoDeGenes + + def __gt__(self, otro): + if self.PorcentajePerdidos != otro.PorcentajePerdidos: + return self.PorcentajePerdidos < otro.PorcentajePerdidos + + if self.Perdidos > 0: + return False + + if self.Empatados != otro.Empatados: + return self.Empatados < otro.Empatados + return self.ConteoDeGenes < otro.ConteoDeGenes + + def __str__(self): + return "{:.1f}% Perdidos ({}), {:.1f}% Empates ({}), {:.1f}% Ganados ({}), {} reglas".format( + self.PorcentajePerdidos, + self.Perdidos, + self.PorcentajeEmpates, + self.Empatados, + self.PorcentajeGanados, + self.Ganados, + self.ConteoDeGenes) + + +if __name__ == '__main__': + unittest.main()