Showing posts with label Artificial Intelligence. Show all posts
Showing posts with label Artificial Intelligence. Show all posts

Wednesday, 17 November 2010

Alpha-beta pruning algorithm with Delphi

I'm trying to get round to writing more often and to do my own research with some interesting and complicated algorithms. Given to my great passion for different types of algorithms, in AI , I have found one very interesting and here you can get it build with Delphi 2010. With this solution, you'll be able to add values to the leaves and run the algorithm to find the nodes that will be evaluated and others that will be pruned. I've used my little framework to build the structure (using a GDI render) and then only focus on the algorithm that was the important part. Alpha-beta pruning is a search algorithm which seeks to reduce the number of nodes that are evaluated in the search tree by the minimax algorithm. It is a search with adversary algorithm used commonly for machine playing of two-player games (Tic-tac-toe, Chess, Go, etc.). It stops completely evaluating a move when at least one possibility has been found that proves the move to be worse than a previously examined move. Such moves need not be evaluated further. Alpha-beta pruning is a sound optimization in that it does not change the result of the algorithm it optimizes (Algorithmist.com). 

The pseudo-code is quite simple, and it looks like this:
The structure of the application is very simple and the most difficult part is the simulation process where you need to draw every step of the algorithm.
You can download the application from here (Thundax Alpha-beta pruning) and play with it to understand how the algorithm works.
This video shows the execution of the program:
The code of the algorithm is explained as follows:

Alpha-beta algorithm:
function TViewMinimax.alphabeta(Node: TNode; alpha: integer; beta: integer): integer;
var
    i: integer;
    ret: integer;
begin
    if Node.isLeaf then
        ret := Node.Value
    else
    begin
        for i := 0 to Node.children.Count - 1 do
        begin
            alpha := max(alpha, -alphabeta(Node.children.Items[i], -beta, -alpha));
            case Node.player of
                tMAX:
                    begin
                        Node.Value := alpha;
                        SetAlpha(Node, alpha);
                        Node.visited := true;
                    end;
                tMIN:
                    begin
                        Node.Value := -alpha;
                        SetBeta(Node, alpha);
                        Node.visited := true;
                    end;
            end;
            if beta <= alpha then
                Break;
        end;
        ret := alpha;
    end;
    result := ret;
end;

Drawing the nodes by using a pre-order method for iterating the binary tree:
procedure TViewMinimax.Draw;
begin
    preorder(Self.FMinimax.root);
end;

procedure TViewMinimax.preorder(root: TNode);
begin
    if Assigned(root) then
    begin
        if root.children.Count > 0 then
        begin
            DrawLine(root, root.children.Items[0]);
            DrawLine(root, root.children.Items[1]);
            preorder(root.children.Items[0]);
            preorder(root.children.Items[1]);
        end;
        DrawRectangle(root);
    end;
end;

procedure TViewMinimax.DrawRectangle(Node: TNode);
begin
    Fcanvas.Font.Name := 'Arial';
    Fcanvas.Brush.Style := bsSolid;
    if Node.visited then
        Fcanvas.Brush.Color := clRed
    else
        Fcanvas.Brush.Color := clWhite;
    if Node.selected then
        Fcanvas.Brush.Color := clGreen;
    Fcanvas.Pen.Width := 2;
    Fcanvas.Pen.Color := clBlack;
    Fcanvas.Rectangle(Node.position.x - gap, Node.position.y - gap, Node.position.x + gap, Node.position.y + gap);
    Fcanvas.TextOut(Node.position.x - 5, Node.position.y - 5, Inttostr(Node.Value));
    Fcanvas.Brush.Color := clWhite;
    if (Node.isLeaf) and (Node.Order <> 0) then
        Fcanvas.TextOut(Node.position.x - 5, Node.position.y + 15, Inttostr(Node.Order));
    if not Node.isLeaf then
    begin
        case Node.player of
            tMAX:
                begin
                    if Node.alpha <> 0 then
                        Fcanvas.TextOut(Node.position.x - 65, Node.position.y - 10, Inttostr(Node.Order) + ': a=' + Inttostr(Node.alpha));
                    if Node.newAlpha <> 0 then
                        Fcanvas.TextOut(Node.position.x - 65, Node.position.y + 5, Inttostr(Node.NewOrder) + ': a=' + Inttostr(Node.newAlpha));
                end;
            tMIN:
                begin
                    if Node.beta <> 0 then
                        Fcanvas.TextOut(Node.position.x - 65, Node.position.y - 10, Inttostr(Node.Order) + ': ß=' + Inttostr(Node.beta));
                    if Node.Newbeta <> 0 then
                        Fcanvas.TextOut(Node.position.x - 65, Node.position.y + 5, Inttostr(Node.NewOrder) + ': ß=' + Inttostr(Node.Newbeta));
                end;
        end;
        if Node.text <> '' then
        begin
            Fcanvas.Brush.Color := clyellow;
            if (Node.newAlpha = 0) and (Node.Newbeta = 0) then
                Fcanvas.TextOut(Node.position.x - 53, Node.position.y + 5, Node.text)
            else
                Fcanvas.TextOut(Node.position.x - 53, Node.position.y + 20, Node.text)
        end;
    end;
end;

Setting up the Minimax Algorithm:
procedure TForm1.Start1Click(Sender: TObject);
var
    vminimax: TViewMinimax;
    minimax: TMinimax;
    words: TStringList;
    alpha, beta: integer;
begin
    minimax := TMinimax.Create;
    if Edit1.Text <> '' then
    begin
        words := TStringList.Create;
        SplitTextIntoWords(Edit1.Text, words);
        minimax.AddNumbers(words);
    end;
    minimax.CreateTree;
    Memo1.Lines.Clear;

    Image1.Canvas.Brush.color := clwhite;
    Image1.Canvas.Rectangle(0, 0, Image1.Width, Image1.Height);

    vminimax := TViewMinimax.Create(Self.Image1.Canvas, minimax, Memo1);
    if CheckBox1.Checked then
        vminimax.time := 1000
    else
        vminimax.time := 0;
    alpha := alphavalue;
    beta := betavalue;

    vminimax.Draw;
    Self.Image1.Canvas.TextOut(951, 25 - 5, 'MAX');
    Self.Image1.Canvas.TextOut(951, 127 - 5, 'MIN');
    Self.Image1.Canvas.TextOut(951, 228 - 5, 'MAX');
    Self.Image1.Canvas.TextOut(951, 328 - 5, 'MIN');

    Start1.Enabled := false;
    CheckBox1.Enabled := false;
    vminimax.alphabeta(minimax.root, alpha, beta);
    vminimax.Draw;
    Start1.Enabled := true;
    CheckBox1.Enabled := true;

    FreeAndNil(words);
    FreeAndNil(minimax);
    FreeAndNil(vminimax);
end;

Splitting the text into numbers (comma separated values):
procedure SplitTextIntoWords(const S: string; words: TstringList);
var
    startpos, endpos: Integer;
begin
    Assert(Assigned(words));
    words.Clear;
    startpos := 1;
    while startpos <= Length(S) do
    begin
        while (startpos <= Length(S)) and (S[startpos] = ',') do
            Inc(startpos);
        if startpos <= Length(S) then
        begin
            endpos := startpos + 1;
            while (endpos <= Length(S)) and not (S[endpos] = ',') do
                Inc(endpos);
            words.Add(Copy(S, startpos, endpos - startpos));
            startpos := endpos + 1;
        end;
    end;
end;

Related links:

Wednesday, 10 November 2010

Genetic Algorithms (GA)

This semester I am improving knowledge in terms of Artificial Intelligence and one of the concepts that most captivated me was the resolution of problems using genetic algorithms. These are based on coding the problem in chromosomes and genes into packages. After applying a series of functions, we need to mutate the population to reach our goal. and find the best solution. The definition from wikipedia is: "The genetic algorithm (GA) is a search heuristic that mimics the process of natural evolution. This heuristic is routinely used to generate useful solutions to optimization and search problems. Genetic algorithms belong to the larger class of evolutionary algorithms (EA), which generate solutions to optimization problems using techniques inspired by natural evolution, such as inheritance, mutation, selection, and crossover".
Basically the algorithm works like this:


There are several examples on the net which uses GA to solve common problems such as these:
One of the examples that best shows the use of GA is the solution from Roger Alsing where the algorithm is trying to copy an image generating polygons. These polygons are created in a population of chromosomes and they try to evolve to fit the source image. I've been playing with it and it's great!. You can download the source code and the binaries from here.


Here you can see the results of playing with the application:

Genetic Vectorizer Example by Roger Alsing:

Using my image (40h of running time):

using Einstein image (20h of running time):
Video showing the start of the algorithm:


In the video you can see the different steps the algorithm is doing by creating a population of chromosomes as polygons and trying to fit them by shape and by colour from the source image. The result of the application is the image shown  above.


Sodorace game:
Sodarace is the on-line Olympics pitting human creativity against machine learning in a competition to design robots that race over 2D terrains using the Sodaconstructor virtual construction kit.

Enjoy the learning!.

Related articles:

Tuesday, 9 February 2010

Parsing and drawing molecular structures with delphi part II

Today I've released the last version of my program with several changes into the graph script implementation. This new version has a little windows with a simple syntax highlighting system with the usually tokens that I use in my script. The scripting language is very easy to use and very intuitive instead of only writing numbers with no sense. Now we can create and manipulate the way the graphs are created by simple commands. I'm working on the syntax checker that will tell you the error line and a little description of the problem.

Now we can write:


layout.node(x).ToNode(y).parameters := [EdgeType = SimpleArrowEdge, SizeBox = 20, LineColor = clRed, PenWidth = 2];


where:

layout, is the canvas where we need to create our node or to look for our node. node(x) is the first node to need to be created and this will be connected to toNode(y). We can finish this sentence here (with a semicolon mark ";") or going on calling the parameters. Where we have, the EdgeType, SizeBox, LineColor and PenWidth parameters. On the EdgeType parameter, we can use this list:
  • SimpleEdge
  • SimpleArrowEdge
  • SimpleDoubleArrowEdge
  • SimpleDoubleLinkedArrowEdge
  • DottedEdge
  • DottedArrowEdge
  • DottedDoubleArrowEdge
  • DottedDoubleLinkedArrowEdge
Each of one of these edges, will draw a different kind of component into the layout. For the SizeBox we can specify an integer number in pixels to build or resize the length of every box inside the command. The line colour is a typical delphi constant like clRed, clBlue, clYellow, etc. and the last property as an integer number as well as the SizeBox, PenWidth, that will change the width of the line drawn.

This script will generate the next diagram:

I'll try to finish all the things that I have in mind this week in order to focus on other parts of the application. Due to the last petitions into the post "Automation Object Library for Vijeo Citect", I'm being forced to show you (willingly) my last achievements into this field. I'm still merging my last application "Thundax Box Manager" with the last version of my "Canvas test Framework", and I'm very keen on releasing the new application "Thundax P-zaggy" that will contain all the modules to work with graphs.

You can now download the last released version from SourceForge:
I hope you enjoy!.

Sunday, 10 January 2010

Force-Directed graph layout approximation part IV

I'm still working on the component, and I've implemented zoom in and zoom out methods for the better visualization of the graph. The issue here is that when we develop a system like this (visual application with mouse edition), all the action it should do is allegedly involved. Like the drag and drop, the zooming system, printing, etc. And you should always take into account all these problems. We have got into a bad habit by what the other programs do, and when we develop a new application, we usually take a little from everywhere. Nevertheless, it's better to let things clear from the beginning and report the final characteristics of your program.

With this new methods, now we can enlarge the graph and zoom out the view until we see all the drawing inside the picture. I haven't finished it yet, but I can show you the latest results:


Wednesday, 6 January 2010

Force-Directed graph layout approximation part III

Today I've released the last version of my project VLO. You can find the latest win32 desktop application with the canvas test here:
In this version I've made several changes due the bug list that I had. Now, after fixing all the little bugs that the application had, I can say that I'm very satisfied with the results. Indeed, I've changed the hierarchy of classes that I have, and I've moved all of them to a TNode and TGraph classes instead of TBox and TBoxManager. In a few weeks I'm gonna release a new application with all this algorithms (and I'm still thinking in the name of this application).

Monday, 4 January 2010

Force-Directed graph layout approximation part II

In this article I'll describe the different calculations that we have to do in Repulsive and Attraction methods to calculate the position of the different nodes. As I published yesterday in part I, I'll focus on the Hooke's and Coulomb's laws and the integration inside the platform.
  • Hooke's Law (attractive force):
The functionality here is very simple, we need to think that every edge represents a spring, and when 2 nodes that are connected try to move in opposite direction (by a force of repulsion), the spring tend to be deformed by that force. When this happens (the spring is being forced to move) , it returns to the initial state of equilibrium. For this, we need to create a model to generate this behaviour. I've found several examples on the Internet, like in johngrindall.com (Pendulum simulation ), for example in youtube we can find very good videos explaining the hooke's law:

Sunday, 3 January 2010

Force-Directed graph layout approximation part I

I've been working on these last days in a Force-Directed graph layout approximation applicated in my VLO Framework (That's why I've been off these days without posting anything). The system consists on applying physics laws to each node calculating the repulsion force using Coulomb's law and the attractive force using Hooke's law. To do this, I've made a lot of changes to accomplish all the requirements of the new functionality. Now, every TBox class implements a TNode class which has all the physics components like hooke_force, coulomb_force, speed, mass, etc. And the manager ThundaxBoxManager that handle all the canvas methods now implements the TGraph class which has all the data about repulsion calculation, attraction calculation, the energy of the system, etc. The algorithm is easy to understand because it works in a simple way. We need to iterate every node and calculate in steps the hooke and Coulomb force for everyone. In every step, there will be new forces that interact with each node, and when the total amount of energy is nearly zero, we can stop the algorithm.

Sunday, 1 November 2009

Cálculo de rutas de transporte parte V

En éste artículo os entrego la última versión de la aplicación con mejoras adicionales sobre la selección de caminos. Continuando con la IV parte, he modificado ligeramente el algoritmo para identificar y marcar diferentes destinos en función de un parámetro situado en la propia conexión. Por ejemplo, si nos encontramos en un ítem que tiene 2 caminos a seguir, éstos deben estar numerados para indicar cual tomar.

Si éste elemento es un dispositivo físico, de alguna manera deberá posicionarse para dirigirse hacia un sitio u otro. De ésta manera si nos encontramos con la siguiente situación:

Indicamos numéricamente que si queremos ir del dispositivo 40 al 11, éste debe estar en su posición '2' y si quiere dirigirse hacia el 39, en la posición '1'. Físicamente representaría que por ejemplo una válvula debe estar posicionada en un sentido (abierta) para acceder a 11 y cerrada para 39 por ejemplo. De éstos ejemplos encontraremos muchísimos en la selección de caminos, y es interesante pararse un momento y pensar sobre éstos y buscar una solución elegante.

De momento he hecho que el nombramiento de las conexiones sea automático y sin ningún orden. Camino que selecciono, camino que marco. Ahora el cálculo es más eficiente, más rápido y con mejor disponibilidad a la hora de hacer pequeños cálculos para obtener éstos parámetros.

Si nos fijamos en el siguiente ejemplo:

Veréis que cada enlace está marcado y cuando generamos la salida para el cálculo de rutas, podemos ver como identifica cada uno de los nodos:

En verde se marcan los orígenes, en azul los elementos bloqueados y en fucsia los destinos. Además ésta versión dispone de las siguientes mejoras:
  • Selección total de los nodos
  • Movimiento total de los nodos mediante botones
  • Enlaces de bloqueo identificados en azul (no se editarán)
Aquí os dejo la última versión:


Thursday, 29 October 2009

Cálculo de rutas de transporte parte IV

En ésta entrega, hago el primer desarrollo profesional de mi framework VLO sobre un ejemplo real para el cálculo de rutas. Como ya sabéis, el VLO framework es una plataforma de desarrollo desarrollada en Delphi 2010 que pretende obtener un vínculo gráfico con un Objeto. De ésta manera esa porción de memoria que estamos utilizando se está representado en pantalla utilizando una serie de conceptos gráficos. Llevo trabajando en éste proyecto unos 2 meses y medio y como podéis ver, si se planifica bien el trabajo, se tienen las ideas claras y partimos con un buen background en orientación a objetos y diseño utilizando patrones podemos crear herramientas increíbles.

Luego vino la implementación de la plataforma sobre ejemplos reales, al principio podía crear diagramas de grafos y calcular la ruta más corta utilizando Dijkstra. Después le di una parte mucha más gráfica y implementé un sistema multi capas para poder disponer de texto, imagen y caja en el mismo componente. A partir de ahí y con otras muchas ideas (gracias a los morning pages) apareció el cálculo de rutas. Hace ya bastantes años que me dedico al sector de la automatización industrial y la mayoría de aplicaciones que desarrollo son para cumplir los requisitos que necesitan éste tipo de industria.

Por lo tanto, continuando con ésta última versión de la aplicación, veréis que hay un pequeño cambio y es que ahora ya tengo implementado el algoritmo para reconocer scada's (Vijeo Citect) utilizando la Graphics Builder Automation Interface, y aquí ya vi un filón.

¿En qué consiste ésta última versión?. Pues bien, si disponemos de un diagrama real de Scada, por ejemplo como el siguiente:

Podemos ver una configuración típica donde se transporta el producto desde unos silos de origen a unos recipientes destino. Ahora lo que hay que hacer, es escanear la librería de "Genies" (Objetos del Vijeo Citect) con Thundax Genies Scanner y volcar los datos en la ubicación de los resources de Thundax Box Manager (para que disponga de todas las imagenes de la librería y así poder representar las boxes con la imagen correcta).

Una vez escaneada una librería como la presentada en la imagen, ya podemos iniciar Thundax Box Manager y escanear la pantalla del Scada con la aplicación utilizando la librería de automatización. Sobretodo antes, visualizar en las opciones de la aplicación Thundax Genies Scanner la ubicación de los resources:


Ahora, iniciamos Thundax Box Manager, y con la pantalla seleccionada del Scada, nos dirigimos al menú Citect -> Load Page Diagram, y en pocos segundos, tendremos cargada la misma estructura en mi aplicación (solo las genies, los símbolos no, porque no representan ninguna estructura de control, es decir, no tienen ninguna lógica dentro del proceso):

Ahora solo tenemos que perder 15 segundos en realizar las conexiones lógicas entre elementos o ítems y luego calcularemos las rutas:

Tenemos que acordarnos de marcar cuales son los orígenes y cuales los destinos:

Ahora entenderéis el concepto de "Bloqueo o Interlock" con la siguiente imagen:

Si os fijáis el producto pasa por la cinta superior, y hay 2 válvulas que sacan el producto. Pero tenemos que tener en cuenta que solo se puede abrir 1 a la vez, ya que sinó enviariamos producto a un destino diferente al requerido. Por lo tanto debe estar bloqueada la otra válvula y no abrirse durante la ejecución de la ruta seleccionada. De ahí que exista el objeto "interlock", y que se gestione en el cálculo de la ruta de transporte.

Ahora solo tenemos que ejecutar "Citect -> Calc Transport Routes" et voilà!, ya tenemos generadas las 100 rutas disponibles que hay en menos de 100 ms.


Además podemos ver la ruta seleccionada haciendo click en la ruta:

Aún quede mucho trabajo por delante, pero me lo tomo con mucha ilusión al ver que por fin el programa tiene una meta asequible: Crear rutas de transporte para que otro programa pueda trabajar con éstas y ejecutarlas. Lo interesante de todo ésto es que podemos ahorrar un montón de tiempo intentando averiguar la cantidad de rutas que tenemos y sobretodo intentar el poderlas implementar gráficamente manteniendo como expliqué hace 2 meses un mapa lógico sobre una estructura que carece de inteligencia.

Aquí os dejo las últimas versiones de mi aplicación:
He cambiado la privacidad de la aplicación y de momento la he hecho "trial de 30 días" esperando crear un paquete más completo con sus diferentes opciones. Espero que la lectura haya sido entretenida!.

Wednesday, 28 October 2009

Cálculo de rutas de transporte parte III

Continuando con los artículos sobre el tema del cálculo de rutas, aquí os dejo la última versión estable y mejorada sobre un ejemplo un poco más complejo y más real para diferentes destinos y con bloqueos entre elementos. El término bloqueo significa que no puede existir una ruta sin antes bloquear una serie de elementos. Éstos se tienen que vigilar, entre sí, ya sea por un equipo inteligente u otro tipo de algoritmo o aplicación. En éste ejemplo veréis como puede llegar a complicarse un sistema de rutas:

Si nos fijamos bien, existen muchos destinos y además existen bloqueos entre caminos (representados por un nuevo concepto de símbolo):


¿Ésto que indica?. Nos indica que los 4 elementos están bloqueados entre sí, y para no dibujar (n-1) flechas entre los diferentes objetos, creamos uno diferente que creará automáticamente el enlace con las demás cajas. De ésta manera cuando se cree el camino que pase por el elemento 4, éste llamará a la caja superior y recogerá los 3 elementos que tiene para introducirlos en la descripción de la ruta.

De ésta manera obtenemos lo siguiente:


Aquí os dejo la última versión de la aplicación más los ejemplos hasta ahora hechos. Éste último ejemplo lo he hecho en menos de 3 minutos, y el cálculo total de rutas en un total de 16ms.
PD: Ésta última versión está protegida con mxProtector y solo permitirá 10 ejecuciones. De momento tengo que poner ésto ya que no es una versión definitiva y no quiero que corra por la red sin antes tener montada una buena base. Además, ahora ya empieza a hacer cosas importantes.

Monday, 26 October 2009

Cálculo de rutas de transporte parte II

Continuando con mi anterior artículo, he mejorado el algoritmo de búsqueda de caminos, de tal manera que ahora permite la búsqueda de un sinfín de posibilidades con tiempos extremadamente pequeños. En la versión anterior me di cuenta de que había un problema con el diseño del algoritmo y era referente a que a la hora de buscar los nodos adyacentes, éstos tienen que estar ordenados por el número de conexiones entrantes que tienen. De ésta manera podemos calcular cosas tan complejas como éstas:

A simple vista parece ser bastante difícil el obtener el número de caminos disponibles pero al final acaba siendo un simple recorrido de nodos:

Como podéis ver en la última imagen, hay 31 rutas disponibles. Además, en ésta última versión, ya está resuelto el problema de multi-orígenes y multi-destinos de tal manera que el algoritmo ya es capaz de recorrerlos todos con un coste cuadrático. Hay que mejorar un poco el coste pero estamos hablando de hacer el cálculo de unas 30 rutas en unos 16 mili segundos.

Si nos fijamos en la siguiente imagen, podréis ver la resolución con múltiples orígenes y múltiples destinos:

Si os fijáis en la imagen también queda marcado el camino seleccionado en la grid, de ésta manera visualmente podemos reconocer la ruta seleccionada y comprobar que los cálculos estén correctamente realizados.

El cálculo ahora es bastante diferente de como se hacía en el artículo anterior. Se me ocurrió la idea de realizar la búsqueda a través de la tabla de adyacentes sin tener que hacer antes un barrido. De ésta manera realizando una serie de operaciones que ahora detallaré, tenemos resuelto el problema del cálculo de rutas de transporte.

Con el siguiente código realizamos el recorrido total de la lista de adyacentes:


fila := 1;
filas := 0;
columna := 1;
for i := 0 to length(AdjacentList) - 1 do
begin
if AdjacentList[i].isSource then
begin
grid[columna, fila] := Inttostr(AdjacentList[i].source);
grid[columna + 1, fila] := Inttostr(AdjacentList[i].destiny);
AdjacentList[i].visited := true;
filas := filas + 1;
fila := fila + 1;
end;
end;

bVisited := false;
while not bVisited do
begin
sum := 0;
for i := 1 to filas do
begin
columna := getLastColumn(i);
SetLength(Connector, 0);
GetList(Connector, AdjacentList, StrToInt(grid[columna - 1, i]));
if Length(Connector) <> 0 then
grid[columna, i] := IntToStr(Connector[0].destiny);
if Length(Connector) > 1 then
begin
newRows := CloneRow(i, Connector);
sum := sum + newRows;
end;
end;
filas := filas + sum;
bVisited := AllRowsFinished(filas, DestinoList);
end;


  • ¿Como funciona?
Primero buscamos los orígenes de la lista de adyacentes y los situamos en la grid, por ejemplo:
1 2
1 3
Luego recorremos ésta lista buscando los adyacentes de los últimos nodos, 2 y 3. Por lo tanto encontraremos que el 2 estará conectado con el 4 y el 5 y el 3 solo con el 6. En el momento que tenemos que añadir el valor 4 detrás del 2, si vemos que hay más conexiones, realizamos un clonado de la línea que estamos tratando y insertamos el siguiente valor en la línea clonada:
1 2 4
1 3
1 2 5 (clonado)
Continuamos con la siguiente fila:
1 2 4
1 3 6
1 2 5
Ahora, tenemos que volver a empezar y volver a añadir las conexiones de los nodos posteriores e ir clonando las diferentes filas hasta llegar al final:
1 2 4 8
1 3 6 7 8
1 2 5 8
1 3 6 8
De ésta manera tan fácil conseguimos construir la lista de rutas de transporte, siempre teniendo en cuenta los diferentes orígenes y destinos que nos podemos encontrar.

Podéis descargar la última versión de la aplicación con los ejemplos aquí:
PD : Para la ejecución del algoritmo correcto, hay que ir al menú Citect -> Calc Routes 2 una vez el diagrama está abierto.

Friday, 23 October 2009

Cálculo de rutas de transporte parte I

Después de unos días muy ajetreados y con mucho trabajo (y que no falte) he podido medio acabar la primera aplicación profesional sobre mi framework. Ésta se trata sobre el cálculo de rutas. He utilizado diversos tipos de algoritmos para su recorrido y búsqueda de diferentes caminos para obtener la ruta de un origen a un destino pasando por una serie de nodos. Luego vincularé la búsqueda de éstas rutas con mi anterior aplicación Thundax Genies Scanner y así poder crear mapas mentales de aplicaciones Scada para la búsqueda de caminos automáticos. En que consiste ésta idea?

Como podéis ver en la siguiente imagen:

Disponemos de un origen marcado en verde y un destino marcado en fucsia. Con mi framework he generado el diagrama y aplicado diversas propiedades a las diferentes cajas y conectores. El problema que se presenta, es el de calcular las diferentes rutas que hay de un punto a otro y guardarlas en algún sitio para poder utilizar éstos datos y evitar sobretodo el cálculo manualmente. Poco a poco, os iré explicando la utilizad de ésto y el propósito de su cálculo.

Por lo tanto, si nos fijamos en la imagen existen 2 rutas para ir del origen (1) al destino (19):

Así, calcularíamos:
RUTA1: 1-4-8-11-19 RUTA2: 1-5-9-11-19
  • ¿Dónde está la complicación aquí?
Parece fácil, no?. Pues no lo es. Es un problema bastante complicado a la que empiezan a haber cruzamientos entre nodos. Si aparecen diversas conexiones entre sí, el numero de combinaciones empieza a crecer exponencialmente.

Si observamos mi aplicación, veremos como se calculan las rutas automáticamente:

Ahora un ejemplo más real:

Como podéis comprobar, son los mismos nodos, pero ahora tenemos 7 rutas disponibles!.

El cálculo de sus rutas es el siguiente:

Aún estoy acabando de perfilar el algoritmo y calculando sus diferentes costes para una mejora en su rendimiento. Dentro de poco tendré disponible la versión para búsqueda de rutas con múltiples orígenes y múltiples destinos. Y poder realizar la búsqueda de rutas de diagramas como el siguiente:

Si os fijáis, incluso he creado un tipo de enlace nuevo con la imagen de una cadena. Éste enlace consiste en un enlace de bloqueo donde los nodos están fuertemente vinculados y también se tienen que tener en cuenta a la hora de escribir la ruta de transporte. Ésto significa que si queremos pasar por un nodo de éstos, el otro debe aparecer bloqueado (en términos de fluidos seria cerrar uno de los caminos).
  • ¿Cómo funciona el algoritmo?
El algoritmo que he creado es del tipo backtracking recorriendo los diferentes nodos y marcando las rutas visitadas. Lo que hago es crear una lista de adyacentes marcando la cantidad de conexiones que tiene cada nodo.

Primero calculo los orígenes y destinos en función del color de la caja:


for i := 0 to boxlist.count - 1 do
begin
if boxlist.items[i].properties.fillColor = clLime then
SetArrayValue(OrigenList, StrToInt(boxlist.items[i].properties.getText));
if boxlist.items[i].properties.fillColor = clFuchsia then
SetArrayValue(DestinoList, StrToInt(boxlist.items[i].properties.getText));
end;
Luego, nos centramos en calcular la lista de adyacentes en función del tipo de conexión. Además existe una lista de bloqueos en función del tipo de conector utilizado:


for i := 0 to connectorList.count - 1 do
begin
conn := connectorList.items[i];
pos1 := StrToInt(conn.SourceBox.properties.getText);
pos2 := StrToInt(conn.TargetBox.properties.getText);
if not conn.Line.ClassNameIs('TAbstractDottedDoubleLinkedArrowLine') and
not conn.Line.ClassNameIs('TAbstractSimpleDoubleLinkedArrowLine') then
SetAdjacentValue(AdjacentList, pos1, pos2)
else
begin
SetAdjacentValue(InterlockList, pos1, pos2);
SetAdjacentValue(InterlockList, pos2, pos1);
end;
end;


El siguiente paso es marcar los diferentes nodos con su peso marcado por la cantidad de nodos conectados a un nodo.


for i := 0 to length(AdjacentList)-1 do
begin
AdjacentList[i].weight := getWeight(AdjacentList, AdjacentList[i].source);
if AdjacentList[i].weight = 1 then
AdjacentList[i].unique := true;
end;


Esto nos permite crear un diagrama con la siguiente información:



function TfrmMain.ExamineNodes(firstNode: integer; lastNode: integer; AdjacentList: TAdjacentArray): string;
var
nodeVisited: integer;
res: TArrayInteger;
i: integer;
father: integer;
begin
nodeVisited := firstNode;
father := nodeVisited;
while (nodeVisited <> lastNode) do
begin
res := GetConnections(AdjacentList, nodeVisited);
if length(res) = 0 then exit;
for i := 0 to length(res) - 1 do
begin
nodeVisited := res[i];
if isVisited(AdjacentList, father, nodeVisited) then
begin
visit(AdjacentList, father, nodeVisited);
setRoute(nodeVisited, father);
ExamineNodes(nodeVisited, lastNode, AdjacentList);
end;
end;
end;
end;


Cómo podéis ver el algoritmo es bastante simple pero se puede mejorar bastante. El coste de calculo es O(n2) para visitar todos los nodos. Aquí os dejo la última versión de la aplicación ThundaxBoxManager v2.3.0 build 57 con los diagramas de ejemplo. Para si resolución hay que ir al menú Citect y allí ejecutar la opción Backtracking2.


Friday, 4 September 2009

jabberwacky

Repasando alguna de mis lecturas he encontrado un artículo interesante que habla del test de touring, y de los intentos que se han hecho para intentar pasarlo. El test pretende que el que habla con un interlocutor no sepa si es una persona o una máquina el que está al otro lado. Hay un concurso muy conocido llamado Loebner Prize donde los programas se exponen delante de un jurado e intentan hablar con ellos. Lo más interesante es que aún ninguno de los programas lo ha podido conseguir aún, y aún falta mucho tiempo para que eso pase. Éstos programas basados en AI, disponen de una base de datos de conocimiento, donde almacenan diferentes datos sobre las conversaciones tenidas.

Los chatterbot más conocidos son los siguientes, según ganaron cada año el Loebner Prize:
  • Año Autor ChatterBot
  • 2005 Rollo Carpenter George
  • 2006 Rollo Carpenter Joan
  • 2007 Robert Medeksza Ultra Hal
  • 2008 Fred Roberts Elbot
Además de éstos también me gustaría destacar Cleverbot que es una variante de jabberwacky más divertida.

Aquí os dejo alguna de las conversaciones hecha con éstos individuos, y la verdad es que es bastante entretenido:

JabberWacky:


CleverBot:

Elbot:


Ultra Hal:

Os recomiendo pasar por alguna de éstas webs y tener una pequeña conversación con ellos!.

Thursday, 16 July 2009

Implementando A* pathfinding en Delphi

Como ya sabéis, me encantan los retos y ejercicios de geometría computacional. Pues bien, aquí os traigo una implementación en Delphi de A* pathfinding, que consiste en encontrar y dibujar la ruta más corta entre 2 puntos saltando obstáculos. Para la resolución del pathfinding utilizaremos un algoritmo A* (A star) que es un algoritmo de búsqueda sobre grafos, intentando ir de un nodo principal a uno final con el coste mínimo. Para mi aplicación utilizaré un algoritmo creado por William Cairns donde he hecho pequeñas modificaciones y mejoras para adaptarlo a mis necesidades. Éste algoritmo se utiliza mucho en los videojuegos. Si alguna vez habéis podido leer algún libro sobre videojuegos (os lo recomiendo sobre todo por las estructuras de datos, OpenGL, etc.) nos muestran la manera que tienen para resolver que un jugador del tipo Starcraft , se mueva de un sitio a otro indicado salvando todos los obstáculos.
En la siguiente imagen, sacada de "Programación de videojuegos con SDL", podemos ver un arbol con posibles caminos dentro del mapa del juego:

En la web de policyAlmanac de U.S podemos encontrar un tutorial -> A* pathfinding para principiantes. El cuál nos muestra unas imágenes muy interesantes que me sirven para mi explicación:

Si nos fijamos en la imagen inicial:

Tenemos que ir del punto verde al punto rojo con el mínimo camino posible. Pero si nos fijamos en la imagen podemos ver que tenemos 2 caminos principales posibles, para éso hay que tener en cuenta la distancia Manhattan de manera que calculamos el número total de cuadros movidos horizontalmente y verticalmente para alcanzar el cuadrado destino desde el cuadro actual, sin hacer uso de movimientos diagonales. Al aplicar los diferentes algoritmos, obtenemos el diagrama siguiente con el cálculo realizado y el camino más corto:


Aplicación Thundax A Star PathFinding:

Aquí os dejo mi aplicación, la podéis descargar desde aquí. Por ejemplo, si creamos el ejemplo anterior, obtenemos algo parecido a:

Ahora, podemos poner los puntos donde queramos y los obstáculos que queramos y buscar el camino:


El código fuente de la unidad Astar.pas lo podéis encontrar aquí:




// originally written by William Cairns - http://www.cairnsgames.co.za
// http://www.pascalgamedevelopment.com/forums/profile.php?mode=viewprofile&u=65
// Enchanchements, additional code by Jernej L.
// http://www.gtatools.com
// please note that the path returned is REVERSED.
// Modified by Jordi Coll

unit Astar;

interface

uses
windows, dialogs, sysutils;

type
AstarRec = packed record
point: Tpoint;
weight: integer;
end;

PInspectBlock = function(X, Y, Fx, Fy: integer): integer;

var
Searching, Found: Boolean;
Astack: array of AstarRec;
Source, Goal: Tpoint;
freedom: integer;

CanGo: PInspectBlock;
GRID: array of array of integer;
GridDimensions: Tpoint;
maxval: integer;
patherror: boolean;
Path: array of Tpoint;
closestpoint: AstarRec;
IsClosest: boolean;

Offsets: array[0..7] of
record
DX, DY: Integer;
Cost: Integer;
end =
((DX: 0; DY: - 1; Cost: 10), //90° neighbour cubes
(DX: - 1; DY: 0; Cost: 10),
(DX: + 1; DY: 0; Cost: 10),
(DX: 0; DY: + 1; Cost: 10),
(DX: - 1; DY: - 1; Cost: 14), //45° diagonals
(DX: + 1; DY: - 1; Cost: 14),
(DX: - 1; DY: + 1; Cost: 14),
(DX: + 1; DY: + 1; Cost: 14));

procedure FindPath(const src, dest, Gridsize: Tpoint; const diagonals, pleasefallback: boolean; const grabcallback: PInspectBlock);

implementation

procedure
InspectBlock(X, Y: Integer);
var
I: Integer;
W: Integer;
AX, AY, AW, ABV: Integer;
begin
if (x = Source.x) and (y = Source.y) then
W := 0
else
W := GRID[x, y];
for I := 0 to freedom do
begin
AX := X + Offsets[I].DX;
AY := Y + Offsets[I].DY;
if (AX = Goal.X) and (AY = Goal.Y) then
begin
Found := True;
Exit;
end;

if (AX >= 0) and
(AY >= 0) and
(AX <= GridDimensions.x - 1) and
(AY <= GridDimensions.y - 1) = false then
continue;

if (ax = Source.x) and (ay = Source.y) then
continue;
if GRID[AX, AY] <> 0 then
continue;

ABV := CanGo(AX, AY, X, Y);
AW := W + Offsets[I].Cost + ABV;

if (ABV <> -1) then
begin
if ABV = 0 then
begin
Found := false;
Searching := false;
Exit;
end;

GRID[AX, AY] := AW;
if aw > maxval then
maxval := aw;

if (ABS(Goal.X - AX) + ABS(Goal.Y - AY)) < closestpoint.weight then
begin
closestpoint.point.x := ax;
closestpoint.point.y := ay;
closestpoint.weight := (ABS(Goal.X - AX) + ABS(Goal.Y - AY));
end;

setlength(Astack, length(Astack) + 1);
with Astack[length(Astack) - 1] do
begin
point.x := ax;
point.y := ay;
weight := aw;
end;

end;
end;
end;

procedure Step;
var
I, LC, X, Y: Integer;
begin
if Found then
Exit;
if not Searching then
begin
InspectBlock(Source.X, Source.Y);
Searching := True;
end
else
begin
if high(astack) = -1 then
begin patherror := true;
exit;
end;
LC := 0;
for i := 0 to length(Astack) - 1 do
begin
if astack[i].weight < astack[LC].weight then
LC := i;
end;
X := Astack[LC].point.x;
Y := Astack[LC].point.y;
move(astack[LC + 1], astack[LC], (length(Astack) - 1 - LC) * sizeof(AstarRec));
setlength(Astack, length(Astack) - 1);
InspectBlock(X, Y);
end;
end;

procedure CalcBestPath;
var
lowest: Tpoint;
lowvalue: integer;
finished: boolean;
function findbestprev(pt: Tpoint): Tpoint;
var
i, ax, ay: integer;
begin
for I := 0 to freedom do
begin
AX := pt.X + Offsets[I].DX;
AY := pt.Y + Offsets[I].DY;
if (AX < 0) or
(AY < 0) or
(AX > GridDimensions.x - 1) or
(AY > GridDimensions.y - 1) then
continue;
if (AX = source.X) and (AY = source.Y) then
begin
finished := True;
Exit;
end;
if GRID[AX, AY] > 0 then
begin
if GRID[AX, AY] < lowvalue then
begin
lowvalue := GRID[AX, AY];
lowest.x := ax;
lowest.y := ay;
end;

end;
end;
end;
begin
if Found = false then
exit;
finished := false;
lowvalue := maxint;
lowest := Goal;
repeat
findbestprev(lowest);
if not finished then
begin
setlength(Path, length(path) + 1);
Path[length(path) - 1] := lowest;
end;
until (finished);
end;

procedure LookForPath;
begin
repeat step;
until (found = true) or (patherror = true);
end;

procedure FindPath(const src, dest, Gridsize: Tpoint; const diagonals, pleasefallback: boolean; const grabcallback: PInspectBlock);
begin
Source := src;
Goal := dest;
freedom := 3;
if diagonals then
freedom := 7;

CanGo := grabcallback;
GridDimensions := Gridsize;
Searching := false;
Found := false;
patherror := false;
closestpoint.weight := maxint;
IsClosest := false;
setlength(Astack, 0);
setlength(Path, 0);
setlength(GRID, 0, 0);
setlength(GRID, gridsize.x, gridsize.y);
LookForPath;
if (patherror = true) and (pleasefallback = true) then
begin
Goal := closestpoint.point;
Searching := false;
Found := false;
patherror := false;
closestpoint.weight := maxint;
setlength(GRID, 0, 0);
setlength(GRID, gridsize.x, gridsize.y);
setlength(Path, length(path) + 1);
Path[length(path) - 1] := closestpoint.point;
LookForPath;
CalcBestPath;
IsClosest := true;
end
else if patherror = false then
CalcBestPath;
end;

end.






Una parte de la implementación del funcionamiento y la generación del dibujo con una TStringGrid lo podéis encontrar aquí:




function blocktester(X, Y, Fx, Fy: integer): integer;
begin
result := -1;
with Form1 do
begin
if (board.Cells[X, Y] = '') or (board.Cells[X, Y] = 'A') then
result := ((ABS(finalPos.x - X) + ABS(finalPos.y - Y)) * 3);
end;
end;

procedure TForm1.SearchClick(Sender: TObject);
var
i: integer;
begin
Astar.findpath(cellPos, finalPos, point(board.colcount, board.rowcount), true, true, @blocktester);
for i := 0 to high(astar.path) do
board.Cells[astar.path[i].x, astar.path[i].y] := '·';
if astar.IsClosest then
Statusbar1.Panels[1].text := 'Close path.';
if ((high(astar.path) = -1) and (astar.Found)) then
caption := 'immediatelly path.'
else
Statusbar1.Panels[1].text := 'Direct Path';
if not astar.Found then
Statusbar1.Panels[1].text := 'There is no path !';
end;

procedure TForm1.boardDrawCell(Sender: TObject; ACol, ARow: Integer; Rect: TRect; State: TGridDrawState);
begin
with Sender as TDrawGrid do
begin
if board.cells[acol, arow] = '1' then
Canvas.Brush.Color := shape1.Brush.color
else if board.cells[acol, arow] = '2' then
Canvas.Brush.Color := shape2.Brush.color
else if board.cells[acol, arow] = '-' then
Canvas.Brush.Color := shape3.Brush.color
else if board.cells[acol, arow] = '·' then
Canvas.Brush.Color := clyellow
else if (acol = mousePos.x) and (arow = mousePos.y) then
Canvas.Brush.Color := clwhite;
Canvas.FillRect(Rect);
Canvas.TextOut(rect.left + 12, rect.top + 8, board.cells[acol, arow]);
end;
if (state = [gdSelected]) then
with TStringGrid(Sender), Canvas do
begin
Brush.Color := clwhite;
FillRect(Rect);
TextRect(Rect, Rect.Left + 2, Rect.Top + 2, Cells[aCol, aRow]);
end;
end;


  • Enlaces de interés:
PathFinding Applet 1.

PathFinding Applet 2.