Chapter 2
Chapter 2
with Python
Brandon Rohrer
Chapter 1:
Can't Artificial Intelligence
Already Do That?
Chapter 2:
Keeping Time with Python
About This Project
How to Train Your Robot is a long term side project. I've
been working on it for 20 years, and I don't know if I'll
ever finish it. But I find it deeply satisfying to share
progress as I go, and who knows, maybe someone will
find it useful. This is already the second chapter. I'm
pretty pleased with that, because it means that the
project has a quantifiable momentum.
Onward we go.
Brandon
Boston, USA
October 18, 2022
Keeping Time with Python
Chapter 2
Tail wags were briefly considered as a way to track time, but were
abandoned due to being distractingly adorable and notoriously
unreliable.
I’ve set this up so that if you choose to, you can easily
fetch and run this code yourself. There’s no better way
to get a feel for how things work than to play with them.
Run it, change it, run it again. Break it and fix it. Put
your own spin on it. All of the scripts we walk through
here you can find on GitHub. The code is available at
brandonrohrer.com/httyr2files.
It runs on my machine
I’m going to apologize in advance; I have not taken
steps to guarantee that my code will run for you.
Writing code that will run on any computer is a
gargantuan task. It becomes especially thorny when you
How to Train Your Robot
That said, I’ve tried to stack the deck in our favor by not
getting too fancy. I try not to do anything that requires
the latest version of Python or operating system-specific
behaviors, at least when I can avoid it. I try not to
require the latest, bleeding edge package versions or
recently released features. In fact, if there's anything we
can code up ourselves, I'll avoid relying on third-party
packages altogether. It’s possible that some of this code
won’t run on your computer, but I’ll do what I can to
avoid that.
import time
current_unix_time = time.time()
print(current_unix_time)
00_unix_time.py
$ python3 00_unix_time.py
1662637918.4849603
import time
unix_time = time.time()
utc_time_of_day_seconds = unix_time % seconds_per_day
utc_time_of_day_hours = (
utc_time_of_day_seconds / seconds_per_hour)
local_time_of_day_hours = (
utc_time_of_day_hours + utc_offset_hours)
local_hour = int(local_time_of_day_hours)
01_time_hour.py
is, that the local time here lags UTC by an even five
hours. I also have to know that there is an additional +1
correction since we are currently on daylight savings
time. This is hard-coded into the variable
utc_offset_hours as -4.
$ python3 01_time_hour.py
local hour: 10
import time
import numpy as np
n_iterations = 10000
total_execution_time = 0
for _ in range(n_iterations):
start_time = time.time()
end_time = time.time()
elapsed_time = end_time - start_time
total_execution_time += elapsed_time
average_time = (
total_execution_time / n_iterations)
print(
"Average execution time:",
f"{average_time:.09} seconds")
02_code_timing.py
Keeping Time with Python
$ python3 02_code_timing.py
03_code_timing_distribution.py
How to Train Your Robot
03_code_timing_distribution.py
import time
import numpy as np
n_iterations = 10000
elapsed_times = np.zeros(n_iterations)
for i_iteration in range(n_iterations):
start_time = time.time()
end_time = time.time()
elapsed_time = end_time - start_time
elapsed_times[i_iteration] = elapsed_time
04_time_timing.py
$ python3 04_time_timing.py
$ python3 05_monotonic_timing.py
import time
import numpy as np
n_iterations = 1000
sleep_duration = .01
sleep_times = np.zeros(n_iterations)
for i_iteration in range(n_iterations):
start_time = time.monotonic()
time.sleep(sleep_duration)
end_time = time.monotonic()
sleep_time = end_time - start_time
sleep_times[i_iteration] = sleep_time
average_time = np.mean(sleep_times)
print(
"Average sleep time:",
f"{average_time:.09} seconds")
06_sleep_timing.py
$ python3 06_sleep_timing.py
07_sleep_timing_distribution.py
The first thing that jumps out is that all of the measured
sleep times are well above the requested 10 ms. This is a
feature of how sleep() is implemented in Python. It’s
guaranteed to sleep at least the time specified. But as we
can see here, it can go over by quite a bit. The median of
this distribution is pretty close to 10.4 ms, a full 4%
higher than the requested time.
How to Train Your Robot
When we dig a little more and see how this varies for
different durations of sleep, even more weirdness
emerges. This plot shows the median sleep time (not the
average) across many sleep durations.
08_sleep_overhead.py
Keeping Time with Python
def sleep(duration):
start = time.monotonic()
end = start + duration
while time.monotonic() < end:
pass
09_sleep_precise.py
09_sleep_precise.py
import time
clock_freq_Hz = 2
clock_period = 1 / float(clock_freq_Hz)
test_duration = 10 # seconds
n_iterations = int(clock_freq_Hz * test_duration)
t0 = time.monotonic()
last_completed = t0
completed = time.monotonic()
duration = completed - last_completed
print(duration * 1000)
last_completed = completed
10_metronome.py
$ python3 10_metronome.py
500.85251999553293
499.75492199882865
500.24256703909487
499.7710359748453
499.94934001006186
500.05020102253184
500.1911209546961
499.7375410166569
500.29632402583957
499.922044982668
499.76122297812253
500.0822790316306
500.2248259843327
11_metronome_errors.py
$ python3 11_metronome_errors.py
the last one left off. Even if the ticks are all consistent,
we're stuck with the fact that sleep() tends to run long.
This version of the metronome creates tick durations
that all fall above zero.
11_metronome_errors.py
$ python3 11_metronome_errors.py
A Pacemaker
At last, we get to put the pieces together to make a
rhythmic timekeeper for our code, a pacemaker that
helps it run not too fast and not too slow. When we have
multiple processes running in parallel, each overseeing
a different part of the robot's activities, it will be quite
useful to know that each is running according to its own
Keeping Time with Python
import time
clock_freq_Hz = 4
clock_period = 1 / float(clock_freq_Hz)
test_duration = 10 # seconds
n_iterations = int(clock_freq_Hz * test_duration)
t0 = time.monotonic()
last_completed = t0
elapsed = time.monotonic() - t0
seconds = int(elapsed)
milliseconds = int(elapsed * 1000) % 1000
print(f" {seconds}:{milliseconds:03}")
completed = time.monotonic()
duration = completed - last_completed
last_completed = completed
12_pacemaker.py
Keeping Time with Python
$ python3 12_pacemaker.py
0:000
0:250
0:500
0:750
1:000
1:250
1:500
1:750
2:000
2:250
2:500
2:750
3:000
How to Train Your Robot
$ python3 12_pacemaker.py
...
8:267
8:518
8:768
9:019
9:269
9:519
9:770
What's next?
Time and pacemakers give us a solid base to start
working with more than one process. Multi-process
development is an exercise in choreography, and we
now have the synchronization tools we need to keep our
dancers from kicking each other in the head.
Recap
You can count the cycles of any regular phenomenon to
keep time. Physics is very helpful here.
time.sleep = True
How to Train Your Robot
Resources
1. There's an online Unix time converter that I refer to often for quick
conversions to and from.
https://www.epochconverter.com/
2. My computer's specifications
Memory: 15.4 GiB
Processor: Intel® Core™ i7-8650U CPU @ 1.90GHz × 8
Graphics: Mesa Intel® UHD Graphics 620 (KBL GT2)
Disk Capacity: 512.1 GB
OS: Ubuntu 20.04.5 LTS, 64 bit
Lenovo Thinkpad T480 that I call Loki
Purchased September 2018
4. If you are doing the time zone lookup yourself, there's a slick
Wikipedia reference for that.
https://en.wikipedia.org/wiki/List_of_UTC_offsets