Importância de entender um pouco de algoritmos

Você também desenvolve plugins para o AutoCAD e não é um programador experiente?
Hoje vou falar um pouco sobre a importância de aprender sobre algoritmos e desempenho.

Se você é um programador experiente, já deve ter ouvido falar da análise de complexidade de um algoritmo.

Se não é, em linhas gerais significa comparar dois ou mais algoritmos com a mesma finalidade e saber dizer qual é mais eficiente. Não vou me ater demais na teoria, então sugiro ver este documento.

Vamos ver um exemplo prático.

Na análise de colisão de objetos no BIM, esse processo pode ser bem demorado se você não escreveu um algoritmo eficiente. NO SOLIDOS isso é feito pelo comando SINTERF, que verifica se dois dispositivos colidem. Quando isso acontece, você tem de alterar a geometria da rede (afundar a rede, reposicionar o poço de visita).

Se tivéssemos 100 dispositivos, quantas colisões teríamos de testar?

Um programador inexperiente talvez dissesse: “Temos de testar todos contra todos”. Isso significa 100 x 100, ou 10.000 interações (aproximadamente, veja o documento!!!). Na análise de complexidade diríamos que este é um algoritmo de complexidade O(n^2), o proporcional ao quadrado do número de itens a testar

Um programador um pouco mais esperto diria: “Se testei A contra B, não preciso testar B contra A”, então, teríamos cerca de n*(n-1)/2 iterações, o que já é bem menos que a primeira opção. Neste caso, 4950.

Mas veja, no caso especifico do SOLIDOS, estamos num loteamento, ou cidade. Qual o tamanho médio de um dispositivo? Um PV, talvez 1,50 x 1,50 m. Um tubo, talvez tenha em média uns 50 a 100 metros. Isso significa que testar a colisão de um PV com outro a mais de 3m de distância é completamente supérfluo, percebe? E o tubo? Bem, esse complica um pouco, pois ele costuma ser muito comprido em relação a largura, então temos de levar isso em conta.

Na API de programação do AutoCAD, para verificar se 2 sólidos colidem, podemos fazer:

interfere = solidoA.CheckInterference(solidoB)

Só que essa operação da API, é um tanto lenta, ainda mais para sólidos “complexos”

Se esse teste custasse 200 milissegundos em média e tivéssemos 100 dispositivos:

  1. testar todos contra todos, ignorando que solidoA.CheckInterference(solidoB) é igual a solidoB.CheckInterference(solidoA) (complexidade = n^2): 10.000 testes ou 2000s = 33 minutos.
  2. testando sem repetir (complexidade = n*(n-1)/2), 4950 iterações, ou 16 minutos, menos ruim, mas ainda péssimo.

Vamos melhorar isso.

Primeiro, não precisa usar o método CheckInterference sempre. Podemos testar o GeometryExtents do sólido. O GeometryExtents é o menor paralelepípedo alinhado com os sistema de coordenadas WCS (World Coordinate System) do AutoCAD, que contem o sólido. Com ele podemos fazer:

Function VerificarInterferencia(solidoA As Solid3d, solidoB As Solid3d) As Boolean
   Dim geo1 As Extents3d = solidoA.GeometricExtents
   Dim geo2 As Extents3d = solidoB.GeometricExtents
   
   If geo2.MinPoint.X > geo1.MaxPoint.X OrElse 
      geo2.MaxPoint.X < geo1.MinPoint.X OrElse 
      geo2.MinPoint.Y > geo1.MaxPoint.Y OrElse 
      geo2.MaxPoint.Y < geo1.MinPoint.Y OrElse 
      geo2.MinPoint.Z > geo1.MaxPoint.Z OrElse 
      geo2.MaxPoint.Z < geo1.MinPoint.Z Then Return False
   
   Return solidoA.CheckInterference(solidoB)

End Function

Neste código, só testamos a colisão de sólidos se os paralelepípedos (GeometricExtents) colidem. Note que o teste no paralelepípedo é matemático, o que demora MUITO menos

Lembra que eu falei que testar dois sólidos muito distantes é supérfluo? E se classificássemos a nossa lista de dispositivos de tal forma que agrupamos aqueles mais próximos entre si, e testamos a colisão nesses agrupamentos ?

Primeiro, como classificar esses dispositivos em grupos de proximidade?

Bom, temos o “GeometryExtents” , podemos usar ele.

Primeiro imagine uma malha, com células cuja largura é condizente com o tamanho médio dos dispositivos, por exemplo: 50 a 100 metros. Veja um algoritimo que calcula a largura máxima dessas células e agrupa esses sólidos:

                Dim dicExtents As New Dictionary(Of Dispositivo, Extents3d)
                Dim dicMalha As New Dictionary(Of String, List(Of Dispositivo))
                Dim larg As Double = 0

                'calcula a maior largura
                For Each disp As Dispositivo In Dispositivos
                    Dim dispExtents = disp.GeometricExtents

                    larg = Math.Max(larg, dispExtents.MaxPoint.X - dispExtents.MinPoint.X)
                    larg = Math.Max(larg, dispExtents.MaxPoint.Y - dispExtents.MinPoint.Y)

                    dic(disp) = dispExtents
                Next

                larg = Math.Ceiling(larg)

                'classifica em função dessa largura
                For Each kv In dicExtents 
                    Dim dispExtents = kv.Value

                    Dim xMin = Math.Floor(dispExtents.MinPoint.X / larg) * larg
                    Dim xMax = Math.Floor(dispExtents.MaxPoint.X / larg) * larg

                    Dim yMin = Math.Floor(dispExtents.MinPoint.Y / larg) * larg
                    Dim yMax = Math.Floor(dispExtents.MaxPoint.Y / larg) * larg

                    For x = xMin To xMax Step larg
                        For y = yMin To yMax Step larg
                            Dim hash = CInt(x) & "," & CInt(y)
                            Dim lista As List(Of SolDevice)

                            If dicMalha.ContainsKey(hash) Then
                                lista = dicMalha(hash)
                            Else
                                lista = New List(Of SolDevice)
                                dicMalha(hash) = lista
                            End If

                            lista.Add(kv.Key)
                        Next
                    Next
                Next

O primeiro looping, percorre a lista de dispositivos, armazenando no dicionário “dicExtents” o paralelepípedo que circunscreve o dispositivo e calcula a maior largura entre eles.

O segundo looping, agrupa o sólido nas células que ele “pode” ter colisões. Note que usei um tipo de função “hash” que permite que mais de um dispositivo tenha o mesmo “hash”. O que é um hash?

A hash (Resumo) é qualquer algoritmo que mapeie dados grandes e de tamanho variável para pequenos dados de tamanho fixo. Por esse motivo, as funções Hash são conhecidas por resumirem o dado (veja esta matéria).

Quando faço:

Dim xMin = Math.Floor(dispExtents.MinPoint.X / larg) * larg

Estou pegando o X do canto inferior esquerdo do paralelepípedo e “simplificando ele para o “X” do vértice inferior esquerdo da célula mais próxima. Repetindo o processo para “Y”, faço uma “hash” que é a coordenada do vértice inferior esquerdo da célula. Acompanhou até aí?

Fazendo o mesmo com o canto superior direito e iterando entre as células deste intervalo, tenho as células onde possivelmente o sólido vai colidir. Note, poderia incluir o Z, em vez de uma malha bidimensional, teria uma tridimensional.

Com a “hash”, coloco o dispositivo na lista (o agrupamento) daquela célula.

Note, que se calculássemos a complexidade deste algoritimo, teríamos algo como:

  1. Complexidade linear para calcular o “GeometryExtents” (ou seja, diretamente proporcional ao número de dispositivos)
  2. Complexidade linear para classificar na os sólidos

Agora, Para cada célula da malha que efetivamente tem sólidos, calculamos as colisões fazendo o teste “todos contra todos” mais “esperto”, que sabem que:

solidoA.CheckInterference(solidoB) é igual a solidoB.CheckInterference(solidoA)

Note:

No pior dos piores casos, teremos apenas uma célula e cairemos na complexidade n * (n-1) / 2 iterações

É seguro dizer que, no nosso exemplo (colisão de dispositivos numa cidade ou loteamento), teremos mais de 1 célula e mesmo que tenhamos dispositivos em todas as células, cada uma delas terá um número reduzido de dispositivos. Então teremos um somatório de iterações que realmente demoram (CheckInterference), muito reduzido:

Número de iterações pesadas = (número de células) * (número médio de dispositivos)  * ( (número médio de dispositivos) –  1 ) / 2

Na pasta suporte do SOLIDOS,  tem um DWG, com 107 dispositivos. Ao rodarmos o comando que verifica as colisões (SINTERF), teremos as seguintes células (em vermelho) com largura de cerca de 130 metros (o maior tubo):

Quadrantes

 

Esta malha tem 56 células, a que tem mais dispositivos tem 10 dispositivos. Em média temos 3 em cada célula, então teremos aproximadamente  321 (calculando 56*3*(3-1)/2) iterações pesadas (aquelas que demoram), o que é bem próximo do numero real, 311 neste DWG. Se cada uma demorar 200 milissegundos, teremos algo em torno de 1 minuto, ou 33 VEZES menos que o algoritmo do programador totalmente iniciante e 16  VEZES menos que o algoritimo do programador “intermediário”.

Preciso dizer que a escala de tempo do AutoCAD algo como “4 segundos da vida real” igual a “4 HORAS no AutoCAD” (hehehe)?

Se você chegou até aqui, dá uma conferida no meu livro!!!

Curso de programação pra Autodesk Civil 3D
Curso de programação pra Autodesk Civil 3D

 

Deixe um comentário

Carrinho de compras
Rolar para cima