11/27/2014 Circle tangential line, A* path finder, and AVL tree

It is Thanksgiving break!  My teaching duty for this semester is almost over.  All I have to do is grading final presentations and last exam.  I am able to relax and work on my hobby projects including YSFLIGHT, Scenery Editor, and Polygon Crest

I thought I start with fixing bunch of reported bugs (Thank you for the bug reports!), but before that I felt it's a shame that computer-controlled airplanes wander into the range of anti-air batteries and shut down.  It's a good idea to write a simple path-planning program so that computer-controlled airplanes can avoid obstacles and anti-air batteries.

I didn't intend to do anything sophisticated.  All I need to do is find a course that avoid all circles that define firing ranges of anti-air batteries.  For example, if I calculate tangential line that passes through the current location of the airplane, and the firing-range circle and keep the airplane outside of the tangential lines, it's done.

I was sure I have written such a function before.  I looked for the functions in my class library, but turned out I never put it in my collection of the reusable functions.  I always add a function or a class to my YS-Class library whenever I write a piece of code that relies only on C/C++ standard.  But, I didn't find such a function.   I wrote one, and added to my class library.

I could easily calculate a threat-avoiding path when there is only one firing-range circle.  I needed to expand it for multiple-circles and in case the airplane is already inside one of the circles.  Which turned out not too trivial.  If I don't care the computational time, I can run an exhaustive search.  It will finish in reasonable computational time, considering the number of anti-air objects of YSFLIGHT.  However, if I need to do it fraction of 1/60 seconds, so that the calculation does not substantially slow down the frame rate, an exhaustive search is not an option.  It does not have to be 100% optimal path.  As long as it finds a reasonable path, it's good enough.  I tried to think something that is easy enough to implement and can come up with a reasonable threat-avoiding path.  But, I couldn't find one.

 I decided to study a path-finding algorithm and implement.  I didn't know much about path-finding, but one of my students was talking about an algorithm called A*.  She was able to implement the algorithm by herself.  If a student can implement it, I should be able to do it quickly.  What's good about writing YSFLIGHT is I end up with learning new algorithms and techniques.

I have implemented an algorithm described in Wikipedia almost as is.  But, I faced one problem, which was finding a node from openSet that has the minimum cost-function value f.  In each step, openSet is updated.  The number of elements in the set grows or shrinks, or the cost-function value of an element in the set may change.  What I did was used a simple array, and kept the element sorted in the descending order.  Cost for finding the location to add a new element is O(logN).  Finding the minimum-f element is O(1).  However, when I insert a new element, I need to shift the elements in the array, which cost O(N).  I need to do N times.  So, the overall computational complexity becomes O(N2), where N is the number of elements in the openSet.  Which will be ok if the number of elements N is small.  But, is it small?

Test program screen shot.  Green: found path.
Black: obstacle.  Cyan: closedSet.  Magenta: openSet.

I implemented and experimented.  In conclusion, the elements in the openSet grows to the number of elements on the boundary of the area where the algorithm has already visited.  It makes sense because the openSet is where the algorithm would search for the next step.  But, it can grow more than insignificant number.

Finding a path from lower left to upper right of a 400x300 lattice took 120ms on my 2008 MacBook Pro.  I won't need that much resolution for the threat-avoiding path for an airplane because the airplane cannot turn quickly anyway.  But, if I want to use it for taxiing-path calculation, I would use even denser resolution.  It's not fast enough.

It must be how I store the openSet.  It needs to be always sorted and needs to be able to find the minimum-f node quickly.  For this kind of purpose, I have been interested in implementing a kind of self-balancing binary tree.  Actually, I needed a similar data structure many times before, but always I found an alternative way of solving the problem.  But, this time, it is a good opportunity to learn and implement one, then I can use it for the future projects.

There seems to be several different kinds of self-balancing binary trees.  I remember one of the students working on the AVL tree for his data-structure course assignment.  If a student could implement it in a week, I should be able to do it in an hour or so.

Why not finding a code from the Internet?  I wouldn't learn anything if I randomly cut & pates someone else's code.

I just implemented the algorithm shown in Wikipedia as is.  My honest feeling was "Is that it?  Is this simple algorithm really maintains the AVL tree structure?"  And, it was, apparently.  I wrote various tests to check if the tree is balanced as advertised, if the balances are calculated correctly, if the elements are stored in the correct order, etc.  I also tested by randomly inserting and deleting nodes.  It worked!

Then I tested the speed by inserting 2 million nodes, which took 9 seconds on my 2008 MacBook Pro.  Slow.  I was expecting it to be much faster.  At that moment, the code had redundant checks that were not supposed to be necessary if the algorithm was implemented correctly.  I could drop those checks.  Nonetheless, there didn't seem to be too much room for speeding up because the algorithm was so simple.

After thinking about an hour for why it was that slow in Dunkin Donuts in Oakland, I realized that I forgot adding the optimization option (/O2) for the compiler!  That was dumb.

It was a templated class.  The compiler was supposed to make simple functions inline, but since I didn't put /O2 option, none of the functions were inline-expanded.  With /O2 option, adding 2 million nodes took less than 3 seconds, almost 2.  Not too bad.  I increased the node count, and inserting 10 million nodes took 24 seconds.  It is supposed to grow by the factor of NlogN.  So, it makes sense.  After dropping redundant checks, the time was reduced to 19 seconds.  I feel that I can further reduce unnecessary balance-calculations, and may be able to gain another 10% or so, but it is practically fast enough.

I also tested A* code with /O2 option, and it took 60 to 80 milliseconds.  I didn't see four-times acceleration with the /O2 option.

But, it was supposed to become much faster by replacing the sorted array with the AVL tree.  But, it didn't happen.  After using the AVL tree for the openSet, it was still 60 to 80 milliseconds.  Looked like the overhead was in somewhere else.  My guess was not right.

Looks like I need to look into the code and speed it up before incorporating with YSFLIGHT.  But, when it is working, it can be used not just for avoiding anti-air threats, but also for avoiding terrain, and auto-calculating taxiing paths (without specifying taxiing path in the Scenery Editor!).  But, maybe not in the next release.

So, I initially tried to do it with circle-tangential calculation, but it was not that straight forward.  I ended up implementing A* path finding algorithm, but it was not as fast as I wanted, and I tried to make it faster by AVL tree, only to find that A* was running so slow because I forgot /O2 option.  And, AVL tree didn't help much.  That was what I was doing over the last weekend.

After writing this much, I realized that if I combine overlapping firing-range circles and make it a set of non-overlapping circles, it is almost trivial to find a path, by the way.

Comments are welcome.  Send E-Mail to: 


This page is link free, but please link to http://www.ysflight.com.  Please use one of following banners.