Mostrando entradas con la etiqueta python. Mostrar todas las entradas
Mostrando entradas con la etiqueta python. Mostrar todas las entradas

lunes, 26 de diciembre de 2022

Django desempolvado, reloaded (parte I)

Aunque Django no es mi herramienta ni entorno de trabajo habitual, me gusta mantenerme actualizado y guardo un especial recuerdo de este framework. Durante un tiempo fue mi medio de trabajo y fue una de las razones que afianzaron mi preferencia por Python como uno de mis lenguajes preferidos.



Como siempre, para conocer algo lo mejor es ponerse manos a la obra y hacer un mini-proyecto lo más realista posible. El tutorial de Django es una maravilla, con el paso de los años lo han ido ampliando y es una fuente de información muy valiosa cuando uno quiere recordar las bases de Django.

Nuestro mini-proyecto: un micro-cms

Desarrollemos un CMS parecido a Wordpress o la plataforma Blogger. No vamos a hacer nada especial:
  • Para clasificar los artículos tendremos secciones (estructura jerárquica) y tags (estructura horizontal)
  • En este CMS pueden escribir diferentes autores
  • Cada artículo puede tener una imagen ilustrativa. También vamos a permitir diferentes plantillas (por ejemplo, post en columnas, post con foto grande,...)
  • Dispondremos de una herramienta de administración o backend para editar artículos, categorías,..
  • La navegación podrá ser por autores, categorías y tags. La página de inicio será un listado con los últimos artículos
Estos son los requisitos de un proyecto que se hubiese planteado hace 15 años, pero no pretendemos hacer nada que no sea refrescar Django, es el objetivo.

  • Primer sprint: configurar el entorno y definir el modelo de datos
  • Segundo sprint: habilitar el backoffice de Django y ver si nos sirve como entorno de edición
  • Tercer sprint: habilitar la página final de un post
  • Cuarto sprint: habilitar las páginas de inicio, navegación por autores, secciones, tags,...
  • ... y lo que se nos vaya ocurriendo...

Primer sprint: configurar el entorno y definir el modelo de datos

En el momento de escribir esto (dic/2022), la versión de Django estable es 4.1.4 
Instalarla es trivial. Aunque se recomienda usar un "virtual env", dado que en el equipo que se va a desarrollar este mini-proyecto no hay conflictos de versiones, instalaremos Django directamente a nivel global.

$ sudo pip install Django==4.1.4

Una vez instalado, creamos el proyecto y entramos en el directorio

$ django-admin startproject microcms
$ cd microcms
$ ls microcms

Se nos lista un fichero manage.py y un directorio que se llama igual que el proyecto, microcms. Según la documentación, el nombre del directorio más externo, es indiferente. No así el interno, que contiene código y el típico __init__.py que lo identifica como un módulo.

Dentro del proyecto vivirán las apps que vayamos desarrollando, para proyectos pequeños o de ejemplo como este podría ser razonable hacerlo todo dentro de una misma app. Por ahora crearemos una app, cms, que es donde definiremos nuestro modelo de datos.
Si más adelante se nos ocurre alguna funcionalidad extra, tendría sentido desarrollarla en una app diferente.

$ ./manage.py startapp cms
$ ls 
total 12K
drwxrwxr-x 3 david david 4,0K dic 26 17:59 cms
-rwxr-xr-x 1 david david  664 dic 26 17:49 manage.py
drwxrwxr-x 3 david david 4,0K dic 26 17:59 microcms

Tenemos un nuevo directorio en el proyecto, es también un módulo en el que se definen modelos, vistas, etc.

$ ls cms/
total 24K
-rw-r--r-- 1 david david   63 dic 26 17:59 admin.py
-rw-r--r-- 1 david david  138 dic 26 17:59 apps.py
-rw-r--r-- 1 david david    0 dic 26 17:59 __init__.py
drwxrwxr-x 2 david david 4,0K dic 26 17:59 migrations
-rw-r--r-- 1 david david   57 dic 26 17:59 models.py
-rw-r--r-- 1 david david   60 dic 26 17:59 tests.py
-rw-r--r-- 1 david david   63 dic 26 17:59 views.py

En este primer sprint trabajaremos con cms/models.py

Definimos las entidades Section, Tag y Article. Por ahora utilizaremos como autores el propio objeto User que incorpora Django. Veremos si más adelante tenemos que personalizar esto.



A reseñar la autorreferencia en la entidad Section, para poder construir un árbol de secciones.

Debemos añadir esta app, cms, en el settings.py del proyecto:


Una vez hecho, esto, podemos hacer las migraciones para crear las tablas. Estamos usando una base de datos local sqlite, podemos examinarla al terminar el proceso y veremos todas las tablas de las diferentes INSTALLED_APPS:



Si ahora intentamos crear una sección, tenemos la primera en la frente: da error porque no le hemos dicho en el modelo que la sección "parent" podría ser vacía.


Si cambiamos la definición, y corremos las migraciones, ya podemos crear Sections sin un antecesor.

parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True)

lunes, 15 de abril de 2019

Cuando unos leguajes tratan los arrays por referencia y otros por copia

¿Qué ocurre cuando asignamos un array a otro? En la mayor parte de los lenguajes, lo habitual es que la nueva variable sea un puntero (referencia) al primer array. Por ejemplo, en Python:

array2 = array1 = {'uno': 1, 'dos': 2}

print('array1 antes:', array1)
print('array2 antes:', array2)

array2['dos'] = 22
array1['tres'] = 3

print('array1 después:', array1)
print('array2 después:', array2)


La salida de este script sería la siguiente:

~ $ python3 testarray.py 

array1 antes: {'uno': 1, 'dos': 2}
array2 antes: {'uno': 1, 'dos': 2}
array1 después: {'uno': 1, 'dos': 22, 'tres': 3}
array2 después: {'uno': 1, 'dos': 22, 'tres': 3}


Como era previsible, al cambiar algo en array1 o en array2, el otro array se modifica, porque las dos variables son una referencia al mismo array. (En Python a este tipo de datos se le llama diccionario, pero podemos asumir que es equivalente al array asociativo de PHP)

Este sencillo ejemplo es muy adecuado para explicar a alguien lo que son las referencias. Se ve claramente que cambiar algo en una variable tiene efectos colaterales.

Sin embargo, podemos encontrarnos sorpresas en otros lenguajes. PHP, por ejemplo, copia el array en la nueva variable y a partir de ese momento, son dos referencias independientes. Mucho cuidado si nuestro array contiene muchos datos, se duplica el consumo de memoria:

<?php 
$array1 = $array2 = array("uno" => 1, "dos" => 2);

echo "array1 antes:"; var_dump($array1); echo "\n";
echo "array2 antes:"; var_dump($array2); echo "\n";

$array1["tres"] = 3;
$array2["dos"] = 22;

echo "array1 después:"; var_dump($array1); echo "\n";
echo "array2 después:"; var_dump($array2); echo "\n";


La salida de este script es la siguiente:

~ $ php testarray.php 

array1 antes:array(2) {["uno"]=> int(1), ["dos"]=> int(2)}
array2 antes:array(2) {["uno"]=> int(1), ["dos"]=> int(2)}

array1 después:array(3) {["uno"]=> int(1), ["dos"]=> int(2), ["tres"]=> int(3)}
array2 después:array(2) {["uno"]=>, int(1), ["dos"]=> int(22)}

¿Qué ha pasado en este caso? Las dos variables son independientes, los cambios en una no afectan a la otra. Cada una apunta a una región de memoria diferente. En PHP se puede conseguir el mismo comportamiento que en otros lenguajes, esto es, que asignar una variable a otra por referencia:

<?php 
$array1 = array("uno" => 1, "dos" => 2);
$array2 = &$array1;

echo "array1 antes:"; var_dump($array1); echo "\n";
echo "array2 antes:"; var_dump($array2); echo "\n";

$array1["tres"] = 3;
$array2["dos"] = 22;

echo "array1 después:"; var_dump($array1); echo "\n";
echo "array2 después:"; var_dump($array2); echo "\n";

En este caso el comportamiento es el mismo que en el primer script en Python.
La expresión $array2 = &$array1asigna por referencia la variable 1 a la variable 2.

¿Qué comportamiento es deseable o más conveniente en un lenguaje de programación? La aproximación de PHP puede parecer más ineficiente, pero si reflexionamos un momento, tiene algo a su favor: es consistente.

<?php
$a = $b = 4;
$a = 5;
var_dump($a); var_dump($b);

La salida es:

int(5)
int(4)

Cada variable es una copia.

En Python, los tipos de datos simples sí que se asignan por copia:

Python 3.6.7 (default, Oct 22 2018, 11:32:17) 
>>> a = b = 4
>>> a = 5
>>> a
5
>>> b
4
>>>

También se ve cómo son variables independientes. 

Todos los tipos de datos, simples y compuestos, se comportan igual PHP, sin embargo, en Python los tipos de datos compuestos (diccionarios, listas,... se asignan por referencia y los tipos de datos simples (enteros,...), se asignan por copia.

Esto no es bueno ni malo... pero hay que estar atento.

sábado, 15 de septiembre de 2018

Refrescando Django (1)

A lo tonto, Django ya tiene unos cuantos añitos. Van por la versión 2.1 en el momento de escribir estas líneas (septiembre de 2018). Refresquemos un poco Django.

Vamos a hacer una web que sirva para guardar marcadores, parecido a lo que hacía Del.icio.us (qué tiempos...)

Requisitos


- La app debe presentar una pantalla de login y ofrecer al usuario la posibilidad de registrarse. El email será la clave para identificar al usuario.

- Una vez autentificado, la aplicación listará los marcadores que el usuario haya ido guardando, así como las etiquetas que el usuario haya asignado al marcador.

- En la pantalla de listado, se puede filtrar por etiquetas.

- Para añadir un marcador, se presenta un formulario al usuario. El sistema debe tratar de hacer una vista previa del marcador (extraer el título, algún resumen, foto representativa...)
El usuario puede asignar etiquetas a cada marcador. Las etiquetas se pueden sugerir autocompletando a partir de las que ya hay.

Preparación del proyecto


Empecemos instalando Django según las instrucciones de la documentación. Una vez instalado, inicializamos el proyecto.


django-admin startproject bookmarks

Lo primero que vamos a hacer es definir los diferentes entidades y sus correspondientes modelos.

Tag: representa una etiqueta para uno o varios marcadores. Sus campos serían, aparte del id, que ya lo autogenera Django, name y description. Sólo es obligatorio name.

Bookmark: representa un marcador. Sus campos serían title, description, url e image. Son obligatorios title y url.


Un marcador puede tener muchas etiquetas, a su vez, una etiqueta puede estar presente en muchos marcadores. Esto nos sugiere una relación ManyToMany. Solo es necesario hacer la asociación entre marcadores y etiquetas en un punto, según nos indican en la documentación. Lo más razonable parece hacer la asociación en la entidad Bookmark, puesto que es la entidad sobre la que todo gira.

User: representa un usuario. Vamos a reutilizar todo el sistema de gestión de usuarios de Django.

Una vez definidos las vistas que queremos y los modelos que vamos a necesitar, empezamos el desarrollo.

Dentro de un proyecto Django podemos tener varias apps, en este caso, para un proyecto tan pequeño, probablemente sólo necesitaremos una app. El proyecto es un contenedor, lo que "hace algo" son las diferentes apps que hay en un proyecto. Llamemos a la app bookmarks.

python3 manage.py startapp bookmarks 

CommandError: 'bookmarks' conflicts with the name of an existing Python module and cannot be used as an app name. Please try another name

No podemos, lógicamente. Ya hay un módulo llamado bookmarks (el correspondiente al proyecto). Tenemos que pensar un nombre para la aplicación que no entre en conflicto y que describa un poco la naturaleza de la función de esta aplicación. Llamémosla manager, ya que esta app será el gestor de marcadores.

En este punto tenemos en el proyecto dos módulos, uno llamado "bookmarks", que corresponde al proyecto en su globalidad, con sus ajustes comunes, rutado... y otro llamado "manager", que corresponde a la aplicación (para este proyecto probablemente sea la única app que lleguemos a hacer).



La carpeta de "fuera", la que contiene el proyecto, nos da igual como se llame, de hecho la vamos a renombrar.



Mejor así. Comprobemos que funciona:

BookmarksManager david$ python3 manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).

You have 15 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

September 15, 2018 - 11:10:40
Django version 2.1.1, using settings 'bookmarks.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
[15/Sep/2018 11:10:49] "GET / HTTP/1.1" 200 16348
[15/Sep/2018 11:10:49] "GET /static/admin/css/fonts.css HTTP/1.1" 200 423
[15/Sep/2018 11:10:49] "GET /static/admin/fonts/Roboto-Regular-webfont.woff HTTP/1.1" 200 80304
[15/Sep/2018 11:10:49] "GET /static/admin/fonts/Roboto-Bold-webfont.woff HTTP/1.1" 200 82564

[15/Sep/2018 11:10:49] "GET /static/admin/fonts/Roboto-Light-webfont.woff HTTP/1.1" 200 81348

Ya hemos terminado de montar la estructura del proyecto.

Definición de modelos

Vamos con los modelos del ámbito de la aplicación: Bookmark y Tag




Empezamos desde el principio utilizando la riqueza de tipos que Django nos aporta. Por ejemplo, un URLField no es más que un campo de texto con un validador adicional que nos comprueba si la URL que pretendemos guardar está bien formada.

Al utilizar un campo ImageField nos salta un error:

ERRORS:
manager.Bookmark.image: (fields.E210) Cannot use ImageField because Pillow is not installed.

HINT: Get Pillow at https://pypi.org/project/Pillow/ or run command "pip install Pillow".


Nos recuerda que para tratar imágenes, debemos instalar Pillow.

Instalamos la aplicación editando el settings.py del proyecto:



Nos quedan definir las relaciones entre Bookmark y Tag y los usuarios del sistema. Para ello, vamos a heredar nuestra entidad BookmarkUser de la clase AbstractUser.



Ejecutamos los comandos makemigrations y migrate y ya tenemos la base de datos preparada.



Preparando el admin

Para que en la aplicación admin de Django aparezcan estas entidades debemos registrarlas en admin.py de la aplicación.



También vamos a crear un usuario administrativo para la interfaz administrativa. Este usuario no es un usuario de la aplicación de marcadores, es un usuario interno de Django. Lo llamaremos "root", en analogía a UNIX.



Si accedemos a la interfaz admin con este usuario, ya podemos empezar a cacharrear con las entidades.




Hasta ahora no hemos hecho nada diferente de lo que el tutorial de Django nos enseña. Vamos a empezar a desarrollar en este punto nuestras funcionalidades.







martes, 6 de marzo de 2018

Euler #35

https://projecteuler.net/problem=35

La (pequeña) dificultad de este problema consiste en calcular todas las posibles rotaciones de un número. La sintaxis de Python hace muy cómoda esta tarea.

import sympy

def check_rotations(number):
 all_primes = True
 number_str = str(number)
 number_str = number_str[1:] + number_str[:1]
 while int(number_str) <> number:
  if not sympy.isprime(int(number_str)):
   all_primes = False
   break
  number_str = number_str[1:] + number_str[:1]
 return all_primes

circular_primes = []
for n in range(1, 1000000):
 if sympy.isprime(n) and check_rotations(n):
  circular_primes.append(n)

print circular_primes
print len(circular_primes)

Puntos a destacar:
  • sympy es una librería de Python especializada en matemáticas con símbolos (polinomios, resolución de ecuaciones, matrices,...) En nuestro script simplemente utilizamos la función isprime() para comprobar si el número es primo. Podríamos haber reutilizado alguna de las funciones que ya escribimos en problemas anteriores.
  • La expresión "cadena"[1:] devuelve todos los caracteres exceptuando el primero: "adena"
  • La expresión "cadena"[:1] devuelve el primer caracter, podríamos haber escrito también "cadena"[0]
  • La sintaxis de subíndices de Python garantiza siempre que s == s[:i] + s[i:] 
Una posible optimización es recorrer solo los números impares, fijando 2 como único par a incluir:

circular_primes = [2]
for n in range(3, 1000000, 2): # El tercer parámetro de range es "step"

martes, 20 de febrero de 2018

Euler #34

Tras un largo parón, otro problema del proyecto Euler, de nuevo lo solucionamos con Python. Tratando los números como cadenas de texto y convirtiendo los dígitos de nuevo a enteros el problema es muy sencillo de resolver.

https://projecteuler.net/problem=34


def fact(n):
 if n <= 1:
  return 1
 else:
  return n * fact(n - 1)

candidates = []
n = 3
while True:
# print "Analizyng", n
 n_str = str(n)
 partial_sum = 0
 for digit in n_str:
  partial_sum += fact(int(digit))
 if partial_sum == n:
  candidates.append(n)
 if n > 1000000:
  break
 n += 1

#print candidates
print sum(candidates)

lunes, 5 de junio de 2017

Euler #33

https://projecteuler.net/problem=33

De nuevo, la facilidad que ofrece Python para pasar de unos tipos a otros y los módulos estándar que trae hacen que este problema sea trivial.

Se puede compactar más la solución, pero así se ve claro cómo vamos filtrando poco a poco los números candidatos hasta llegar a la solución. También ayuda mucho el enunciado del problema, estableciendo cuántos resultados son esperables.

#!/usr/bin/env python
from fractions import Fraction
from operator import mul

fracts = []
for d in range(10, 100):
 for n in range(10, 100):
  if n / d < 1 and n % 10 != 0 and d % 10 != 0:
   num = str(n)
   den = str(d)
   for x in range(1, 10):
    xstr = str(x)
    if (num[0] == xstr and den[1] == xstr) or (num[1] == xstr and den[0] == xstr):
     new_n = int(num.replace(xstr, '', 1))
     new_d = int(den.replace(xstr, '', 1))
     if (new_n * d == new_d * n):
      fracts.append(Fraction(new_n, new_d))

print reduce(mul, fracts, 1)

La función "reduce" es muy útil para operar con listas en bloque. Es un atajo para no tener que escribir bucles:

tot = 1
for f in fracts:
    tot *= f
print tot

Así mismo, la clase Fraction es lo suficientemente inteligente para lidiar con enteros, operar con fracciones, simplificarlas,...

sábado, 27 de mayo de 2017

Euler #32

Llevaba bastante tiempo parado con la resolución de problemas del proyecto Euler.
Vamos con el número 32. La resolución con Python es muy fácil convirtiendo enteros en "strings":

#!/usr/bin/env python

numbers = []

for a in range(9999):
  for b in range(9999):
    p = a*b
    asString = str(a) + str(b) + str(p)
    if len(asString) > 9:
      break
    if len(asString) == 9 and \
     '1' in asString and \
     '2' in asString and \
     '3' in asString and \
     '4' in asString and \
     '5' in asString and \
     '6' in asString and \
     '7' in asString and \
     '8' in asString and \
     '9' in asString:
     if not p in numbers:
       numbers.append(p)
       print a, "*", b, " = ", p, " => ", asString

print sum(numbers)

jueves, 16 de marzo de 2017

El conjunto de Mandelbrot para 'dummies'




¿Quién no se ha sentido atraído por la belleza de esta figura? La representación gráfica del conjunto de Mandelbrot es el típico ejemplo de figura fractal y es, probablemente, la más popular.


Pero, ¿qué es exactamente, y cómo se llega a esta figura?

Definiendo el conjunto de Mandelbrot


Pertenecen al conjunto de Mandelbrot todos aquellos números complejos que cumplen una propiedad: que el valor de una sucesión determinada, que ahora veremos, para un determinado número, es convergente, es decir, no tiende a infinito.

La sucesión de la que hablamos es recursiva, y  es esta:

zn+1 = z2 + C

donde C es el número que estamos tratando de comprobar si pertenece al conjunto o no y z se va calculando recursivamente, esto es, tomando como entrada de cada iteración el valor obtenido anteriormente.

Recordemos que la aritmética de números complejos involucra operaciones con binomios del tipo a+bi, donde i es la unidad compleja. Recordemos que las operaciones en las que interviene i son un poco especiales, en este caso debemos recordar que i2 es -1.

Vamos a hacer unas cuentas con la ayuda del intérprete de Python, que tiene soporte de serie para números complejos (la unidad imaginaria se denota con j en Python).

Primero, definimos la función que aplicaremos de forma recursiva:
>>> def fm(z, c):
...     return z**2 + c

Ahora, vamos a probar qué pasa con algunos números al aplicarle la función. Empezaremos con el número 1 + i, en notación Python habría que escribirlo como 1 + 1j. Aplicaremos la función anterior cinco veces seguidas, en la primera iteración tanto z como c serán 1 + i y en las siguientes, el resultado de la función se usa como entrada para la siguiente. Es más difícil decirlo que hacerlo:
>>> z = c = 1+1j
>>> for x in range(5):
...     z = fm(z, c)
...     print z
... 
(1+3j)
(-7+7j)
(1-97j)
(-9407-193j)
(88454401+3631103j)

Vemos que tras apenas 5 iteraciones, los valores se van disparando, así que, claramente, la sucesión de valores es divergente, se va a infinito.

Probemos con 0.5 + 0.3j:
>>> z = c = 0.5 + 0.3j
>>> for x in range(5):
...     z = fm(z, c)
...     print z
... 
(0.66+0.6j)
(0.5756+1.092j)
(-0.36114864+1.5571104j)
(-1.79416445761-0.82469660658j)
(3.03890160806+3.25928267968j)

También parece que diverge, pero no tan rápido como el caso anterior.

Otro caso más, con el número 0.2 - 0.1j:


>>> z = c = 0.2 - 0.1j
>>> for x in range(5):
...     z = fm(z, c)
...     print z
... 
(0.23-0.14j)
(0.2333-0.1644j)
(0.22740153-0.17670904j)
(0.220485371029-0.180367812122j)
(0.216081251188-0.179536927955j)

Tras cinco iteraciones, no parece que se vaya a infinito. Probemos otras cinco iteraciones:


>>> for x in range(10):
...     z = fm(z, c)
...     print z
... 
(0.214457598616-0.177589128054j)
(0.214454163201-0.176170675885j)
(0.214954481072-0.175561069755j)
(0.21538373972-0.175475277291j)
(0.215598582395-0.175589042903j)
(0.215651236743-0.175713497468j)
(0.215630222717-0.175785666083j)
(0.215595792549-0.175809404656j)
(0.215572598999-0.175807535868j)
(0.215563255771-0.175798574862j)

 Parece claro que este número, 0.2 - 0.1j, al aplicarle la función del principio, la sucesión que se obtiene es convergente.

Se dice que un número pertenece al conjunto de Mandelbrot cuando la sucesión es convergente, es decir, no se va a infinito. Se puede probar que en cuanto que uno de los resultado de la sucesión se sale del rango de ± 2 ± 2i, la sucesión será divergente, esto es, el número no pertenecería al conjunto. La forma más rápida de calcular esto es comprobando, para cada uno de los resultados intermedios, si el módulo del número complejo es > 2, o lo que es lo mismo, si la suma de los cuadrados de sus partes real e imaginaria es > 4. De esta última forma llegamos al mismo resultado ahorrándonos hacer una raíz cuadrada, que siempre es más costosa computacionalmente.

Sabiendo esto, vamos a empezar a comprobar todos los posibles números imaginarios que hay entre -2 - 2i y 2 + 2i, y viendo si la sucesión que se deriva de cada uno de ellos diverge. Obviamente, hay infinitos números en esta región, así que tenemos que ir calculando el mayor número de ellos con pequeños incrementos.

De la misma forma, tendremos que hacer unas cuantas iteraciones para saber si la sucesión diverge o no. Por ejemplo, para el número 0.5 + 0.3j que vimos antes tuvimos que hacer 5 iteraciones o calcular 5 términos de la sucesión antes de darnos cuenta de que realmente divergía.

Empecemos de forma sencilla: moviéndonos en pasitos de 0.1 y haciendo 3 iteraciones. Tendríamos esta representación gráfica:



Vemos que tenemos muy poco nivel de detalle, incrementemos paulatinamente el nivel de detalle (haciendo los incrementos más pequeños y aumentando el número de iteraciones para "cazar" más números que están fuera del conjunto), ahí van unas cuantas muestras:





En este punto, ya tenemos un detalle bastante fino, pero es probable que, como hemos hecho pocas iteraciones, haya números que pensemos que pertenecen al conjunto, pero en realidad, no pertenecen. Aumentemos las iteraciones.





Hasta ahora nos hemos limitado a colorear en rojo los números que, al aplicar la función, la sucesión diverge; en negro, los que converge.

El siguiente paso en detalle, y aquí es donde aparece la "magia" de este conjunto, es asignar diferentes colores a la velocidad con la que los números que no pertenecen al conjunto, al evaluar la sucesión, divergen. Esto es, si asignamos colores diferentes al número de iteraciones necesarias para determinar si el número no pertenece al conjunto, empezamos a obtener unos bonitos gráficos.

La gracia en este caso es encontrar una gradación de colores y asignación al número de iteraciones que quede bonita. Por ejemplo, una sencilla, basada en amarillos:



Un último ejemplo, con mayor resolución, utilizando una gama más o menos aleatoria de colores:


Implementación


Es bastante sencillo implementar una representación gráfica del fractal. Esta que se muestra está hecha en HTML Canvas y Javascript, usando "plain Javascript", sin plugins ni librerías.

Lo primero que debemos hacer es definir un canvas en nuestro HTML. También hemos agregado unos input para poder jugar con el incremento y el número de iteraciones a probar.


<canvas id="canvas"></canvas>
<form>
<p>Increment: <input name="increment" value="0.01"></p>
<p>Iterations: <input name="iterations" value="1000"></p>
<p><input type="button" value="Run!" id="run"></p>
<p id="message"></p>
</form>

A continuación, escribimos el código Javascript que hará los cálculos y representará los puntos en pantalla. El primer paso, es acceder e inicializar el elemento canvas:


var width = 600;
var height = 600;
var c = document.getElementById("canvas");
c.setAttribute('width', width);
c.setAttribute('height', height);
var ctx = c.getContext("2d");

Ahora definimos algunas constantes y funciones:
  • adjust_scale(i, j) => centra los puntos correctamente en el gráfico. Nuestro sistema de coordenadas es el mátemático (0, 0) en el centro, en un canvas el (0, 0) es la esquina superior izquierda.
  • get_color(nIter) => devuelve un color en función del número de iteraciones. En esta función se puede ser creativo y experimentar para probar diferentes gamas de colores y gradaciones. En nuestro ejemplo nos limitamos a una gama de rojos.
  • mandelbrot(z, c) => calcular un término de la sucesión aplicando la fórmula zn+1 = z2 + C, ya explicada.
  • in_mandelbrot_set(c, maxIter) => determina si un número C está en el conjunto de Mandelbrot utilizando un máximo de iteraciones. Devuelve un objeto con dos claves: si la sucesión es convergente y el número de iteraciones necesarias para llegar a dicha conclusión.
  • run() => lanza el gráfico.

El código completo es este:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<canvas id="canvas"></canvas>
<form>
<p>Increment: <input name="increment" value="0.01"></p>
<p>Iterations: <input name="iterations" value="1000"></p>
<p><input type="button" value="Run!" id="run"></p>
<p id="message"></p>
</form>

<script>
var width = 600;
var height = 600;
var c = document.getElementById("canvas");
c.setAttribute('width', width);
c.setAttribute('height', height);
var ctx = c.getContext("2d");

var mandelbrot_limit = 1.8;
var mandelbrot_max_module = 4;



function adjust_scale(i, j) {
    return [(width/(2*mandelbrot_limit))*i + (width/2), (height/(2*mandelbrot_limit))*j + (height/2)];
}

function get_color(nIter) {
    var factor = 50 + nIter;
    factor = parseInt(factor);
    if (factor > 255) factor = 255;
    return "rgb("+factor+",0,0)";
}

function mandelbrot(z, c) {
    var x = z[0]; 
    var y = z[1];
    var real = x*x - y*y + c[0];
    var imag = 2*x*y + c[1];
    return [real, imag];
}

function in_mandelbrot_set(c, maxIter) {
    var x = 0;
    var res = c
    while (x < maxIter) {
        res = mandelbrot(res, c);
        if ((res[0]*res[0] + res[1]*res[1]) > mandelbrot_max_module) {
            return {'status': false, 'nIter': x}; // No está en el conjunto, hemos tardado x interaciones en saberlo
        }
        x++;
    }
    return {'status': true, 'nIter': x};
}

function run() {
    ctx.fillStyle = "#FFFFFF";
    ctx.fillRect(0, 1, width, width);

    var mandelbrot_inc = parseFloat(document.forms[0].elements.increment.value);
    var mandelbrot_test_iterations = parseInt(document.forms[0].elements.iterations.value);
    var point_width = 1;
    if (mandelbrot_inc >= 0.0009) point_width = 2;
    if (mandelbrot_inc >= 0.009) point_width = 8;       
    if (mandelbrot_inc >= 0.09) point_width = 16;       

    document.getElementById("message").innerHTML = "Increment: "+mandelbrot_inc+", Iterations: "+mandelbrot_test_iterations;
    for(var i=-mandelbrot_limit; i<=mandelbrot_limit; i += mandelbrot_inc) {
        for (var j=-mandelbrot_limit; j<=mandelbrot_limit; j += mandelbrot_inc) {
            check = in_mandelbrot_set([i, j], mandelbrot_test_iterations);
            if (check.status) {
                ctx.fillStyle = "#000000";
            } else {
                ctx.fillStyle = get_color(check.nIter);
            }
            var point = adjust_scale(i, j);
            ctx.fillRect(point[0], point[1], point_width, point_width);
        }
    }
}

document.getElementById("run").addEventListener('click', run);

</script>