YouTube channel, College Humor, has a game show series called 'Um, Actually' - a game of nerdy corrections, and general nerd trivia.
One of the 'shiny questions' involves putting things is order, for example, putting space ships or fictional creatures in order by size.
The way the scoring works (as far as I can tell) is you get one point for each item you put in the right 'position'. So if you guess (1,3,2,4), then you would get 2 points for getting 1 and 4 in the correct position.
The problem was, in the creatures game, there was this one creature which looked like a single-celled organism, but was actually the size of a galaxy (or something), making it the largest.
So suppose you get everything else in the right order, but you fell for the trap and put this surprisingly massive organism as smallest e.g. (2,3,4,1)
In that case, everything is in the wrong position, so no points. But I would argue since all but one are in the right order you should only lose one point.
The FineBros channel does a similar game - e.g. put the top 10 most liked videos of 2019 in order. Under their scoring system, you get 2 points if an entry is in the right position, and 1 point if it's off by one. So (1,3,2,4) would be worth 6 points and (2,3,4,1) would be worth 3 points
This is slightly better, but you still have a case where you can lose significant points for getting just one or two entries out of place. For example, if you had (3,4,5,6,7,1,2), then you would get 0 points, even tho 5 of 7 are in the right order.
So, can we come up with a better scoring system?
Levenshtein Distance
The first things that comes to mind is Levenshtein or 'minimum edit' distance.
This measures the distance between two strings (words) based on the minimum number of edits to get from one to the other. In this context, an edit is a single character addition (cats -> chats), deletion (cats -> cat), or substitution (cats -> bats)
Now, this doesn't seem quite relevant to our problem; we're not adding, deleting, or substituting, we're swapping. For Levenshtein, a swap would be considered a deletion + an addition (or 2 substitutions) e.g. cats -> cas -> cast
We could calculate the Levenshtein distance and just divide it by 2 - effectively treat delete+add (2 edits) as equivalent to a swap (1 edit). Or else, we can just take the general principle of finding the minimum number of swaps required to get from the guess solution to the correct order.
As for the actual score, we can take the number of entries in the list (N) minus the minimum number of swaps.
So for our original examples, (1,3,2,4) is worth 3, and (2,3,4,1) is also worth 3.
In the latter case, 1 wasn't 'swapped' with an actual element, but you could think of it as a swap with an implied 'null' element - (..,null,1,2,3,4,null,..) -> (..,null,null,2,3,4,1,null,...)
Dynamic Programming
Dynamic programming is an approach to solving a certain class of problem which would take a ridiculous amount of time to solve by brute force. With dynamic programming, these problems can be solved in a more reasonable amount of time by breaking them down and solving recursively.
One of the classic problem in dynamic programming is - find the longest increasing sub-sequence in a list of numbers?
For example, in (2,3,1,7,4,9,5,8) the longest increasing sub-sequence would be (2,3,4,5,8). So in this case, we might say the score is 5/8
Going back to our other examples, (1,3,2,4) would have sub-sequence (1,2,4) or (1,3,4), worth 3 in either case. And (2,3,4,1) would be (2,3,4) which is also worth 3
Weighted Error
This is all well and good, but doesn't it feel like (2,3,4,1) is 'more wrong' than (1,3,2,4). Each has one element out of order, but in the former that one element if further from where it's 'supposed' to be.
One correction might be to calculate the error as sum[abs(x-x')] where x is the position of an element, and x' is where it's supposed to be. So (1,3,2,4) would be (abs(1-1) + abs(3-2) + abs(2-3) + abs(4-4)) = 2
However, this is flawed. For the (2,3,4,1) example, the error comes out at 6. Once again, we're being penalised for all the elements that are in the right order but off-by-one
So a slight modification would be to only calculate the error for those elements which are out of place - that is, find the longest sub-sequence, and then calculate the error for all elements which don't belong to that sub-sequence.
For (1,3,2,4) the error would be 1 and for (2,3,4,1) it would be 3
But how do we get from the error to the actual score?
We can start by calculating the maximum error, then subtracting the calculated error from it. So what's the maximum error?
The most wrong you can be would be to get everything in the wrong order - i.e. all elements reversed. So if we have N elements, the error would be
abs(1 - N) + abs(2 - N-1) + ... + abs(N -1) = (N - 1) + (N - 3) + ... + (N - 1) = 4 * (N - 2)
Actually, there's a subtle flaw in this reasoning - even if all the elements are reversed, there is technically a longest sub-sequence of 1, so one of the entries shouldn't count towards the maximum error. The element we chose as the 1 correct one will affect the maximum error. If we chose the first element in the sequence, the max error is reduced by N-1, whereas if we chose one from the middle the max error is reduced by 1 or 0 (depending if there is an odd or even number of elements)
For simplicity, we'll just assume the middle entry and say max error adjustment is 0
So going back once more to our original examples, (1,3,2,4) has a score of 8 - 1 = 7, and (2,3,4,1) has a score of 8 - 3 = 5, making the latter 'more wrong' as desired.
Conclusion
I'm not sure if there was a point to all this. I don't think this is useful outside of scoring this particular kind of game.
It might be interesting to run an neural net or genetic algorithm or similar using this as the score function. I'd be interested to see how a neural net performed at sorting, in terms of performance and correctness.
But anyway,
Chris.
Showing posts with label everyday maths. Show all posts
Showing posts with label everyday maths. Show all posts
Monday, December 30, 2019
Put the following in order
Labels:
algorithm,
everyday maths,
games,
maths,
over-thinking,
problem solving,
puzzles,
random,
scoring,
sorting
Sunday, July 31, 2016
Barber Queue
Time was when I needed a hair cut, I'd go at noon on a weekday, when the barber's is typically empty. One of the perks of being unemployed.
But these days I have to get my haircuts on Saturdays, before noon. Which is bad enough in itself - ideally I'd never see Saturday mornings at all. And to make matters worse, Saturday morning is also when the barbers is at its busiest.
Hence, I found myself sat in the waiter area of a barbershop for the best part of an hour. But this got me thinking about how queuing works at a barbers.
First In Who's Next?
At its core, the barber's queue is just a first-in first-out (FIFO) queue. But it has two interesting features:
1) The queue 'structure' is unordered
In general the queue 'structure' will be a waiting area with a bunch of seats. When someone new joins the queue, they're free to sit wherever. In fact, odds are, they'll pick a seat in a similar way to how men choose urinals - attempting to maximise personal space.
But the key point is, if someone were to just look at the queue, they wouldn't be able to tell who was next.
2) Each member of the queue knows whether or not they're next
Each member of the queue probably doesn't know who exactly is next, but they do know (with reasonable certainty) whether or not it's them.
The way this works is relatively simple - when you join the queue, you're aware of who was there when you arrived (and of anyone who arrives after you). So when all the people who were there ahead of you have gone, you know that you're next.
O(M G)
Okay, lets break out some Python (2.7)
Just for fun, let's say that the capacity of the queue is fixed - i.e. the waiting area has a fixed number of seats (though in practice, people are free to stand, as I was forced to).
Here, we're picking a 'seat' by iterate over the queue looking for the first empty slot (with a value of None). We could implement any seat picking strategy we fancy, this is just the easiest.
Once we find an empty seat, we create a new 'Member' object for the item, with position set to the current length of the queue, then increment the queue length. Also, if the queue has no empty slots, we raise an exception.
Of course, this isn't how things work in practice. The barber doesn't go to each person and say "are you next?", "how about you?". They simply say "who's next?", and the person who believes they are next steps forward. Though arguably, that's just equivalent to asking every member concurrently. But let's not complicate things.
So there we have it. Of course, this type of queue isn't really useful from a programming perspective.
All of insertion, deletion, and lookup are worst-case \(O(N)\), where N is the queue's capacity (not the number of people in the queue). Which is pretty much worse than all other types of queue.
Why do items keep disappearing from my queue?
Okay, let's move away from computer-sciencey queues. There are certain behaviours in real-world queues that don't apply or wouldn't make sense to programmatic queues.
For one, as I alluded to earlier, the capacity of the queue is not enforced - there's room for overflow, even if it means people have to stand.
But on the other hand, when a place does get that full, people are less likely to stick around.
In particular, we have two situations:
1) There's a non-zero probability that a person will not stick around if the queue is full or close to full. This probability will tend to be related to the length of the queue when that person arrives
\[p(not join) \sim f(capacity, length)\]
For example,
\[p(not join) = A\left(\frac{length}{capacity}\right) - C\]
where A and C are some constants relating to how likely the person is to stick around if the queue is 'full', and at what point they consider the place to be 'too full'.
2) There's a non-zero probability that a person already in the queue will leave before they're served. This probability will typically depend on how long the person has been waiting, and how many people are still ahead of them
\[p(leave) \sim g(wait, position)\]
It's interesting because as time passes, wait increases, but position decreases. So how the probability evolves depends on how those factors balance against one another.
In particular, the probability evolution will likely depend on how each particular person responds to the sunk-cost fallacy - i.e. are they the sort to think "well, I've waited this long, I might as well see it through to the end", or do they think "this is taking too long, I've got better things to do with my time"?
For the sake of arguing, lets go with an exponential function for the general form.
For a sunk-cost person we might have
\[p(leave) = B \cdot \exp\left(-d\cdot \frac{wait}{position}\right)\]
This is a function where p goes to zero as position goes to zero or wait goes to infinity (B and d are some arbitrary constants).
Whereas for a non-sunk-cost person we might have
\[p(leave) = B \cdot \exp\left(-\frac{d}{wait \cdot position}\right)\]
This is a function where p goes to zero as position goes to zero, but goes to one as wait goes to infinity.
This gives us an interesting graph
Because 'position' is discrete you get this nice step function, with intervals of the probability steadily rising, then suddenly dropping. We can also see that there's a point at which probability of leaving is maximum, around the time you're in the middle of the queue. Which seems plausible.
We also have situations where a person will leave and come back later. But since, when they come back, they have to join the back of the queue, they're mathematically indistinguishable from a someone arriving for the first time.
One other complicating situation is people in groups. For example, if there's a parent and child ahead of you in the queue, the child is getting their hair cut by one member of staff, the parent is waiting; another member of staff asks 'who's next?' - is it you or the parent?
Situations like these add uncertainty into a member's queue position, and by extension their knowledge of whether they're next.
In that situation, we might wait to see if anyone else steps up, and if not we can assume it's our turn.
So we have \(p(next)\), which is a Bayesian probability that updates over time to reflect whether anyone else has stepped up yet. The longer we wait with no-one stepping up, the closer our probability gets to one.
Of course, if you wait too long, someone behind you might assume you're not in the queue after all and try to go ahead of you. But that's a topic for another blog.
Nothing's so simple that it can't be made complex
I've written about queuing before, in the context of a mathematical model of a cafe.
The long and short of it is this - people arrive at random (following a Poisson distribution) and join the queue with some probability (see above). Each iteration, some people are served, some join the queue, some get tired of waiting and leave, etc.
I'm going to iterate in 5 min time-step and say that a haircut takes 10-25 minutes (i.e. 2-5 steps). The exact duration is randomly generated for each customer.
I'm going to say that on average one person shows up every 10 minutes (0.5 per step). Here's an example of how that might look over 12 steps (one hour), using the Poisson distribution: [2, 0, 2, 0, 0, 0, 1, 0, 0, 1, 0, 0]
I set up the simulation so that you specify some number of steps for the barbers to be considered 'open'. After that, no more people are added to the queue, but the simulation keeps running until the queue is empty.
To begin with, I made it so that everyone who arrived stayed.
With 2 workers, capacity 10, and an arrival rate of 0.5, the queue length typically stayed below 3. The highest I saw it go was 7, which is still comfortably within capacity.
Above is an example of how the queue size varied over time in a particular simulation.
Increasing the arrival rate to 0.7, the average maximum queue length goes up into the low teens. And when the rate goes up to 1, the maximum queue length goes all the way up into the 30s.
Homework question - how does maximum queue length vary as a function of number of workers and arrival rate?
As I mentioned, those simulations assumed that everyone stuck around. Once you turn on probabilistic leaving, things get a bit more interesting.
I tried both versions of the leaving probability. The result was largely the same, except that sunk-cost people tend to leave sooner - average wait before leaving 2.6 for sunk vs 7.4 for non-sunk. This is what we'd expect - people adhering to sunk cost will tend to leave before they get too invested.
In the above example, orange is a time step when someone left the queue, and red is when a newcomer decided not to join the queue. I tuned the probability constants so that people don't start leaving until we're close to or at capacity, as we'd expect in real life.
You and I have different ideas of what constitutes 'interesting'
Going back to the original description of the barber's queue, here's an example of a full queue (bracketed numbers are queue positions)
Notice how, even with the basic seat picking strategy (take the first available), the members aren't in a predictable ordered.
When we look at the waiting times, we can see some interesting things. For example
Here we have a person who had to wait 9 steps (45mins) to be served, followed by someone who only had to wait 3 steps (15mins). Which just goes to show, how long you wait in a queue is very much a matter of timing and luck.
It's also interesting that you can run the simulation multiple times with the exact same settings, and one time the queue will never go higher than 4, while in the the next it'll go as high as 13. This is complexity at work - various small random factors in the model interacting to produce wildly different outcomes.
So yeah. If you're interested, you can see the full code here. I may have gotten carried away with the object-orienting.
Oatzy.
[Post-Script
My boss recently pointed out that it'd been over a year since my last blog post. That was another perk of being unemployed - more time to come up with dumb blog posts. Anyway, here's a quick update on some stuff.
Pirate Game
The last blog post was about the making of an Android game - The Pirate Game.
The game is now finished-ish and has been released in 'beta' on the Play Store.
In the previous post, I mentioned that the game would eventually get a less utilitarian design. I ended up making that design myself (because I'm a control freak). I'm pretty pleased with how it turned out.
Also, following a... less than positive review, I added some new game play modes.
I never did figure out multiplayer, though. If I ever get the time or inclination to go back to the game, that'll be on the todo list. But for the time being, I don't anticipate any updates to the game. Certainly not any time soon.
New Job
So yeah. I finally got a job. I'm now a Software Developer at a company called Pixit Media.
The company sells large scale 'storage solutions' to companies primarily in the VFX industry, as well as universities and other such people that do high performance computing.
What I personally work on is primarily a Python API for the IBM Spectrum Scale (GPFS) filesystem. You can see the API docs online. I wrote a decent amount of the documentation (and the code that's being documented).
In particular, the 'Getting Started With List Processing' guide. Admittedly the topic is a bit niche - I doubt many readers of this blog even know of GPFS, let along have a cluster with it installed. But you might still find it interesting; you can learn some stuff about MapReduce - a technique for taking advantage of parallelism when processing large data-sets.
There's also the 'example scripts' repository - scripts written to use the API, some of while I wrote. But, again, they're a bit niche.
]
But these days I have to get my haircuts on Saturdays, before noon. Which is bad enough in itself - ideally I'd never see Saturday mornings at all. And to make matters worse, Saturday morning is also when the barbers is at its busiest.
Hence, I found myself sat in the waiter area of a barbershop for the best part of an hour. But this got me thinking about how queuing works at a barbers.
First In Who's Next?
At its core, the barber's queue is just a first-in first-out (FIFO) queue. But it has two interesting features:
1) The queue 'structure' is unordered
In general the queue 'structure' will be a waiting area with a bunch of seats. When someone new joins the queue, they're free to sit wherever. In fact, odds are, they'll pick a seat in a similar way to how men choose urinals - attempting to maximise personal space.
But the key point is, if someone were to just look at the queue, they wouldn't be able to tell who was next.
2) Each member of the queue knows whether or not they're next
Each member of the queue probably doesn't know who exactly is next, but they do know (with reasonable certainty) whether or not it's them.
The way this works is relatively simple - when you join the queue, you're aware of who was there when you arrived (and of anyone who arrives after you). So when all the people who were there ahead of you have gone, you know that you're next.
O(M G)
Okay, lets break out some Python (2.7)
Just for fun, let's say that the capacity of the queue is fixed - i.e. the waiting area has a fixed number of seats (though in practice, people are free to stand, as I was forced to).
class BarberQueue(object): def __init__(self, capacity): self._capacity = capacity self._queue = [None]*capacity self._length = 0 def __len__(self): return self._length def __str__(self): return ', '.join(str(i) if i is not None else '_' for i in self._queue) def push(self, obj): pass def pop(self): passSo each member of the queue has some awareness of who is ahead of them. But they don't need to know specifically who's who, they just need to keep track of how many are remaining. And in fact, that remaining count is exactly equivalent to the member's position in the queue.
class Member(object): def __init__(self, obj, position=0): self.value = obj self._position = position def __str__(self): return "%s (%s)" % (self.value, self._position) def is_next(self): return self._position == 0 def move_up(self): self._position -= 1So, going back to the push and pop methods
def push(self, obj):
if self._length == self._capacity:
raise Exception("Queue is full! Please come back later.")
for i, m in enumerate(self._queue):
if m is None:
self._queue[i] = Member(obj, self._length)
self._length += 1
return
Here, we're picking a 'seat' by iterate over the queue looking for the first empty slot (with a value of None). We could implement any seat picking strategy we fancy, this is just the easiest.
Once we find an empty seat, we create a new 'Member' object for the item, with position set to the current length of the queue, then increment the queue length. Also, if the queue has no empty slots, we raise an exception.
def pop(self):
if self._length == 0:
raise Exception("The queue is empty")
for i, m in enumerate(self._queue):
if m is not None:
if m.is_next():
value = m.value
self._queue[i] = None
self._length -= 1
else:
m.move_up()
return value
Here we iterate over the queue looking for the member who is 'next' (has position 0). While we're looking for the next person, we also de-increment the positions of the other members.Of course, this isn't how things work in practice. The barber doesn't go to each person and say "are you next?", "how about you?". They simply say "who's next?", and the person who believes they are next steps forward. Though arguably, that's just equivalent to asking every member concurrently. But let's not complicate things.
>>> b = BarberQueue(3)
>>> for i in xrange(3):
b.push(i)
>>> print b
0 (0), 1 (1), 2 (2)
>>> b.push(4)
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
b.push(4)
File "<pyshell>", line 36, in push
raise Exception("Queue is full! Please come back later")
Exception: Queue is full! Please come back later
>>> b.pop()
0
>>> print b
_, 1 (0), 2 (1)
>>> b.push(4)
>>> print b
4 (2), 1 (0), 2 (1)
>>> for _ in xrange(4):
b.pop()
1
2
4
Traceback (most recent call last):
File "<pyshell>", line 2, in <module>
b.pop()
File "<pyshell>", line 46, in pop
raise Exception("Empty queue!")
Exception: Empty queue!
>>> print b
_, _, _
So there we have it. Of course, this type of queue isn't really useful from a programming perspective.
All of insertion, deletion, and lookup are worst-case \(O(N)\), where N is the queue's capacity (not the number of people in the queue). Which is pretty much worse than all other types of queue.
Why do items keep disappearing from my queue?
Okay, let's move away from computer-sciencey queues. There are certain behaviours in real-world queues that don't apply or wouldn't make sense to programmatic queues.
For one, as I alluded to earlier, the capacity of the queue is not enforced - there's room for overflow, even if it means people have to stand.
But on the other hand, when a place does get that full, people are less likely to stick around.
In particular, we have two situations:
1) There's a non-zero probability that a person will not stick around if the queue is full or close to full. This probability will tend to be related to the length of the queue when that person arrives
\[p(not join) \sim f(capacity, length)\]
For example,
\[p(not join) = A\left(\frac{length}{capacity}\right) - C\]
where A and C are some constants relating to how likely the person is to stick around if the queue is 'full', and at what point they consider the place to be 'too full'.
2) There's a non-zero probability that a person already in the queue will leave before they're served. This probability will typically depend on how long the person has been waiting, and how many people are still ahead of them
\[p(leave) \sim g(wait, position)\]
It's interesting because as time passes, wait increases, but position decreases. So how the probability evolves depends on how those factors balance against one another.
In particular, the probability evolution will likely depend on how each particular person responds to the sunk-cost fallacy - i.e. are they the sort to think "well, I've waited this long, I might as well see it through to the end", or do they think "this is taking too long, I've got better things to do with my time"?
For the sake of arguing, lets go with an exponential function for the general form.
For a sunk-cost person we might have
\[p(leave) = B \cdot \exp\left(-d\cdot \frac{wait}{position}\right)\]
This is a function where p goes to zero as position goes to zero or wait goes to infinity (B and d are some arbitrary constants).
Whereas for a non-sunk-cost person we might have
\[p(leave) = B \cdot \exp\left(-\frac{d}{wait \cdot position}\right)\]
This is a function where p goes to zero as position goes to zero, but goes to one as wait goes to infinity.
This gives us an interesting graph
Because 'position' is discrete you get this nice step function, with intervals of the probability steadily rising, then suddenly dropping. We can also see that there's a point at which probability of leaving is maximum, around the time you're in the middle of the queue. Which seems plausible.
We also have situations where a person will leave and come back later. But since, when they come back, they have to join the back of the queue, they're mathematically indistinguishable from a someone arriving for the first time.
One other complicating situation is people in groups. For example, if there's a parent and child ahead of you in the queue, the child is getting their hair cut by one member of staff, the parent is waiting; another member of staff asks 'who's next?' - is it you or the parent?
Situations like these add uncertainty into a member's queue position, and by extension their knowledge of whether they're next.
In that situation, we might wait to see if anyone else steps up, and if not we can assume it's our turn.
So we have \(p(next)\), which is a Bayesian probability that updates over time to reflect whether anyone else has stepped up yet. The longer we wait with no-one stepping up, the closer our probability gets to one.
Of course, if you wait too long, someone behind you might assume you're not in the queue after all and try to go ahead of you. But that's a topic for another blog.
Nothing's so simple that it can't be made complex
I've written about queuing before, in the context of a mathematical model of a cafe.
The long and short of it is this - people arrive at random (following a Poisson distribution) and join the queue with some probability (see above). Each iteration, some people are served, some join the queue, some get tired of waiting and leave, etc.
I'm going to iterate in 5 min time-step and say that a haircut takes 10-25 minutes (i.e. 2-5 steps). The exact duration is randomly generated for each customer.
I'm going to say that on average one person shows up every 10 minutes (0.5 per step). Here's an example of how that might look over 12 steps (one hour), using the Poisson distribution: [2, 0, 2, 0, 0, 0, 1, 0, 0, 1, 0, 0]
I set up the simulation so that you specify some number of steps for the barbers to be considered 'open'. After that, no more people are added to the queue, but the simulation keeps running until the queue is empty.
To begin with, I made it so that everyone who arrived stayed.
With 2 workers, capacity 10, and an arrival rate of 0.5, the queue length typically stayed below 3. The highest I saw it go was 7, which is still comfortably within capacity.
Above is an example of how the queue size varied over time in a particular simulation.
Increasing the arrival rate to 0.7, the average maximum queue length goes up into the low teens. And when the rate goes up to 1, the maximum queue length goes all the way up into the 30s.
Homework question - how does maximum queue length vary as a function of number of workers and arrival rate?
As I mentioned, those simulations assumed that everyone stuck around. Once you turn on probabilistic leaving, things get a bit more interesting.
I tried both versions of the leaving probability. The result was largely the same, except that sunk-cost people tend to leave sooner - average wait before leaving 2.6 for sunk vs 7.4 for non-sunk. This is what we'd expect - people adhering to sunk cost will tend to leave before they get too invested.
In the above example, orange is a time step when someone left the queue, and red is when a newcomer decided not to join the queue. I tuned the probability constants so that people don't start leaving until we're close to or at capacity, as we'd expect in real life.
You and I have different ideas of what constitutes 'interesting'
Going back to the original description of the barber's queue, here's an example of a full queue (bracketed numbers are queue positions)
542b5af6 (3), 33e323cf (5), 69e60241 (6), b3f12010 (0), fc89732e (7), 991f8709 (1), a0cb93cc (2), 17186c75 (4), 57269a3e (8), 8f1b61ca (9)
Notice how, even with the basic seat picking strategy (take the first available), the members aren't in a predictable ordered.
When we look at the waiting times, we can see some interesting things. For example
...
1c7081d1 waited 7.0
34 1
35 3
50be68cf waited 9.0
36 3
37 4
26ca11b7 waited 3.0
38 3
39 3
a571a7da waited 5.0
...
Here we have a person who had to wait 9 steps (45mins) to be served, followed by someone who only had to wait 3 steps (15mins). Which just goes to show, how long you wait in a queue is very much a matter of timing and luck.
It's also interesting that you can run the simulation multiple times with the exact same settings, and one time the queue will never go higher than 4, while in the the next it'll go as high as 13. This is complexity at work - various small random factors in the model interacting to produce wildly different outcomes.
So yeah. If you're interested, you can see the full code here. I may have gotten carried away with the object-orienting.
Oatzy.
[Post-Script
My boss recently pointed out that it'd been over a year since my last blog post. That was another perk of being unemployed - more time to come up with dumb blog posts. Anyway, here's a quick update on some stuff.
Pirate Game
The last blog post was about the making of an Android game - The Pirate Game.
The game is now finished-ish and has been released in 'beta' on the Play Store.
In the previous post, I mentioned that the game would eventually get a less utilitarian design. I ended up making that design myself (because I'm a control freak). I'm pretty pleased with how it turned out.
Also, following a... less than positive review, I added some new game play modes.
I never did figure out multiplayer, though. If I ever get the time or inclination to go back to the game, that'll be on the todo list. But for the time being, I don't anticipate any updates to the game. Certainly not any time soon.
New Job
So yeah. I finally got a job. I'm now a Software Developer at a company called Pixit Media.
The company sells large scale 'storage solutions' to companies primarily in the VFX industry, as well as universities and other such people that do high performance computing.
What I personally work on is primarily a Python API for the IBM Spectrum Scale (GPFS) filesystem. You can see the API docs online. I wrote a decent amount of the documentation (and the code that's being documented).
In particular, the 'Getting Started With List Processing' guide. Admittedly the topic is a bit niche - I doubt many readers of this blog even know of GPFS, let along have a cluster with it installed. But you might still find it interesting; you can learn some stuff about MapReduce - a technique for taking advantage of parallelism when processing large data-sets.
There's also the 'example scripts' repository - scripts written to use the API, some of while I wrote. But, again, they're a bit niche.
]
Labels:
algorithm,
coding,
everyday maths,
mathematical methods,
maths,
modelling,
nerd,
over-thinking,
probability,
programming,
python,
simulation
Thursday, March 12, 2015
Watch Face Designs for Android Wear (and How to Make Them)
In a previous pair of blogs, I designed some clock widgets for Android, using Zooper. At the end, I said that if I ever got a smart watch I'd remake them for Android Wear.
Well, here I am following up on that promise. I've also thrown in a few new designs for good measure.
Now, these are mostly concept watch faces - which is to say, they're more 'look at this cool thing you can do', than the sort of watch faces that you'd actually want to use.
As with the previous blogs, I've provided download links for my designs (which you should be able to customise), along with general instructions for how they're constructed, as well as some code snippets.
I know that a lot of the people who read this post will have come here from trying to google how to do something. As someone who does the same, I want this to be as helpful as possible.
Note, these faces were designed in particular for my watch - Moto 360. So if you have a different device (in particular, a square faced device) you might want/need to tweak some stuff.
Watchmaking Apps
There are two popular apps for making Android Wear watch faces - Facer and WatchMaker. I've tried (and paid for) both.
Facer is very bare-bones - the only objects available are text, shapes and images, and the only customisation options are object size, position, rotation, colour, and opacity. I think it's aimed more at people who want to import images to construct their faces.
WatchMaker has many more build-in objects, including dials, hands, weather, battery, countdowns, series, etc. There are also more customisation option to play with (and more still if you pay for Premium).
Facer uses the same syntax and tags as Zooper, so if you're familiar with that you might prefer Facer. But that also means it has the same shortcoming, in particular with respect to if-statements.
Watchmaker, on the other hand, uses the programming language 'Lua' for its coding. I'm not familiar with Lua as a whole, but I found it easy to pick up here, and easier to work with than the Zooper-style syntax.
Of the two apps, I personally prefer WatchMaker - I like all the extra built-in objects and customisations, and I find the Lua syntax, and the way WM handles layout (see below) easier to work with.
This means, then, that most (but not all) of the download links and code snippets in this blog are going to be for WatchMaker. You should be able to recreate a lot of the designs in Facer though.
Design Layout
Facer and WatchMaker use different coordinate origins - Facer positions objects relative to the top-left corner of the screen, where WatchMaker positions objects relative to the centre of the screen.
Now, for watch design, we'll often need to convert the positions of objects from polar coordinates - distance from the centre of the screen (r), and rotation about the centre (\(\theta\)) - to Cartesian coordinates (x, y).
Traditionally, the conversion is
\[ x = r \cos(\theta) \\
y = r \sin(\theta) \]
But this positions objects relative to the positive x-axis, or 3 o'clock (blue). Really, we want to position objects relative to the negative y-axis, or 12 o'clock (yellow).
To correct this, we have to subtract 90 degrees from the angle, \(\theta\). Or, alternatively, we can use some trigonometric identities to rewrite the conversion as
\[ x = r \cos(\theta-90) \equiv \ r \sin(\theta) \\
y = r \sin(\theta-90) \equiv \ -r\cos(\theta) \]
In Facer, we also have to add an offset of about \(x_0 ,\ y_0 = 160px\) to x and y, so that objects are positioned relative to the centre of the screen (rather than the top left corner).
Note that in both apps, the functions 'sin' and 'cos' take variables in radians, whereas the angles given by tags are in degrees. This means we have to include 'rad()' (Facer) or 'math.rad()' (WatchMaker) whenever we use trig functions.
Finally, it's worth noting that (for the Moto 360, at least) Facer gives us a design area of about 320px, matching the watch's screen dimensions. WatchMaker, on the other hand, gives us around 520px. This means that faces designed in WatchMaker will be scaled down on the watch itself, and in some cases, this leads to pixelation.
Anyway, with all that in mind, lets look at some designs.
> A Bunch of Binary
Proportional Binary [WM]
I have to start, of course, with my pet design - already available in javascript, and for Zooper.
Each ring segment is a binary bit, sized in proportion to the value it represents - the 4 minutes bit is twice as big as the two minutes bit, etc. The outer ring is hours (24h), the inner is minutes.
In Zooper, this design was constructed using curved rectangles, which neither of the face making apps support. However, in WatchMaker Premium, we can get more or less the same effect by creating a circle with a 'Segment Between' shader, and hiding the centre of the segment with a circle that's the same colour as the face background.
Once we have that, the rest of the face construction is essentially the same as the one described in the previous blog. It's set up so that the segment opacity is 100% if the bit (segment) is 'on', and 15% if 'off'.
A quick reminder, the n-th bit is off if \( x \bmod 2^{n+1} \lt 2^n \), and on otherwise. So for the twos bit in the minutes ring we have
And so on for all the other segments.
Unfortunately, this is one of the designs that suffers significant pixelation on the actual watch screen. A slight improvement was to make the rings narrower such as below. Here I've also added a seconds ring.
In this case we could put some other information in the middle of the screen and use the binary as decoration.
Binary Rings [Facer]
We can actually do an approximation of the previous design in Facer. This is done with triangles, that are strategically covered with circles/rings. However, because of how this construction works, the layout is a little restricted in how broad and close together the rings can be.
In this case, it's easier to have all the bits/segments be the same size. We could possibly have proportionally sized bits like above, but it'd be a lot of fiddling. On the other hand, Facer doesn't seem to have the pixelation issue, so it could be worth the effort.
Again, the opacity of the bits is changed as described above and in the previous blog. For example
Aside - It's not obvious, but the brackets in the above are important. In general, if you find your code isn't behaving in Facer, try copy-pasting it into a text object to see what it's being interpreted as. Usually you can solve problems by adding/removing brackets, or by looking for any spaces that might have been auto-inserted after punctuation marks.
Binary Numbers [Facer]
This is the first face I made for Android Wear. The bits are all individual text objects, with their values set according to that method I keep banging on about. For example, the twos minute bit is a text object with value
The face shown above is 24hour format, where the leftmost hours digit is a dummy bit. An alternative might be to do 12hour format, with an am/pm bit.
Aside - I had this idea for a face where the background is an image of a circuit board with two rows of those little square LEDs, which could be 'illuminated' to represent the time in binary. But I'm not much of an artist, so I doubt I could create a decent background image.
Trinary [WM]
In trinary, or base 3, each bit (trit?) can have one of three values - 0, 1, 2. In Zooper, I handled this by using curved progress bars, but that doesn't really work in WatchMaker. In this case, I had to use pairs of segments for each bit.
As with Zooper, I had the first of each bit pair change colour when the second was 'on', to make reading easier. Sometimes, though, WatchMaker doesn't change the segment colours like it should (possible bug?). If you prefer, you can download a version without colour changing here.
I explained how to do trinary in the previous blog. A quick reminder - the n-th digit of a number in base b is given by \(\lfloor \frac{x}{b^n} \bmod b\rfloor\).
As an example, for the pair of segments in the 3s minute bit we have
Notice that when we use an if-statement with the colour, we have to wrap the colour value in apostrophes (inverted commas). We don't have to do this for a lone colour value.
Similar to the binary design at the top, we have the pixelation problem on the actual watch, and again we can sort of help this by making the rings narrower.
Clocks in other bases (as seen in the previous blog) can be constructed in a similar way.
> Planets and Parametrisations
Orbital [WM]
I'm a fan of 'cryptic' clock designs - clocks that let you tell the time, but don't make it obvious. In this design, the position on the planet relative to the sun is the hours hand, and the position of the moon relative to the planet is the minutes hand.
We set the planet's position as described in the Layout section
\[ x = r_h \sin(\theta_h) \\
y = -r_h\cos(\theta_h) \]
For the moon, we work out it's position relative to the planet, then add that to the planet's position relative to the sun
\[ x = r_h \sin(\theta_h) + r_m \sin(\theta_m) \\
y = -r_h \cos(\theta_h) - r_m \cos(\theta_m) \]
The code for this is
Orbital Numbers [WM]
This is basically the same principle as the above, except we've replaced the sun with the hours, the planet with the minutes, and the moon with the seconds. The code is similar to above, except we replace {drh} with {drm} and replace {drm} with {drs}. We also need to make the orbital radii slightly bigger to avoid overlapping - 165 for the planet/minutes, and 55 for the moon/seconds.
Elliptical Orbit [WM]
Here the planet does an elliptical orbit once per minute. The orbit also precesses (rotates) over the course of an hour, so that the planet's aphelion (furthest point from the sun) points to the current minutes past the hour.
An elliptical orbit, with aphelion pointing to 12 o'clock is given by
\[ x = -r_x\sin(\theta) \\
y = y_0 + r_y\cos(\theta) \]
To make the orbit precess, we can use the 2D rotation matrix
\[\begin{pmatrix}
\cos\phi & -\sin\phi \\
\sin\phi & \cos\phi
\end{pmatrix}\]
which gives use the rotated coordinates
\[ x' = x\cos\phi - y\sin\phi\\
y' = x\sin\phi + y\cos\phi \]
where \(\phi\) is the angle of rotation - in this case the minutes past the hour. So for the code, the precessing orbit is given by
The downside to this design is that it's pretty hard to tell where the aphelion is (i.e. the minutes past the hour) without watching the orbit for several seconds. Also, in it's current state, there's no way of telling the hour. Admittedly, this isn't great, design-wise. Really I just wanted to show off the rotation matrix.
As a variation, we could have, for example, three elliptical orbits (one for each hand), to make a face modeled after the classic portrayal of an atom (nucleus in the centre, with electrons whizzing around).
Binary Orbit [Facer] [WM]
This design was inspired by another previous blog post. Here we have a planet doing a figure-8 orbit around a pair of stars.
To set the planet's position, the sideways figure-8 path can be parametrised as
\[ x = r_x\sin\left(t\right) \\
y = r_y\sin\left(2t\right)\]
or in code
Changing the signs in front of either or both of the sin-functions will change the direction of orbit.
I was tempted to also have the stars (co)rotate - using the rotation matrix above to rotate the planet's orbit - so that they could act like an hours/minutes hand. But that seemed too much like hard work.
Spiral [WM]
In this design, the ball starts at the centre, and spirals outwards towards midday, then spirals back to the centre towards midnight. The ball's angular position gives the minutes past the hour.
The spiral is parameterised as
\[ x = r(t) \sin(\theta) \\
y = - r(t) \cos(\theta) \]
where the radius r is now a function of time. In code, this is
where {dtp} is the fraction of the day that has passed. Notice that we can use a conditional as a variable. That's pretty handy.
Originally, I didn't bother with the numbers (another cryptic design), but figured I should make it a little more readable. The numbers change at midday/midnight. Coding this is just a matter of creating text objects with values like
And so on.
Heart [WM]
Yes, I know, it looks ridiculous. When I realised I could have objects trace out any shape that can be parameterised, my first thought was the heart curve.
This has the equations
\[ \begin{align*}x &= 16\sin(t)^3 \\
y &= 13\cos(t) - 5\cos(2t) - 2\cos(3t) - \cos(4t)\end{align*} \]
and can be coded as
In the screenshot/gif, the heart graphic is just decoration. The ball doesn't trace out that exact shape, but it's close.
I added a (poorly drawn) arrow as an hour hand. The shaft is a rectangle rotated using {drh24}. The triangles that make up the arrow head and fletching are moved around using the usual sin and -cos, and have to be rotated with {drh24} to make sure they point in the right direction.
> Analogue-esque
Metric [WM]
Quick reminder - Metric, or 'decimalised' time redefines the day as being 10 metric hours long, with 100 metric minutes to the metric hour, and 100 metric seconds to the metric minute.
Aside - this means a metric minute is equal to 1 milliday, and a metric second is 10 microdays.
For the hours hand, we just set its rotation to '#DWFH#' (Facer) or '{drh24}' (WatchMaker) - even though the day is being defined as 10 hours long, the hours hand still only needs to do one revolution per day.
For the minutes hand, we have to do the full conversion, as described previously, then convert that to a rotation (in degrees). The code looks like this
Note that in Facer the whole expression has to be wrapped in brackets to be interpreted and evaluated as maths.
The seconds hand is a little trickier. If we do the conversion as before,
the hand will point to the right time, but because it will only update every standard (non-metric) second, it will only 'tick' about 87 times per revolution (instead on 100 times).
There is a crafty work around, though. First, we have to include milliseconds (WatchMaker) or 'smooth rotation for seconds' (Facer) in the conversion, so that the hand will update more frequently. We then use the 'floor' function to round down to the nearest whole number - this will make the hand only 'tick' once per metric second. The code looks like this
Alternatively, we could omit the 'floor' function, and have a smooth seconds hand.
Backwards [WM]
Here, I've tried to make the face design as close to the one on my bedroom wall as possible. Code-wise, this is just a matter of setting the hands' rotations to
Squares [WM]
This was just a random idea I had, and is another good example of a cryptic clock. Each square represents a hand, and they're scaled by \(\sqrt{2}\) so that they always stay within each other. The code is basically the same as for any other analogue design - set the square rotations to '{drh}-45' / '(#DWFKS#-45)', etc.
Probably I should have added indicators to show which corner of each square is the 'hand'. But I reckon you could figure out the time (or at least guess) without. I mean, the above is obviously showing 9:29:55.
Spotlight [WM]
This is not my design - I couldn't find the original source. You can actually download a version of this face from the PlayStore. This is more for anyone who's curious about how to (re)create it. Plus, this way you can customise it to your heart's content, as you'll see further down.
The basic idea is to create a dial that is bigger than the watch face itself, and place the centre of the dial somewhere off-screen. We then move the dial in a clockwise circle around the outside of the face, as
The hour line is just a rectangle with rotation {drh}
Spotlight with Minutes [WM]
One of the problems with the previous design is that there are only five minor markers between each hour marker - so in this context they're equivalent to 12 minutes each. This makes telling the minutes past the hour tricky. To remedy this we can introduce a little minutes circle.
This is just a matter of creating a circle outline and a minutes object, and moving them around as
Alternatively, we could use a dial that has a more useful number of minor markers. But the minutes circle is more visually striking, I think.
Spotlight with Minutes and Date [WM]
Okay, last one of these. I figured 'centre', above the hour line was the best place to put the date. For this, we have to make sure the date stays on the right side of the line and stays the right way up.
The code to do this is
Where the first terms for x and y move the date around with the hour line (note the signs), and the second terms are the positioning functions (dx, dy) that shift the date to above the hour line. The conditional in the rotation makes sure the text stays the right way up.
There is a slight problem with this on the Moto 360 - as you might notice in the above, the date will dip below the 'flat tire' line between about 11:15 and 12:45. If you have a 360, you might want move the date further along the line (change '165' in the code to something smaller).
> Fun with Progress Rings
Circles 1 [WM]
This and the following two designs are all variations on the same theme - it was inspired by an advert, I think. Can't remember what it was an advert for, though.
The outer ring segment represents the hours, and fills in (clockwise) towards midday, then 'unfills' from midday to midnight. This is made with a 'Segment Between' shader (WatchMaker Premium only) with
The orange circles around the outside are hour markers, and also give the hour ring the appearance of rounded corners. In this design, the smaller white circles over the hour markers act as minute markers, which change colour with the passing minutes, as for example
for the 5 minute marker, and so on.
Circles 2 [WM]
This design has the minutes as a separate inner ring, with four marker that behave similar to above, and with a 'progress ring' which indicates the exact minutes past the hour. The progress ring is made with another Segment shader, and the rounded corners on leading edge is done with a small circle, moved around with sin and -cos.
Circles 3 [WM]
This design is a sort of combination of the previous two, with the minutes progress ring placed on top of the hours ring. The minutes ring is made white when it overlaps an hours ring, and orange otherwise.
To make this we start with Circles 1, and for the minutes ring create two Segment Between shaders, which overlap half of the hours ring, with
We then need to create a smaller copy of the hours segment from Circles 1, as well as a segment with
to cover up the centre of the minutes segments and recreate the inner half of the hours ring.
The minute markers work as in Circles 1, but this time the hour markers also change colour, as for example,
You might notice in the screenshots above that this design seems to have a thin grey shadow/outline around some of the ring. I don't know if this is a bug or a feature of the shaders, but ideally it shouldn't be there.
Apple Activity [WM]
I saw something that looked like this in an Apple Watch promo - I think it was supposed to be an activity tracker. Anyway, it's a watch face for Android, now (take that, Apple). Shaders for progress rings, circles over the edges for rounded corners, I'm sure you get the idea by now.
Buffering [WM]
I was playing with shaders and came up with this, that looks sort of like a loading/buffering ring. The ring fills and unfills once a second, and the point where the ring fills from/to indicates the current seconds past the minute.
The code for this is
The percentage in the image above is the percentage of the day that has passed
The format function is used to round the percentage to one decimal place (Lua uses C-style formatting). Alternatively, I guess you could have hour and minute rings doing similar to this seconds ring, but that might be a bit dizzying.
Bonus: How to Make a Battery Ring in Facer [Facer]
As we've seen, making a progress ring in WatchMaker Premium is easy with shaders. But, you can actually make something similar to a progress ring in Facer.
The easy way to do this is to use polygons (squares, triangles, hexagons) to cover up sections of a ring, with the polygons coloured the same as the face background. We can then alter the polygon opacities to hide/reveal sections of the ring.
This gives us a progress ring with discrete intervals. How granular it is will depend on how many polygons we're willing to set up. For a battery meter, we could cover up a ring with 100 little squares, but it'd be a pain in the arse to build.
There is also a way to do a continuous progress ring, without too much effort - though it's a bit of an elaborate workaround. First we cover up a ring with 6 triangle, then we can shift, rotate and change the opacities of those triangles to hide/reveal the ring.
It works sort of like a folding fan. The first triangle moves into the position of triangle 2, then turns transparent. Triangle 2 then moves to 3 and turns transparent. And so on, until we get to triangle 6. Obviously, if we move triangle 6 around the circle like the others, it'll cover up section 1. So instead, we move it vertically upwards.
Admittedly, this makes the angle on the leading edge of the ring look a bit off in section 6. But we work with what we've got. There are various fiddly little work-arounds that we could use to correct this - e.g. we could cover the leading edge with a little circle or square. Those are left as an exercise for the reader.
The code works like this
And so on for the next 3 triangles (with the boundaries and initial positions suitably adjusted). Finally, for the last triangle, we have
Aside - constructing this made Facer run slooooooow.
So there you have it - it's not easy, but it is possible to make a progress ring in Facer. You could do the same trick in WatchMaker (free) if you didn't want to pay for Premium. But personally, I'd prefer to pay - shaders are easier, and more flexible.
> Other Assorted Nonsense
50 Shades of Grey [Facer] [WM]
This is the sort of dumb thing I think up when I can't get to sleep. To make this, we create a watchface with a white background, and add a black circle/square that fills the whole screen. We then set the circle's opacity to
This will cycle through 50 different shades of grey - from white to black - over the course of a minute.
Alternatively, we could have it cycle from white to black to white, so that we don't have the sudden jump from black to white at the end of a minute - [download]
For that, the code is
Aside - Facer doesn't seem to allow mathematical expressions in both the then-statement and the else-statement (?!), so we can't adapt the above. Which is annoying.
That Dress [WM]
You know the one. This works similarly to the above design - only this time we use 'HSV' shaders (WatchMaker Premium). For the background, we have a circle that fills the screen with
And the text, and the bars at the top and bottom of the screen are
This will go from black-blue to white-gold over the course of a minute. We can get a similar effect by layering a blue circle on top of a white background, and black text on top of a gold copy of the text, then varying the opacity of the black and blue. That method should work in Facer.
Alternatively, we can have the face cycle black-blue to white-gold to black-blue - [download]
Racing Numbers [WM]
Racing, in the sense that the numbers are moving from the right side of the screen to the left, all at different speeds (I couldn't think of a better name). Alternatively, you can think of it as the numbers being positioned horizontally based on their current value. It's straightforward enough to construct, I'm sure you can figure it out.
Jitters [Facer]
I don't have a gif of this - so imagine the time in the above image frantically jittering around the centre of the screen. The background is brown because I was tempted to call it 'too much coffee'.
This design is pretty much a case of, I saw that Facer had a 'random' function and I wanted to contrive a use for it. To make this face, we just set the position of the time object to
The time will be moved with every frame refresh - in Facer's case, I think that's 60fps. As far as I can tell there's no way to slow it down. I imagine this would get annoying after a while.
Conclusion
If you have a smart watch, making the face a replica of a 'dumb' watch seems a bit like missing the point. Having said that, if I saw a replica of my old watch I'd probably download it.
But the point is, you can do some design things with a smart watch that you could never do with a dumb watch. These are just a few ideas of some cool things you can do with face making apps.
Feel free to download, use, modify, re-post, etc. any of the designs or code in this post. You don't need to ask permission. If you do re-post, a name-check or link back would be nice, but I won't hold you to it.
The folder with all the faces is available here.
If you have any problems, want more details, or just want help with something, feel free to ask in the comments and I'll see what I can do.
Oatzy.
[If I had any sense I'd be trying to make some money off these...]
Well, here I am following up on that promise. I've also thrown in a few new designs for good measure.
Now, these are mostly concept watch faces - which is to say, they're more 'look at this cool thing you can do', than the sort of watch faces that you'd actually want to use.
As with the previous blogs, I've provided download links for my designs (which you should be able to customise), along with general instructions for how they're constructed, as well as some code snippets.
I know that a lot of the people who read this post will have come here from trying to google how to do something. As someone who does the same, I want this to be as helpful as possible.
Note, these faces were designed in particular for my watch - Moto 360. So if you have a different device (in particular, a square faced device) you might want/need to tweak some stuff.
Watchmaking Apps
There are two popular apps for making Android Wear watch faces - Facer and WatchMaker. I've tried (and paid for) both.
Facer is very bare-bones - the only objects available are text, shapes and images, and the only customisation options are object size, position, rotation, colour, and opacity. I think it's aimed more at people who want to import images to construct their faces.
WatchMaker has many more build-in objects, including dials, hands, weather, battery, countdowns, series, etc. There are also more customisation option to play with (and more still if you pay for Premium).
Facer uses the same syntax and tags as Zooper, so if you're familiar with that you might prefer Facer. But that also means it has the same shortcoming, in particular with respect to if-statements.
Watchmaker, on the other hand, uses the programming language 'Lua' for its coding. I'm not familiar with Lua as a whole, but I found it easy to pick up here, and easier to work with than the Zooper-style syntax.
Of the two apps, I personally prefer WatchMaker - I like all the extra built-in objects and customisations, and I find the Lua syntax, and the way WM handles layout (see below) easier to work with.
This means, then, that most (but not all) of the download links and code snippets in this blog are going to be for WatchMaker. You should be able to recreate a lot of the designs in Facer though.
Design Layout
Facer and WatchMaker use different coordinate origins - Facer positions objects relative to the top-left corner of the screen, where WatchMaker positions objects relative to the centre of the screen.
![]() |
| Notice that the y-axis is upside down in both apps. |
Now, for watch design, we'll often need to convert the positions of objects from polar coordinates - distance from the centre of the screen (r), and rotation about the centre (\(\theta\)) - to Cartesian coordinates (x, y).
Traditionally, the conversion is
\[ x = r \cos(\theta) \\
y = r \sin(\theta) \]
But this positions objects relative to the positive x-axis, or 3 o'clock (blue). Really, we want to position objects relative to the negative y-axis, or 12 o'clock (yellow).
To correct this, we have to subtract 90 degrees from the angle, \(\theta\). Or, alternatively, we can use some trigonometric identities to rewrite the conversion as
\[ x = r \cos(\theta-90) \equiv \ r \sin(\theta) \\
y = r \sin(\theta-90) \equiv \ -r\cos(\theta) \]
In Facer, we also have to add an offset of about \(x_0 ,\ y_0 = 160px\) to x and y, so that objects are positioned relative to the centre of the screen (rather than the top left corner).
Note that in both apps, the functions 'sin' and 'cos' take variables in radians, whereas the angles given by tags are in degrees. This means we have to include 'rad()' (Facer) or 'math.rad()' (WatchMaker) whenever we use trig functions.
Finally, it's worth noting that (for the Moto 360, at least) Facer gives us a design area of about 320px, matching the watch's screen dimensions. WatchMaker, on the other hand, gives us around 520px. This means that faces designed in WatchMaker will be scaled down on the watch itself, and in some cases, this leads to pixelation.
Anyway, with all that in mind, lets look at some designs.
> A Bunch of Binary
Proportional Binary [WM]
![]() |
| 22:22 |
I have to start, of course, with my pet design - already available in javascript, and for Zooper.
Each ring segment is a binary bit, sized in proportion to the value it represents - the 4 minutes bit is twice as big as the two minutes bit, etc. The outer ring is hours (24h), the inner is minutes.
In Zooper, this design was constructed using curved rectangles, which neither of the face making apps support. However, in WatchMaker Premium, we can get more or less the same effect by creating a circle with a 'Segment Between' shader, and hiding the centre of the segment with a circle that's the same colour as the face background.
Once we have that, the rest of the face construction is essentially the same as the one described in the previous blog. It's set up so that the segment opacity is 100% if the bit (segment) is 'on', and 15% if 'off'.
A quick reminder, the n-th bit is off if \( x \bmod 2^{n+1} \lt 2^n \), and on otherwise. So for the twos bit in the minutes ring we have
% opacity in range: {dm}%4 < 2 and 15 or 100
And so on for all the other segments.
Unfortunately, this is one of the designs that suffers significant pixelation on the actual watch screen. A slight improvement was to make the rings narrower such as below. Here I've also added a seconds ring.
![]() |
| [download] |
In this case we could put some other information in the middle of the screen and use the binary as decoration.
Binary Rings [Facer]
![]() |
| 22:17 |
We can actually do an approximation of the previous design in Facer. This is done with triangles, that are strategically covered with circles/rings. However, because of how this construction works, the layout is a little restricted in how broad and close together the rings can be.
In this case, it's easier to have all the bits/segments be the same size. We could possibly have proportionally sized bits like above, but it'd be a lot of fiddling. On the other hand, Facer doesn't seem to have the pixelation issue, so it could be worth the effort.
Again, the opacity of the bits is changed as described above and in the previous blog. For example
Opacity: $(#Dm#%4)<2?15:100$
Aside - It's not obvious, but the brackets in the above are important. In general, if you find your code isn't behaving in Facer, try copy-pasting it into a text object to see what it's being interpreted as. Usually you can solve problems by adding/removing brackets, or by looking for any spaces that might have been auto-inserted after punctuation marks.
Binary Numbers [Facer]
![]() |
| 21:30 |
This is the first face I made for Android Wear. The bits are all individual text objects, with their values set according to that method I keep banging on about. For example, the twos minute bit is a text object with value
Text: $(#Dm#%4)<2?0:1$
The face shown above is 24hour format, where the leftmost hours digit is a dummy bit. An alternative might be to do 12hour format, with an am/pm bit.
Aside - I had this idea for a face where the background is an image of a circuit board with two rows of those little square LEDs, which could be 'illuminated' to represent the time in binary. But I'm not much of an artist, so I doubt I could create a decent background image.
Trinary [WM]
![]() |
| 21:35 |
In trinary, or base 3, each bit (trit?) can have one of three values - 0, 1, 2. In Zooper, I handled this by using curved progress bars, but that doesn't really work in WatchMaker. In this case, I had to use pairs of segments for each bit.
As with Zooper, I had the first of each bit pair change colour when the second was 'on', to make reading easier. Sometimes, though, WatchMaker doesn't change the segment colours like it should (possible bug?). If you prefer, you can download a version without colour changing here.
I explained how to do trinary in the previous blog. A quick reminder - the n-th digit of a number in base b is given by \(\lfloor \frac{x}{b^n} \bmod b\rfloor\).
As an example, for the pair of segments in the 3s minute bit we have
Segment 1:
% opacity in range: math.floor({dm}/3)%3 >= 1 and 100 or 15
Color: math.floor({dm}/3)%3 == 2 and '2cb7ff' or '1b74c0'
Segment 2:
% opacity in range: math.floor({dm}/3)%3 == 2 and 100 or 15
Color: 1b74c0
Notice that when we use an if-statement with the colour, we have to wrap the colour value in apostrophes (inverted commas). We don't have to do this for a lone colour value.
Similar to the binary design at the top, we have the pixelation problem on the actual watch, and again we can sort of help this by making the rings narrower.
![]() |
| [download] |
Clocks in other bases (as seen in the previous blog) can be constructed in a similar way.
> Planets and Parametrisations
Orbital [WM]
![]() |
| 10:05 |
I'm a fan of 'cryptic' clock designs - clocks that let you tell the time, but don't make it obvious. In this design, the position on the planet relative to the sun is the hours hand, and the position of the moon relative to the planet is the minutes hand.
We set the planet's position as described in the Layout section
\[ x = r_h \sin(\theta_h) \\
y = -r_h\cos(\theta_h) \]
For the moon, we work out it's position relative to the planet, then add that to the planet's position relative to the sun
\[ x = r_h \sin(\theta_h) + r_m \sin(\theta_m) \\
y = -r_h \cos(\theta_h) - r_m \cos(\theta_m) \]
The code for this is
Planet:
x: 150*math.sin(math.rad({drh}))
y: -150*math.cos(math.rad({drh}))
Moon:
x: 150*math.sin(math.rad({drh})) + 50*math.sin(math.rad({drm}))
y: -150*math.cos(math.rad({drh})) - 50*math.cos(math.rad({drm}))
Orbital Numbers [WM]
This is basically the same principle as the above, except we've replaced the sun with the hours, the planet with the minutes, and the moon with the seconds. The code is similar to above, except we replace {drh} with {drm} and replace {drm} with {drs}. We also need to make the orbital radii slightly bigger to avoid overlapping - 165 for the planet/minutes, and 55 for the moon/seconds.
Elliptical Orbit [WM]
Here the planet does an elliptical orbit once per minute. The orbit also precesses (rotates) over the course of an hour, so that the planet's aphelion (furthest point from the sun) points to the current minutes past the hour.
An elliptical orbit, with aphelion pointing to 12 o'clock is given by
\[ x = -r_x\sin(\theta) \\
y = y_0 + r_y\cos(\theta) \]
To make the orbit precess, we can use the 2D rotation matrix
\[\begin{pmatrix}
\cos\phi & -\sin\phi \\
\sin\phi & \cos\phi
\end{pmatrix}\]
which gives use the rotated coordinates
\[ x' = x\cos\phi - y\sin\phi\\
y' = x\sin\phi + y\cos\phi \]
where \(\phi\) is the angle of rotation - in this case the minutes past the hour. So for the code, the precessing orbit is given by
x: -75*math.sin(math.rad({drs}))*math.cos(math.rad({drm})) - (150*math.cos(math.rad({drs}))-80)*math.sin(math.rad({drm}))
y: -75*math.sin(math.rad({drs}))*math.sin(math.rad({drm})) + (150*math.cos(math.rad({drs}))-80)*math.cos(math.rad({drm}))
The downside to this design is that it's pretty hard to tell where the aphelion is (i.e. the minutes past the hour) without watching the orbit for several seconds. Also, in it's current state, there's no way of telling the hour. Admittedly, this isn't great, design-wise. Really I just wanted to show off the rotation matrix.
As a variation, we could have, for example, three elliptical orbits (one for each hand), to make a face modeled after the classic portrayal of an atom (nucleus in the centre, with electrons whizzing around).
Binary Orbit [Facer] [WM]
This design was inspired by another previous blog post. Here we have a planet doing a figure-8 orbit around a pair of stars.
To set the planet's position, the sideways figure-8 path can be parametrised as
\[ x = r_x\sin\left(t\right) \\
y = r_y\sin\left(2t\right)\]
or in code
WM:
x: 225*math.sin(math.rad(6*{ds}))
y: 100*math.sin(math.rad(12*{ds}))
Facer:
x: (155+140*sin(rad(6*#Ds#)))
y: (150-70*sin(rad(12*#Ds#)))
Changing the signs in front of either or both of the sin-functions will change the direction of orbit.
I was tempted to also have the stars (co)rotate - using the rotation matrix above to rotate the planet's orbit - so that they could act like an hours/minutes hand. But that seemed too much like hard work.
Spiral [WM]
![]() |
| 9:35pm |
In this design, the ball starts at the centre, and spirals outwards towards midday, then spirals back to the centre towards midnight. The ball's angular position gives the minutes past the hour.
The spiral is parameterised as
\[ x = r(t) \sin(\theta) \\
y = - r(t) \cos(\theta) \]
where the radius r is now a function of time. In code, this is
x: 17 + 420*({dtp}<0.5 and {dtp} or 1-{dtp}))*math.sin(math.rad({drm}))
y: -17 - 420*({dtp}<0.5 and {dtp} or 1-{dtp}))*math.cos(math.rad({drm}))
where {dtp} is the fraction of the day that has passed. Notice that we can use a conditional as a variable. That's pretty handy.
Originally, I didn't bother with the numbers (another cryptic design), but figured I should make it a little more readable. The numbers change at midday/midnight. Coding this is just a matter of creating text objects with values like
Text: {dh23}<12 and 11 or 1
And so on.
Heart [WM]
Yes, I know, it looks ridiculous. When I realised I could have objects trace out any shape that can be parameterised, my first thought was the heart curve.
This has the equations
\[ \begin{align*}x &= 16\sin(t)^3 \\
y &= 13\cos(t) - 5\cos(2t) - 2\cos(3t) - \cos(4t)\end{align*} \]
and can be coded as
x: 192*(math.sin(math.rad(6*{ds})))^3
y: -156*math.cos(math.rad(6*{ds}) + 60*math.cos(math.rad(12*{ds}) + 24*math.cos(math.rad(18*{ds}) + math.cos(math.rad(36*{ds})
In the screenshot/gif, the heart graphic is just decoration. The ball doesn't trace out that exact shape, but it's close.
I added a (poorly drawn) arrow as an hour hand. The shaft is a rectangle rotated using {drh24}. The triangles that make up the arrow head and fletching are moved around using the usual sin and -cos, and have to be rotated with {drh24} to make sure they point in the right direction.
> Analogue-esque
Metric [WM]
Quick reminder - Metric, or 'decimalised' time redefines the day as being 10 metric hours long, with 100 metric minutes to the metric hour, and 100 metric seconds to the metric minute.
Aside - this means a metric minute is equal to 1 milliday, and a metric second is 10 microdays.
For the hours hand, we just set its rotation to '#DWFH#' (Facer) or '{drh24}' (WatchMaker) - even though the day is being defined as 10 hours long, the hours hand still only needs to do one revolution per day.
For the minutes hand, we have to do the full conversion, as described previously, then convert that to a rotation (in degrees). The code looks like this
WM: 360*((0.00011574*(3600*{dh23}+60*{dm}+{ds}))%1)
Facer: (360*((0.00011574*(3600*#DH#+60*#Dm#+#Ds#))%1))
Note that in Facer the whole expression has to be wrapped in brackets to be interpreted and evaluated as maths.
The seconds hand is a little trickier. If we do the conversion as before,
WM: 360*((0.011574*(3600*{dh23}+60*{dm}+{ds}))%1)
Facer: (360*((0.011574*(3600*#DH#+60*#Dm#+#Ds#))%1))
the hand will point to the right time, but because it will only update every standard (non-metric) second, it will only 'tick' about 87 times per revolution (instead on 100 times).
There is a crafty work around, though. First, we have to include milliseconds (WatchMaker) or 'smooth rotation for seconds' (Facer) in the conversion, so that the hand will update more frequently. We then use the 'floor' function to round down to the nearest whole number - this will make the hand only 'tick' once per metric second. The code looks like this
WM: 3.6*math.floor((1.1574*(3600*{dh23}+60*{dm}+{ds}+{dss}/1000))%100)
Facer: (3.6*floor((1.1574*(3600*#DH#+60*#Dm#+#DWFSS#/6))%100))
Alternatively, we could omit the 'floor' function, and have a smooth seconds hand.
Backwards [WM]
Here, I've tried to make the face design as close to the one on my bedroom wall as possible. Code-wise, this is just a matter of setting the hands' rotations to
WM: 360-{drh}, 360-{drm}, 360-{drs}
Facer: (360-#DWFKS#), (360-#DWFMS#), (360-#DWFS#)
Squares [WM]
This was just a random idea I had, and is another good example of a cryptic clock. Each square represents a hand, and they're scaled by \(\sqrt{2}\) so that they always stay within each other. The code is basically the same as for any other analogue design - set the square rotations to '{drh}-45' / '(#DWFKS#-45)', etc.
Probably I should have added indicators to show which corner of each square is the 'hand'. But I reckon you could figure out the time (or at least guess) without. I mean, the above is obviously showing 9:29:55.
Spotlight [WM]
This is not my design - I couldn't find the original source. You can actually download a version of this face from the PlayStore. This is more for anyone who's curious about how to (re)create it. Plus, this way you can customise it to your heart's content, as you'll see further down.
The basic idea is to create a dial that is bigger than the watch face itself, and place the centre of the dial somewhere off-screen. We then move the dial in a clockwise circle around the outside of the face, as
x: -350*math.sin(math.rad({drh}))
y: 350*math.cos(math.rad({drh}))
The hour line is just a rectangle with rotation {drh}
Spotlight with Minutes [WM]
One of the problems with the previous design is that there are only five minor markers between each hour marker - so in this context they're equivalent to 12 minutes each. This makes telling the minutes past the hour tricky. To remedy this we can introduce a little minutes circle.
This is just a matter of creating a circle outline and a minutes object, and moving them around as
x: 135*math.sin(math.rad({drh}))
y: -135*math.cos(math.rad({drh}))
Alternatively, we could use a dial that has a more useful number of minor markers. But the minutes circle is more visually striking, I think.
Spotlight with Minutes and Date [WM]
Okay, last one of these. I figured 'centre', above the hour line was the best place to put the date. For this, we have to make sure the date stays on the right side of the line and stays the right way up.
![]() |
| Positions of the date relative to the hour line (left) and the corresponding positioning functions (right) |
The code to do this is
x: -165*math.sin(math.rad({drh})) + ({dh11}<6 and -20 or 20)*math.cos(math.rad({drh}))
y: 165*math.cos(math.rad({drh})) + ({dh11}<6 and -20 or 20)*math.sin(math.rad({drh}))
Rotation: {drh} + ({dh11}<6 and -90 or 90)
Where the first terms for x and y move the date around with the hour line (note the signs), and the second terms are the positioning functions (dx, dy) that shift the date to above the hour line. The conditional in the rotation makes sure the text stays the right way up.
There is a slight problem with this on the Moto 360 - as you might notice in the above, the date will dip below the 'flat tire' line between about 11:15 and 12:45. If you have a 360, you might want move the date further along the line (change '165' in the code to something smaller).
> Fun with Progress Rings
Circles 1 [WM]
![]() |
| 2:27am (left) and 9:28pm (right) |
The outer ring segment represents the hours, and fills in (clockwise) towards midday, then 'unfills' from midday to midnight. This is made with a 'Segment Between' shader (WatchMaker Premium only) with
Degree Start: {dh23} < 12 and 0 or {drh0}
Degree End: {dh23} < 12 and {drh0} or 360
The orange circles around the outside are hour markers, and also give the hour ring the appearance of rounded corners. In this design, the smaller white circles over the hour markers act as minute markers, which change colour with the passing minutes, as for example
Colour: {dm}>=5 and 'ffbd2a' or 'fbfbfb'
for the 5 minute marker, and so on.
Circles 2 [WM]
![]() |
| 9:34pm |
This design has the minutes as a separate inner ring, with four marker that behave similar to above, and with a 'progress ring' which indicates the exact minutes past the hour. The progress ring is made with another Segment shader, and the rounded corners on leading edge is done with a small circle, moved around with sin and -cos.
Circles 3 [WM]
![]() |
| 1:36am (left) and 8:51pm (right) |
To make this we start with Circles 1, and for the minutes ring create two Segment Between shaders, which overlap half of the hours ring, with
Segment 1:
Segment Start: 0
Segment End: {drm}<{drh0} and {drm} or {drh0}
Color: {dh23}<12 and 'fbfbfb' or 'ffbd2a'
Segment 2:
Segment Start: {drh0}
Segment End: {drm}>{drh0} and {drm} or {drh0}
Color: {dh23}<12 and 'ffbd2a' or 'fbfbfb'
We then need to create a smaller copy of the hours segment from Circles 1, as well as a segment with
Segment Start: {dh23} < 12 and {drh0} or 0
Segment End: {dh23} < 12 and 360 or {drh0}
Color: fbfbfb
to cover up the centre of the minutes segments and recreate the inner half of the hours ring.
The minute markers work as in Circles 1, but this time the hour markers also change colour, as for example,
Colour: ((({dh11}>1 and {dh23}<12) or ({dh11}<1 and {dh23}>=12)) and {dm}>=5) and 'fbfbfb' or 'ffbd2a'
You might notice in the screenshots above that this design seems to have a thin grey shadow/outline around some of the ring. I don't know if this is a bug or a feature of the shaders, but ideally it shouldn't be there.
Apple Activity [WM]
I saw something that looked like this in an Apple Watch promo - I think it was supposed to be an activity tracker. Anyway, it's a watch face for Android, now (take that, Apple). Shaders for progress rings, circles over the edges for rounded corners, I'm sure you get the idea by now.
Buffering [WM]
I was playing with shaders and came up with this, that looks sort of like a loading/buffering ring. The ring fills and unfills once a second, and the point where the ring fills from/to indicates the current seconds past the minute.
The code for this is
Segment Start: ({ds}%2==0) and {drs} or ({drs}+0.36*{dss})%360
Segment End: ({ds}%2==1) and {drs} or ({drs}+0.36*{dss})%360
The percentage in the image above is the percentage of the day that has passed
Text: string.format('%.1f', 100*{dtp})
The format function is used to round the percentage to one decimal place (Lua uses C-style formatting). Alternatively, I guess you could have hour and minute rings doing similar to this seconds ring, but that might be a bit dizzying.
Bonus: How to Make a Battery Ring in Facer [Facer]
As we've seen, making a progress ring in WatchMaker Premium is easy with shaders. But, you can actually make something similar to a progress ring in Facer.
The easy way to do this is to use polygons (squares, triangles, hexagons) to cover up sections of a ring, with the polygons coloured the same as the face background. We can then alter the polygon opacities to hide/reveal sections of the ring.
This gives us a progress ring with discrete intervals. How granular it is will depend on how many polygons we're willing to set up. For a battery meter, we could cover up a ring with 100 little squares, but it'd be a pain in the arse to build.
There is also a way to do a continuous progress ring, without too much effort - though it's a bit of an elaborate workaround. First we cover up a ring with 6 triangle, then we can shift, rotate and change the opacities of those triangles to hide/reveal the ring.
It works sort of like a folding fan. The first triangle moves into the position of triangle 2, then turns transparent. Triangle 2 then moves to 3 and turns transparent. And so on, until we get to triangle 6. Obviously, if we move triangle 6 around the circle like the others, it'll cover up section 1. So instead, we move it vertically upwards.
Admittedly, this makes the angle on the leading edge of the ring look a bit off in section 6. But we work with what we've got. There are various fiddly little work-arounds that we could use to correct this - e.g. we could cover the leading edge with a little circle or square. Those are left as an exercise for the reader.
The code works like this
Triangle 1: x: (160+50*cos(rad(3.6*#BLN#))+80*sin(rad(3.6*#BLN#))) y: (160-80*cos(rad(3.6*#BLN#))+50*sin(rad(3.6*#BLN#))) Rotation: (30+3.6*#BLN#) Opacity: $#BLN#<(100/6)?100:0$ Triangle 2: x: $#BLN#<(100/6)?260:(160+50*cos(rad(3.6*#BLN#))+80*sin(rad(3.6*#BLN#)))$ y: $#BLN#<(100/6)?160:(160-80*cos(rad(3.6*#BLN#))+50*sin(rad(3.6*#BLN#)))$ Rotation: $#BLN#<(100/6)?30:(30+3.6*#BLN#)$ Opacity: $#BLN#<(200/6)?100:0$
And so on for the next 3 triangles (with the boundaries and initial positions suitably adjusted). Finally, for the last triangle, we have
x: 110 y: $#BLN#<(500/6)?80:(80 - 130*(0.06*#BLN# - 5))$ Rotation: 30 Opacity: $#BLN#<100?100:0$
Aside - constructing this made Facer run slooooooow.
So there you have it - it's not easy, but it is possible to make a progress ring in Facer. You could do the same trick in WatchMaker (free) if you didn't want to pay for Premium. But personally, I'd prefer to pay - shaders are easier, and more flexible.
> Other Assorted Nonsense
50 Shades of Grey [Facer] [WM]
This is the sort of dumb thing I think up when I can't get to sleep. To make this, we create a watchface with a white background, and add a black circle/square that fills the whole screen. We then set the circle's opacity to
WM: 2*math.floor(5*{dsps}/6000)
Facer: (2*floor(#DWFSS#/7.2))
This will cycle through 50 different shades of grey - from white to black - over the course of a minute.
Alternatively, we could have it cycle from white to black to white, so that we don't have the sudden jump from black to white at the end of a minute - [download]
For that, the code is
WM: {ds}<=30 and 4*math.floor(5*{dsps}/6000) or (198-4*math.floor(5*{dsps}/6000))
Aside - Facer doesn't seem to allow mathematical expressions in both the then-statement and the else-statement (?!), so we can't adapt the above. Which is annoying.
That Dress [WM]
You know the one. This works similarly to the above design - only this time we use 'HSV' shaders (WatchMaker Premium). For the background, we have a circle that fills the screen with
Colour: 3228de
Saturation: -{dsps}/600
And the text, and the bars at the top and bottom of the screen are
Colour: a07b35
Value: 8*({dsps}/6000)-80
This will go from black-blue to white-gold over the course of a minute. We can get a similar effect by layering a blue circle on top of a white background, and black text on top of a gold copy of the text, then varying the opacity of the black and blue. That method should work in Facer.
Alternatively, we can have the face cycle black-blue to white-gold to black-blue - [download]
Circle:
Saturation: {ds}<=30 and -{dsps}/300 or {dsps}/300-198
Text/bars:
Value: {ds}<=30 and {dsps}/375-80 or 78-{dsps}/375
Racing Numbers [WM]
Racing, in the sense that the numbers are moving from the right side of the screen to the left, all at different speeds (I couldn't think of a better name). Alternatively, you can think of it as the numbers being positioned horizontally based on their current value. It's straightforward enough to construct, I'm sure you can figure it out.
Jitters [Facer]
I don't have a gif of this - so imagine the time in the above image frantically jittering around the centre of the screen. The background is brown because I was tempted to call it 'too much coffee'.
This design is pretty much a case of, I saw that Facer had a 'random' function and I wanted to contrive a use for it. To make this face, we just set the position of the time object to
x: (150+rand(0,20)) y: (170+rand(0,20))
The time will be moved with every frame refresh - in Facer's case, I think that's 60fps. As far as I can tell there's no way to slow it down. I imagine this would get annoying after a while.
Conclusion
If you have a smart watch, making the face a replica of a 'dumb' watch seems a bit like missing the point. Having said that, if I saw a replica of my old watch I'd probably download it.
But the point is, you can do some design things with a smart watch that you could never do with a dumb watch. These are just a few ideas of some cool things you can do with face making apps.
Feel free to download, use, modify, re-post, etc. any of the designs or code in this post. You don't need to ask permission. If you do re-post, a name-check or link back would be nice, but I won't hold you to it.
The folder with all the faces is available here.
If you have any problems, want more details, or just want help with something, feel free to ask in the comments and I'll see what I can do.
Oatzy.
[If I had any sense I'd be trying to make some money off these...]
Labels:
android,
android wear,
binary,
clock,
coding,
design,
everyday maths,
geometry,
mathematical methods,
maths,
metric,
metric clock,
nerd,
time,
trigonometry
Sunday, December 22, 2013
Modelling the Survival of Chocolates
So it's been about a year since I last blogged. I've been kinda busy with university stuff, and growing my facial hair into something approaching a beard. I might write a blog about it at some point.
Anyway, I've got a little free time now - at least until after Christmas - so here we are. Let's waste some time...
"This is a real paper, published in a proper journal"
So the other day, my dad hands me this print out of an academic paper, "this seems like your sort of thing". The paper is called
"The survival time of chocolates on hospital wards: covert observational study" [1]
The basic set-up is this: staff at various hospitals put out boxes of Roses and Quality Street chocolates on wards, and (covertly) recorded every time one was taken. From this, they analysed the 'survival' of the chocolates, according to Kaplan-Meier survival analysis.
They found that the rate of chocolate consumption fit an exponential decay, with the median survival time for a chocolate being 51 minutes, and the survival half life ("time taken for 50% of the chocolates to be eaten") being 99 minutes. They also found that Roses chocolates were preferred over Quality Street.
"Damn it, man, I'm a physicist, not a doctor!"
So I'm reading this paper, and I'm thinking "I reckon I could model this". So I had a crack at it.
Basically, what we've got here is an agent-based modelling problem. Individuals encounter a box of chocolates at random intervals. Each box has a variety of different types of chocolates. So each individual will look through the box for any chocolate(s) they like the look of, and help themselves.
We can make a few simplifications to this. First of all, we work in fixed, discrete time intervals. In each time interval, some number of people will encounter the box of chocolates - one person per interval in the simplest model, and some random number of people in a more complex model.
Now, different people have different preferences. We could generate for each person a random set of preferences, but for simplicity we'll assume some 'average preference probability'. In other words, imagine there are equal numbers of each type of chocolates - what is the probability that any given person will pick type 1, type 2, etc. ?
So, in the model, each person will pick a random chocolate based on this preference probability distribution. This accounts for the fact that some types of chocolate (strawberry) are naturally more popular than others (toffee penny).
Here's Where it Gets Maths-y
We have a box of N chocolates total. There are m different types of chocolate, with each type i=1,..,m having a count of ni such that
Each type also has a 'preference probability' pi with 0 <= pi <= 1 and
In each time step dt an individual randomly chooses a type of chocolate, i in {1,...,m}, based on the preference distribution, and one chocolate of that type is removed from the box according to
This process is repeated until N(t) = 0
In the more complex model, some random number of individuals (following a Poisson distribution) each choose chocolates in each given time step. This is also functionally equivalent to a single individual choosing multiple chocolates in a single given time step.
What Differential Does It Make?
In the simplest model, one person chooses a single chocolate in each time step. This is nice, because it means we can express the model as a set of differential equations, which can be solved exactly.
For each type of chocolate we have the rate of consumption given by
which has standard (exponential decay) solution
And since the total count N is just the sum of the individual counts ni we get the full solution
In other words, it's just the sum of several exponential decays (with different decay rates) - making it, roughly, an exponential decay itself.
In particular, in the special case where all the chocolates are equally preferred such that pi = p for all i=1,..,m we get the solution
And all that is loosely what was observed on the hospital wards.
But these 'exact' solutions don't capture some of the subtleties of the real life set-up. For example, the toffee pennies might be completely ignored until they are the only option left; whereas, the exact solution assumes that (fractions of) all of the types of chocolate are being taken all the time, just at different rates.
"We did not seek ethical approval for this study..."
As an improvement on the analytical solution, we can create a computer simulated model, that captures more of the randomness of real life (code below). For the smplest model, the results look something like this
Notice that in this version of the model, the number of chocolates decays more slowly, and has some of the granularity seen in the observed data plot.
Plus, the randomness means the specific behaviour varies from run to run - for example, the orange line decays relatively quickly, while the blue has a long stretch when nothing is being taken.
Interestingly, this model seems to have a linear decrease for the first ~20-30 time steps in every sample run. This is probably because, for those first few steps, everyone can get what they want. But eventually, certain types run out, and the rate of consumption decays away.
Why Do Chocoholics Come in Threes?
I wrote about the Poisson distribution before, back when I was modelling cafes. Basically, while we might see an average of one person per time interval, we don't expect to see them so evenly spaced out. Rather, people will arrive in 'bursts' of varying size. This behaviour is described by the Poisson distribution.
So I wrote my simulation in Python, using the numpy library for the Poisson distributed random numbers. The results look something like this
In this case, the initial decrease is less linear than in the simple model, but the overall decay times are roughly the same (except for the blue line above). This is what we'd expect since, in the above, I chose the average number of people per interval to be 1.
So yeah. Try the code, have a play with the parameters, see what happens. The output is the total number of chocolates remaining after each time step.
One thing to look at would be how changing the probabilities affects the rate of decay - for example, does the number of chocolates decay faster when all the chocolates are equally popular, or when some are much more popular than others. I suspect the former is the case.
It might also be worth adding an option where a person can pick again if their prefered chocolate isn't avaiable.
And the IgNobel Prize for Medicine goes to...
So that's that. And I can definitely see the original paper being nominated for an IgNobel, at the very least.
As for me, I have some gift wrapping I really need to get to...
Merry Christmas!
Citation
[1] P R Gajendragadkar, et al. "The survival time of chocolates on hospital wards: covert observational study" BMJ 2013;347:f7198 (Published 14 December 2013)
Oatzy.
[Next job, modelling Her Majesty's nuts...]
Labels:
analytical,
chocolate,
everyday maths,
maths,
modelling,
problem solving,
programming,
simulation,
sociology
Subscribe to:
Posts (Atom)


















































