Posts about python


YCanyons Of Mars


Canyons Of Mars

"Slow and steady wins the race" - Not sure about that, but at least we can go for finishing the race...

Following Right Wall

Similarly as in previous years, this brought plenty of mathematics. One was to calculate amount rover needs to steer in order to avoid the wall. Math problem is really as following:

  • if we know how much rover traverses each 'tick' (main control loop of "Canyons of Mars" is running at 10Hz),
  • if we know what is angle of rover towards the wall

we would like to calculate what is the distance of a point from rover we want it to steer around.

Oh as pictures tell thousands words:

Math Problem

Here we know what arc size is (AB), what distance to wall is (d), angle to the wall (𝝰), we don't know what Θ is but really need to calculate r.

It was interesting seeing how our A level students tackle the problem (coming up with alternate solution and in half the time than me) and how our GCC students do the same.

Anyway - one way to solve it is given on this picture:

Math Solution

Arc length is r * Θ and using Euclidean geometry we can prove from above picture that Θ is really same as 𝝰. From there it is easy:

length of arch = r * Θ = r * 𝝰 => r = length of arc / 𝝰

And length of arch is really speed our rover is travelling.

Now by the combing distance of point we want rover to steer and angle we want rover to deflect from the wall (if it is too close or get closer if it is too far) we can get really smooth auto correction as what you can see in the video...

YPython Comprehensions


Python Comprehensions

Wev've had many posts that were mainly pictures; let's have one with some code!

Over last couple of months I've got impression that some people do not regard Python as good, modern language (well it has some of the history to make some people stop and think). As any language it has its good and bad sides along with where it can be used as a really good choice and where maybe not.

Python is quite a high level programming language which had its object orientation added relatively late and some of it reflect in somewhat clumsy syntax. But, there are some other sides of Python that put is at par with other popular languages like Java/C#, Ruby, JavaScript and such. Some of it's syntax sugar is making it even better... And that's what we'll look into here: list and dictionary comprehensions.

The problem

Our code is separated in many small processes which acquire data from sensors, inputs and such, process them and send them by MQTT messages to high level controllers that make decisions and send them to lower level controllers that know how to command wheels, and then from there to wheel controllers that steer and drive them. For simplicity originally we adopted sending messages in plain ASCII - human readable form so debug can be really easy. For instance shell command:

mosquitto_sub -v -t wheels/#

would show required positions and speeds of wheels,

mosquitto_sub -v -t move/#

would show what was required of rover to do - rotate, drive in straight line at angle or steer around some point at some distance form the rover.

Distance sensors do provide similar messages that look like this:

0:1201 45:872 90:563 135:797 180:1622 225:433 270:319 315:478

Solution

To transform it from string like that to a dictionary we can use simple code like this:

distances = {}
for entry in message.split(" "):
    split = entry.split(":")
    distances[split[0]] = split[1]

But with list comprehensions we can do better:

distances = {int(k):float(v) for k,v in [entry.split(":") for entry in message.split(" ")]}

Let's see what it really does:

message.split(" ")

is making a list of strings that are separated by given separator (an empty string here " ") and result is:

['0:1201', '45:872', '90:563', '135:797', '180:1622', '225:433', '270:319', '315:478']

Next is to take list of strings that are separated by ':' and make a list of two element lists:

[entry.split(":") for entry in message.split(" ")]

Result of it is:

[['0', '1201'], ['45', '872'], ['90', '563'], ['135', '797'], ['180', '1622'], ['225', '433'], ['270', '319'], ['315', '478']]

That's called a list comprehension. And in there we can filter elements and/or transform them on the fly. For instance, if we have a list of strings that represent numbers like this:

['1', '2', '3', '4', '5']

we can transform it to list of numbers:

[int(n) for n in ['1', '2', '3', '4', '5']]

where result is:

[1, 2, 3, 4, 5]

Also, we can transform list of numbers so we exclude all greater than 2:

[n for n in [1, 2, 3, 4, 5] if n > 2]

where result is:

[3, 4, 5]

Now back to our problem. We've produced list of lists of two elements. Now we can use those two elements and produce a dictionary using first element as key and second as value:

{int(k):float(v) for k,v in [entry.split(":") for entry in message.split(" ")]}

Also, just before using key we will transform it to int and value to float.

Result is:

{0: 1201.0, 45: 872.0, 90: 563.0, 135: 797.0, 180: 1622.0, 225: 433.0, 270: 319.0, 315: 478.0}

But we had another problem to deal with: sometimes we have data passed to us at various parts of the code as just a list without keys given - where keys are understood. For instance logging: we have record containing values that were previously logged (something like [1550348686.8102736,b'fr',b'SK',1.0,32,0.0,0.0,0.03261876106262207,0.0,0.0,0.0,0.0]) and list of fields that were used to create that record. Each field is an python class that has toString(value) and fromString(value)methods. First is to produce csv file and second to create value from csv's column.

So, to create CSV file (which we can use later on for analysing what went wrong) we would like to create a string out of record and back to create a record back of string. But our data and data definitions are in separate arrays. Ordinary solution people would normally go with (and including me before I got fascinated with comprehensions) would go something like this:

result = {}
for i in range(len(logger_def.fields)):
    result[field.fields[i].name] = field.fields[i].fromString(record[i])

But now with comprehensions we can do better. Also, in order to put two arrays together we can use method 'zip' which alternates elements from one and another array. For instance:

zip([1, 2, 3, 4], ['a', 'b', 'c', 'd'])

would give:

[(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]

(not really - would we need to 'convert' iterator to a list with something like this

[e for e in zip([1, 2, 3, 4], ['a', 'b', 'c', 'd'])]

but final result is the same)

Now we can combine these two together:

result = {f.name: f.fromString(v) for f, v in zip(logger_def.fields, line.split(","))}

(try simple example from above:

{k: v for k, v in zip([1, 2, 3, 4], ['a', 'b', 'c', 'd'])}

)

Conclusion

Those are just one aspect of Python that make it interesting and fun to work with. There are much more to the language...