JEG2

JEG2

Author of Designing Elixir Systems with OTP

I’ve been playing with several simulations for a while now, to learn more about how Elixir’s processes work. Now I am cleaning up some ideas in preparation for a training at ElixirConf.

I’ve seen multiple things that work well and plenty that don’t. I have a lot of ideas about what I want to show and talk about. However, I would love to see how others would approach the same challenges. Maybe your ideas are even better than mine!

If you’re interested in contributing, I would love to hear how you would design a solution to the following problem. Don’t feel like you have to write code. You can just describe what processes and data structures you would use.

I may have followup questions, depending on how this goes. :smiley:

Here’s the problem:

Draw a 64 lily pads wide by 128 lily pads high pond with 3,000 turtles and 3,000 frogs distributed randomly across them. No critter should share a pad. Each critter periodically counts surrounding neighbors of the same type. If the same type count is less than 30% of the total number of neighbors (up to eight), the critter will try to move to an open pad in a random direction, some random distance between one and five steps away.

Thanks in advance for any input you provide!

Showing Posts 1 to 10

terakilobyte

terakilobyte

Do they all evaluate the 30% rule at the same time or 1 by 1?

If they are below 30% but one of their species jumps in and raises above that threshold, should they still move or no?

Qqwy

Qqwy

TypeCheck Core Team

All right, let’s give this a go.

64*128 + 3000+3000 = 14192. This is quite a lot, but I think I would still go for the full ‘everything is a process’ approach here, because it makes it the most easy to extend the system in the future (e.g. adding extra logic to a lilypad, adding extra logic to either of the critters, the moving logic, et cetera).

Every Lilypad is a process

Every Lilypad is a process. These are started in two steps by the main process (the ‘world’):

  1. Fill a map with the {x,y} tuples as keys, and their values will be the PIDs of the Lilypond-process at that location. (optionally passing each of them their position at startup).
  2. Connect all Lilypond processes, by telling each of them the PIDs of their horizontal, vertical and diagonal neighbours. When passed these PIDs, a Lilypad will start monitoring them.

When a Lilypad process crashes, this is seen by its neighbours, and it is then removed from their neighbours-list, so no critter can try to travel to a Lilypad that doesn’t exist anymore.

The World is linked with trapping exits to the Lilypads, so if one of them crashes, a new one is started that responds to the same location, which is then added to its neighbours (and the neighbours to itself).

Every Critter is a process.

Every Critter is a process. They are started by the world after the Lilypads have been generated. To do this, the map of (coordinates => pids) is shuffled, and then the first part of that is used as their new homes.

Critters are processes that determine ever so often if they want to move. They use Process.send_after to send themselves a message periodically to see if they are lonely (less than 30% of their eight neighbouring lilypads is taken. I in other words: less than three neighbours)

To check if they are lonely or not, they do the following:

  1. Ask the Lilypad they are standing on for a list of its neighbours
  2. Ask each of these neighbours if it contains something.

If there isn’t enough, we have a list of neighbours that are empty, so we can pick one at random and travel in that direction. After traveling, we repeat this procedure, with the only exception being that the connection we travel in is now fixed (so if we cannot travel further in a certain direction because a Lilypad is already taken, we stop).

‘Travel’ means that the Critter unlinks and deregisters itself from the Lilypad it came from, and links+registers itself to the Lilypad it went to.

Problems with this approach

The only ‘glaring’ problem with this solution is what happens when a Lilypad with a critter on it crashes: Right now, the Critter is not restarted, it just ‘drowns’(disappears) when the Lilypad it stood on ‘sinks’(crashes). This might be prevented by having the Critter<->Lilypad connection use monitors instead, and have a Critter that had to swim because the Lilypad it stood on does no longer exist periodically ping the World process to ask if there is already a new Lilypad for this location.

To simplify all of the ‘check if a Lilypad exists at a certain location’ checks, one could use name registration for the processes. However, with the amount of processes we have here, this might actually slow the system down. So this is something we should measure before we go one way or the other.

edit: as suggested below, this answer also has problems with drawing the resulting state.

Why this approach?

  • All of the components are easily extendable.
  • When one Lilypad or Critter dies, the rest of the system is unaffected.
  • There is no single-process bottleneck. The World process is only used during startup and during ‘where is the new lilypad’ resolution in the case one of them has crashed and needs to be restarted.
JEG2

JEG2 OP

Author of Designing Elixir Systems with OTP

I’ve purposely tried not to specify timing, because I feel like that’s a very interesting variable here. I’m open to either approach.

Yes, next time the critter checks they would decide they are now unhappy and seek to move.

JEG2

JEG2 OP

Author of Designing Elixir Systems with OTP

This is a neat approach.

It does change the way the simulation works a little bit from the way I have seen it done in the past. I have seen a critter jump out of a group when surrounded (because they can move up to five spaces). Using your approach though, the intervening pads must also be open.

I do think this is a cool aspect of your processes-as-a-datastructure solution.

However, I think there’s one aspect you haven’t yet considered: how do we manage the drawing of the current state of the pond?

bbense

bbense

However, I think there’s one aspect you haven’t yet considered: how do we manage the drawing of the current state of the pond?

MOAR PROCESSES…

The View of the pond is essentially a log of all the state changes. The tricky problem is ordering these state changes. One approach would be to use mnesia to create a virtual shared log, another approach would be to simply timestamp each state change and use standard logging services. The recent changes to time in Erlang 18 ( i.e. an absolute incremental timer) would be useful.

The current process model emits events, you need a separate set of processes to consume and visualize the events.

gregvaughn

gregvaughn

I could do it in a single process. I’ve done Conway’s Game of Life that way. This would be my approach if I were to interpret the critter movement as being a synchronized “turn”.

However, I suspect you’re looking to use this as an exercise in concurrency, so plan B would be to treat each critter as a separate process. I’d expect to have the lily pad map stored in an ETS table, likely with a helper function to get stats of what’s in neighboring cells. One other separate rendering process can get a snapshot of the ETS data and display it on a timed interval.

JEG2

JEG2 OP

Author of Designing Elixir Systems with OTP

Any thoughts on the differences of using ets verses an Agent for this? Just curious.

One problem I’ve seen some, using similar approaches to this, is the need to copy the current state into a rending process. A 64 by 128 pond is over 8,000 pads. Depending on the data structure, this memory copy can take time, enough to slow the rendering refresh rates down.

JEG2

JEG2 OP

Author of Designing Elixir Systems with OTP

14,192, actually.

gregvaughn

gregvaughn

ETS can allow parallel reads (IIRC). An Agent is going to serialize all reads. I haven’t done much with ETS yet, but this could be a great place to use match specifications. I need to try to find some spare time to try to code some of this. It’s starting to sound like fun :slight_smile:

I’m getting all hand-wavey now because my knowledge is not that concrete. ETS tables can have a single owning process that is the only one allowed to write. This could be the one responsible for rendering. Each critter process would have to send a message in order to move. The receiving process could have a cached version of the data optimized for rendering that it updates as well as updating ETS.

I’m just guessing here without perf numbers, but that render-optimized data structure might be best served as an Erlang array because it needs random access updates.

JEG2

JEG2 OP

Author of Designing Elixir Systems with OTP

Question for the Processes All The Way Down Team: it seems that the only state we need to store is the current location of the critters and we could choose to keep that in critter processes or lily pad processes, so is there really a good argument for doing both?

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews