In the previous post on the topic we looked into how to combine ingredients such that they fulfill various (linear) nutritional (lower- and upper-) bounds while optimizing some (linear) objective function such as total cost of the ingredients. We have seen that Linear Programs are a great way to describe these sorts of problems and shown how to use a solver library for rust to practically do it for a toy problem.

While it can be argued this is already on the mathematically more sophisticated side of dog food preparation, we’ll push it a tiny bit farther. In this post we’ll challenge a bit our modelling (in particular the objective function) and look into solver libraries for rust that allow for more general problem formulations.

So far we assume the objective is to minimize (or perhaps even maximize) some property of the ingredients we select, such as total cost. At the same time we assumed that when the optimal diet required us to gauge 47.523g we would in practice be able to do this with “sufficient” precision as to not mess up the nutrient constraints.

Together these lead to a slightly weird situation: Let’s say wlog that our objective minimizes, then our optimal solution will be in some intersection of lower bounds for nutrients. Now if while measuring our dog food, we are a bit too generous, that’s likely fine (to a point), but any gram too little would violate our nutrient constraints.

Figure 2 shows a possible way out: We can tighten the constraints a bit to create a “safety margin” (dashed) around our constraints which would give us some leeway while measuring bonemeal with the kitchen scale. Depending on where you get your nutrient bounds from, this has perhaps been already done for you to some degree (in which case you likely don’t know the actual, hard constraints).

Since some ingredients (such as meat) come in larger quantities and others (such as supplements) come in much lower, the margin “width” would ideally take that into account. Also some nutrients have a wider area compared to their masses as its very hard (but not impossible) to overdose on say some vitamins, while it may be very easy for others (not in the figure). So the “opposing” bound would also need to be taken into account as to not render the problem *infeasible *while leaving no space for a solution.

However some questions remain: How big is this resulting leeway for any given *Ingredient *in each direction (overdose/underdose)? In the figure see for example the blue point that represents the “safe” optimal solution and the blue line showing the leeway for Ingredient 1. With the known bounds and some math this can certainly be computed.

But what if what we want is actually not so much to optimize some cost, but rather want the maximum ease during meal preparation? Is there a way to move this blue dot to the “center” such that the leeway in both directions is maximal for each ingredient?

Consider Figure 3: For maximal measuring leeway, we would instead like to inscribe into our or feasible region a box (more specifically, rectangle in 2d-case, cuboid in 3d and so on). To ensure this box is as large as possible, we’ll want to maximize its area (volume). You could perhaps prefer other criteria here such as maximizing the leeway for the smallest-leeway-ingredient or something along those lines and use that interchangeably.

\(l_i\) | Lower Box side for Ingredient \(i\) |

\(u_i\) | Upper Box side for Ingredient \(i\) |

\(A_{ij}\) | Amount of Nutrient \(j\) in Ingredient \(i\) |

\(a_j\) | Lower bound for Nutrient \(j\) |

\(b_j\) | Upper bound for Nutrinet \(j\) |

Tab. 1: Notation

\(
\max \prod_{i} u_i – l_i \\
\textrm{s.t.} \\
\begin{array}{lllll}
a & \le & A l \\
& & A u & \le & b \\
0 & \le & l & \le & u \\
\end{array}
\)

Eqn. 1: Problem formulation.

Fig. 3: Maximum “leeway box” inside the feasible region polytope.

This formulation, loosly inspired by (a brief paragraph in) [B19] should be easy enough to understand: We \(\max\)imize the product of all the edge lengths (upper box side minus lower box side), i.e. the volume. Constraints are that the lower (ingredient box) edges shan’t violate the lower (nutrient) bounds and the analogue for the upper edges. And of course upper edges should be higher values than lower edges and there should still be no negative ingredients amounts.

There isa couple of libraries out there for Rust that give you access to linear and non-linear solvers of various sorts. The table below shows a comparison which is by no means fair or complete (eg. argmin contains a lot of algorithms while others on this list contain very few or only one).

In our case, we (still) have linear constraints (our nutrient bounds), but our objective function is non-linear. Since it is however quadratic, it qualifies as convex and could for example be solved neatly with totsu. In our example we opted instead for cobyla which is slightly more general and thus allows us to experiment with our target function more freely.

Variable Constr. | Linear Constr. | Convex Constr. | Arbitrary Constr. | Linear Objective | Convex Objective | Arbitrary Objective | Derivative-Free | |
---|---|---|---|---|---|---|---|---|

good_lp | ✓ | ✓ | ✓ | – | ||||

totsu | ✓ | ✓ | ✓ | ✓ | ✓ | – | ||

argmin | ✓ | ✓ | ✓ | (✓) | ||||

gomez | ✓ | ✓ | ✓ | ✓ | ✓ | |||

ipopt-rs | ✓ | ✓ | ✓ | ✓ (2x diffable) | ✓ | ✓ | ✓ (2x diffable) | |

cobyla | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |

Again, the full source code is available on Github, here is the relevant solving function for Cobyla:

```
use cobyla::{fmin_cobyla, CstrFn};
use crate::diet_problem::DietProblem;
// Our variables are one flat vector of values,
// we interleave lower and upper rectangle sides for each of the dimensions,
// these compute the indices into the variable vector.
pub fn lower(i: usize) -> usize { i * 2 }
pub fn upper(i: usize) -> usize { i * 2 + 1 }
pub fn optimize_cobyla(problem: &DietProblem) {
// Cobyla constraints are CstrFn (constraint functions) defined
// as Fn(&[f64]) -> f64, in our case, closures (which each have their own unique concrete type, hence the dyn).
// In order to make these closures live longer than the scope they were defined in (which is
// the loop body), we need to put them in a box.
let mut boxed_constraints = Vec::<Box::<dyn CstrFn>>::new();
// Create a constraint for each lower nutrient bound
for (nutrient, minimum) in &problem.minima.nutrients {
// Make a closure that computes by how much we violate this bound
// and put it into the box. Move in the ref to `problem`.
let constraint = Box::new(move |x: &[f64]| {
// Sum up over all ingredients i
// The amount of nutrient `nutrient` in that ingredient times the lower rectangle
// side of ingredient i
let s: f64 = problem.ingredients
.iter()
.enumerate()
.map(|(i, ingr)| { ingr.nutrients.get(&nutrient.clone()).unwrap_or(&0.0) * x[lower(i)] })
.sum();
// ... and demand it is >= the minimum
f64::min(minimum.clone() - s, 0.0)
});
boxed_constraints.push(constraint);
}
// Analog for upper bounds
for (nutrient, maximum) in &problem.maxima.nutrients {
let constraint = Box::new(move |x: &[f64]| {
let s: f64 = problem.ingredients
.iter()
.enumerate()
.map(|(i, ingr)| { ingr.nutrients.get(&nutrient).unwrap_or(&0.0) * x[upper(i)] })
.sum();
f64::min(s - maximum, 0.0)
});
boxed_constraints.push(constraint);
}
// Cobyla expects a Vec<&dyn CstrFn> which we can now obtain by "unboxing" the
// `boxed_constraints`.
// &** is because "reference to box of CstrFn" -> "reference to CstrFn"
let constraints: Vec<&dyn CstrFn> = boxed_constraints.iter().map(|b| &**b).collect();
fn cost(x: &[f64], _data: &mut ()) -> f64 {
// Compute volume
let mut volume = 1.0;
for i in 0..(x.len() / 2) {
// x has, for each ingredient, a lower bound and an upper bound
// volume is the product of all of those that are actually included
if x[lower(i)] != 0.0 && x[upper(i)] != 0.0 {
volume *= x[upper(i)] - x[lower(i)];
}
}
volume
}
// For each dimension/ingredient we have a left- and right box side
let mut x = vec![0.0; problem.ingredients.len() * 2];
// Actually run cobyla with some parameters
let (status, x_opt) = fmin_cobyla(cost, &mut x, &constraints, (), 0.5, 1e-4, 2000, 1);
println!("COBYLA status {:?} x_opt {:?}", status, x_opt);
for (i, ingredient) in problem.ingredients.iter().enumerate() {
println!("{:20}: {:5.2} .. {:5.2}", ingredient.name, x[lower(i)], x[upper(i)]);
}
}
```

[B19] Mehdi Behroozi. *Largest Inscribed Rectangles in Geometric Convex Sets*. 2019. https://arxiv.org/abs/1905.13246.

In this post we’ll look into how to make your own dog food, seen as an optimization problem. I’ll give a short background intro into this way of feeding your dog (called BARF), which I’m by no means an expert in. What you thus can *not *expect from this article is specific guidance on what the best nutrition for your dog is.

But maybe I can make you a bit curious about linear optimization.

There are various ways to feed your dog, from dried and canned food of various sorts and qualities over half-prepared meals and cooking yourself to BARF.

BARF originally refers to “Bone and Raw Food” but nowadays is usually retrofitted as meaning “Biologically Appropriate Raw Food”. As both names imply, the diet revolves around raw ingredients. While ready-to-thaw BARF mixes exist, a lot of people mix fresh ingredients for individual meals of their pets.

Reasons for a BARF diet are plentiful and so are reasons to avoid it. A full discussion would derail the scope of this article, but if you are interested to know more, I can only urge you to weigh all the pros and cons and listen to the vet of your trust perhaps a little more than the friendly BARF store owner. A really complete and differentiated view of the topic can be found in the works of Julia Fritz [F18] (German). Most importantly, do not take nutrient limits lightly. It turns out, compared to the amount of food they eat, dogs need much more nutrients than humans or wolves.

We will consider the case of preparing a meal for your dog (or any other animal, really) with certain nutrient constraints from a, lets say, somewhat theoretical standpoint.

At our disposal are a certain number of *Ingredients *such as Beef, Apples, Bone, whatever have you. Each ingredient has a specific (linear) cost attached to it, which may be its price per kilogram or simply 0 in case we don’t care. An ingredient (obviously) cant be negative, having -20g of something in a meal simply does not make sense.

Each such ingredient holds some amount of *Nutrients* such as Fat, Protein or Vitamin A. For our purposes we can also treat properties like energy content and mass as nutrients, as they behave the same in our calculations. Nutrients scale linearly with its mass. I.e. if 100g of Fumblesauce contains 20g of Fat, then we assume 1kg will contain 200g and so forth. We also assume we can meter our ingredients to sufficient precision so that obtaining 123.4567g of Gobblermeat or something “close enough” is possible (we might get back to this in a future post).

Note that units don’t matter so much here but are just for illustration. As long as we keep it consistent, we can even have different units for measuring the amount of each nutrient; but of course in all contexts in which we use that nutrient, we have use the same scale. To make this article simpler we’ll completely ignore units here, but when you actually work with the data you may want to spend a minute or two to think about them.

Our goal is, of course to obtain a meal (that is a combination of ingredients of some amounts), that fulfills a certain minimum and maximum amounts for certain ingredients. Some of these may have only a lower OR upper bound (i.e. it may not matter if you have too much of some specific Vitamin), some may be bounded from above and below.

Table 1 shows some notation for Variables. Equipped with these, we can formulate or problem as a Linear Program (right next to the table). Note the term “program” may be a bit misleading here (but that’s what its called). It is nothing more than an optimization criterion (minimize the price) and some inequality constraints.

\(x_i\) | How much of ingredient \(x\) to take (what we want to find out) |

\(A_{ij}\) | Amount of nutrient \(j\) contained in ingredient \(i\) |

\(a_j\) | Minimum amount of nutrient \(j\) (lower bound) |

\(b_j\) | Maximum amount of nutrient \(j\) (upper bound) |

\(c_i\) | Cost of ingredient \(i\). \(c^T x\) is called the Objective Function. |

Tab. 1: Notation for the Dog Diet Problem

\(
\min c^T x \\
\textrm{s.t.} \\
\begin{array}{lllll}
a & \le & Ax & \le & b \\
0 & \le & x \\
\end{array}
\)

Eqn. 1: Linear Program for the Dog Diet Problem.

Fig. 2: Illustration of the Dog Diet Problem for 2 ingredients.

Linear Programs are nice, because they can in practice be solved efficiently and many highly optimized solvers are available. There is enough to be said about linear programs to fill a entire books about them (and people have, there is a lot of content to find), but for our purpose it will be enough to know there are black boxes out there for most programming languages / computation environments that solve these kinds of problems very efficiently.

It reads like this: We want to minimize the product of the cost vector \(c\) (eg monetary cost of the ingredients) and our variable ingredient amount vector \(x\) subject to having the product of the ingredient-to-nutrients-amount-matrix \(A\) and the ingredient amount \(x\) (that is the total number of nutrients in the end) be constrained to be between \(a\) and \(b\). Also, there shant be negative amounts of ingredients so \(x \ge 0\). As a side note, there is always an optimal solution in some “corner” (intersection of constraints), Figure 2 might provide some visual intuition to convince you (actual proofs for this exist of course).

First, we create a couple of structs to hold our data:

```
#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
pub enum Nutrient {
Energy,
Protein,
Calcium,
// ...
}
// ...
#[derive(Default)]
pub struct Ingredient {
pub name: String,
pub nutrients: HashMap<Nutrient, f64>,
}
// ...
pub struct DietProblem {
pub minima: Ingredient,
pub maxima: Ingredient,
pub ingredients: Vec<Ingredient>,
}
```

So a *Nutrient *for us is an enum that tells us which nutrient we are talking about, an *Ingredient* in contrast has an actual name and a number of nutrients with amounts. The problem we want to solve is a *DietProblem *which has some lower and upper bounds of nutrients (expressed as Ingredients) and a vector of ingredients to choose from.

I also could not resist to add a little macro to allow to define them conveniently:

```
let minima = ingredient!{ (min) Energy: 1000.0, Protein: 100.0, Calcium: 1.0 };
let maxima = ingredient!{ (max) Energy: 1100.0, Protein: 200.0, Calcium: 2.0 };
let mut d = DietProblem::new(minima, maxima);
d.add(ingredient!{ (GobblerMeat) Energy: 1.25, Protein: 0.28 });
d.add(ingredient!{ (BulbFruit) Energy: 1.0 });
d.add(ingredient!{ (FrobnizerBone) Energy: 1.0, Calcium: 0.01 });
```

Admittedly, that might be a bit on the questionable side but it **is **readable and super fun to write.

For the actual solving the use the Rust crate good_lp which wraps a couple of LP solvers and has a pretty nice API:

```
pub fn optimize_good_lp(problem: &DietProblem) {
// Good LP variables. These are the values we want to optimize,
// in our case amounts of ingredients.
let mut variables = variables!();
let mut amounts = HashMap::<String, Variable>::new();
// Good LP expressions that express how much of each nutrient
// our solutions will have.
let mut nutrient_sums = HashMap::<Nutrient, Expression>::new();
for ingredient in problem.ingredients.iter() {
// Create variable tracking how much of this ingredient we'll use
let amount = variables.add(variable().min(0));
amounts.insert(ingredient.name.clone(), amount);
// Extend nutrient expressions
for (nutrient, contents) in ingredient.nutrients.iter() {
*nutrient_sums.entry(*nutrient).or_default() += amount * *contents;
}
}
// Here: Maximize "0" (i.e. just find any solution that obeys the constraints).
// This could instead be minimizing the cost of the ingredients or
// something along those lines
let mut lp = variables.maximise(0).using(default_solver);
// Add constraints: We want at least all the nutrients in
// the `problem.minima` "ingredient"
// and most those in `problem.maxima`.
for (nutrient, contents) in problem.minima.nutrients.iter() {
lp.add_constraint(
constraint!(nutrient_sums[&nutrient].clone() >= *contents)
);
}
for (nutrient, contents) in problem.maxima.nutrients.iter() {
lp.add_constraint(
constraint!(nutrient_sums[&nutrient].clone() <= *contents)
);
}
// Actual work happens here:
let solution = lp.solve().unwrap();
}
```

The full source code can be found in https://github.com/Droggelbecher/dogfood, enjoy!

[F18] Julia Fritz.* Hunde barfen*. 2018. 2. Auflage. ISBN 978-3-8001-0924-1, Ulmer-Eugen Verlag.

Because we can. Thats why.

```
#!/usr/bin/env python
import pickle
import argparse
import subprocess
import sys
import numpy as np
from matplotlib import pyplot as plt
plt.style.use("dark_background")
def plot(f, *, xmin=-5, xmax=5, ymin=-5, ymax=5, levels, dpi=200):
dpi = float(dpi)
fig, ax = plt.subplots(dpi=dpi)
xs = np.linspace(float(xmin), float(xmax), 1024)
ys = np.linspace(float(ymin), float(ymax), 1024)
X, Y = np.meshgrid(xs, ys)
ns = {**np.__dict__, **dict(x=X, y=Y)}
zs = eval(f, ns)
if callable(zs):
zs = zs(xs, ys)
if levels is not None:
if len(levels) <= 2 and isinstance(levels[0], str):
n = 15
if len(levels) >= 2:
n = int(levels[1])
functions = {
'linear': [lambda x: x, lambda x: x],
'square': [np.sqrt, lambda x: x**2],
'cubic': [lambda x: x**(1/3), lambda x: x**3],
}
f_inv, f = functions[levels[0]]
levels = np.linspace(
np.sign(zs.min()) * f_inv(np.abs(zs.min())),
np.sign(zs.max()) * f_inv(np.abs(zs.max())),
n,
endpoint=False
)
levels = np.sign(levels) * f(np.abs(levels))
c = ax.contour(X, Y, zs, levels, cmap='summer')
else:
c = ax.contour(X, Y, zs, cmap='summer')
ax.clabel(c, inline=True, fontsize=10)
ax.set_box_aspect(1)
for spine in ax.spines.values():
spine.set_edgecolor("grey")
spine.set_linewidth(2)
ax.set_aspect("equal")
ax.grid(color="grey", alpha=0.5)
fig.savefig("/tmp/plot_contour.png", pad_inches=0.1, bbox_inches="tight")
subprocess.call(
["kitty", "+kitten", "icat", "--align", "left", "/tmp/plot_contour.png"]
)
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument('--xlim', default=[-5, 5], nargs=2)
p.add_argument('--ylim', default=[-5, 5], nargs=2)
p.add_argument('--levels', nargs='+', default=None)
p.add_argument('--dpi', default=200)
p.add_argument('function')
args = p.parse_args()
plot(args.function, xmin=args.xlim[0], xmax=args.xlim[1], ymin=args.ylim[0],
ymax=args.ylim[1], levels=args.levels, dpi=args.dpi)
```

]]>We consider an issue that appears frequently in game development and simulation projects: We have many moving points in a 2d world and need to find out which points are in range of which other points. We’ll call this the “range query problem” here (even though that is a bit imprecise). This has several applications:

- Detecting collisions of units (either directly or as a preprocessing step before doing more complex polygon intersection operations)
- Figuring out which units are affected by weapons range or areal effects.
- Detecting partners for chemical or other interactions in simulations, perhaps also useful for simplifying simulation of gravity effects

The techniques discussed here easily extend to any number of dimensions (curse of dimensionality may apply though), but for this discussion we’ll focus on the 2d case. Some of the discussed methods (in particular Spatial Hashing) extend easily to arbitrary shapes but for simplicity we’ll stick to circular or square radii. Figure 1 illustrates the situations: Many points are on the plane and we are interested in the points inside the query area (blue dashed circle) which is centered around the query point (blue point).

We’ll briefly introduce how you would address this issue in the most straightforward way in any Entity Component System (ECS), briefly touch on the pros and cons of tree-based methods and then dive a bit into Spatial Hashing and how one could implement such a thing in Rust (for use in Bevy in particular). In the end we’ll spend some time comparing the ECS approach and Spatial Hashing in terms of performance to provide some intuition on when it might be worth it to use one over the other and what good parameter ranges could be.

An Entity Component System or “ECS” for short is a design pattern that comes in handy in game development, when you have to deal with a lot of objects (*Entities*), each of which has some subset of a range of properties (*Components*). I.e. some can move, some can be drawn to the screen, some have a health bar, some can be selected by the user, etc…). If a lot of different combinations of these exists among your objects, a class hierarchy might just not be the best approach to express this. An ECS approach can help in that case.

ECS’s do a lot of useful things that I wont get into in the scope of this post (there should be plenty of resources about it on the net), but one thing that I do like to mention is that they will typically story data about their entities in a very specific way: Instead of packing all data together with the entity in a struct or class like so:

```
struct Entity {
position: Optional<Vec2>; // Not all entities have a position
name: Optional<String>; // ... or a name ...
animation: Optional<ImageHandle>;
// ... whatever other components you may think of ...
}
```

Instead of such a vector of entity objects, ECSs will do something more similar to the code below. That is, it will store the same kind of component from the different entities together:

```
type Entity = u32;
struct Entities {
positions: Vec<Optional<Vec2>>,
names: Vec<Optional<String>>,
animation: Vec<Optional<ImageHandle>>,
// ... whatever other components you may think of ...
}
```

This allows very performant access if you work with only a few components, but want to touch most or all of the entities that have them (this is what the *Systems *in ECS do). I.e. for each entity you want to read its speed vector and update its position from it. This would require iteration along only two vectors which is both space efficient and sequential and thus very cache friendly. And it turns out, caches these days are so fast that for many practical problems they are more important than asymptotic complexity *[citation missing]*.

This is still a blatant oversimplification on how ECSs store their data, in reality its much more clever (see for example https://docs.unity3d.com/Packages/com.unity.entities@0.1/manual/ecs_core.html on *Archetypes*), for our discussion here it is enough to understand that all the positions of our objects will be tightly packed in memory and thus an exhaustive scan will be rather efficient. Using this directly for spatial range queries is betting everything on efficient caching mechanisms at the cost of completely ignoring asymptotic complexity. Or to put it even simpler: We gladly do way too many operations but thanks to caching they will also be super fast.

We already mentioned it above; the ECS we’ll be using here (for both the “ECS approach” and as a basis for Spatial Hashing) is Bevy.

A common approach to spatial data structures is to form some sort of tree such as a K-D-Tree (in our case with K=2), Quad-Tree, R-Tree, etc… (the list is virtually infinite). Since I’m trying to argue about a rather large class of data structures here in one go, this is gonna be very hand-wavy, but I hope I can convey you a gut feeling.

The general idea is usually the same: You partition the space in some simple way, say by splitting it in half (for some definition of half \(\rightarrow\) that’s a K-D-Tree) or into quadrants (Quadtree), then you repeat for each part until the resulting spaces are “small enough”. The result then looks something like Fig. 2, i.e. the data structure is more fine-grained where the points are denser and coarser where there are only little points. This then gives you something like a \(\Theta (\log n)\) insert/update/lookup and also rather efficient range queries.

The big strength for these class of data structures is their flexibility: You don’t need to know anything about the distribution of your points or what ranges your queries will be; a tree structure will usually adapt very nicely to whatever you have. There is two downsides to this: Your data access will usually not be very sequential so this is not cache-friendly in the common case, the other is maintaining & navigating these structure costs time: Most of the operations will be \(\Theta (\log n)\). This will often hold even if you’re updating a point that you know everything about that there is to know, which in the ECS case would cost no more than \(\Theta (1)\).

Grid methods simplify the idea of tree-based methods in the sense, that they sometimes can be considered a single layer of a spatial tree. In general, we divide our space (in our case 2d) into square cells of size \(h \times h\) and then have some efficient ways of figuring out a) which cells we need to consider and b) to keep track of what is in each cell. Plenty variants of this pattern exist and it seems the nomenclature is much less well defined than for tree structures. We’ll just give one definition here so we have something to talk about, but just be aware this is not the only way to do it (in particular wrt the choice of data structures being used).

We assume here each point has some sort of ID which we’ll call `Entity`

or `EntityID`

. Each cell is represented as `Cell = HashMap<Entity, Position>`

that lets us quickly check whether an Entity is in the cell and where exactly it is. If we want to find specific points in the cell, we can just iterate the whole hashmap (\(\Theta (n)\)) and a known entity can be removed in \(\Theta (1)\).

Cells themselves are managed in a `HashMap<CellIndex, Cell>`

that maps the integer cell coordinate (eg. `(4, 7)`

) to the cell contents (if the cell contains anything at all).

Figure 3 illustrates how this can be used for fast queries: We first identify the cells which overlap with the query area (here any cell that has part of the blue circle) and then scan each cell for all points that are precisely in the area (by iterating through the `HashMap<Entity, Position>`

. It should be apparent that this needs to check way fewer points than the ECS method above so has the potential of being much faster. This saving however comes at some maintenance cost: Whenever an entity moves, we need to check whether it has crossed a cell boundary. If so, we need to remove it from one cells hash map and put it into the next one.

It is worthwhile to note that the choice of \(h\) here should be dependent or \(r\), which implies that wildly changing query radii can not efficiently be supported with a single Spatial Hashmap. In the interwebz I found recommendations like setting eg \(h=2r\). Which intuitively makes sense: In this case we would never need to query more than 9 cells (see eg Fig. 4 where 8 need to be queried). This is not necessarily optimal though, as it depends on the relative cost of accessing a more cells (small \(h\)) vs scanning extra points because of too large cells (large \(h\)). We’ll know at least a bit more at the end of the comparison chapter below.

- “SimonDev” video on spatial hashing in JS. In this variant, points have a size so can be in multiple cells at once.
- “10 Minute Physics” video on a compact spatial hashing implementation.

The basic data structures are `SpatialHashmap`

, and `Grid`

. The latter holds the chosen grid spacing together with some simple methods to convert between coordinates and cell indices which we shall skip here (full code will be attached below). The `SpatialHashmap`

also holds a `hash map`

attribute which maps a 2d grid position (`IVec2`

) to a second hash map which maps an `Entity`

to its precise position (`Vec2`

).

```
#[derive(Debug)]
pub struct SpatialHashmap {
pub grid: Grid,
hashmap: HashMap<IVec2, HashMap<Entity, Vec2>>,
}
#[derive(Debug, Clone, Copy)]
pub struct Grid {
pub spacing: f32,
}
```

Inserting an Entity into the hash map is straight forward: Compute the relevant grid cell and insert the entity together with its position into the nested hash map. In case that nested hash map does not exist yet (because the cell was so far unused), create one (that’s done by the `or_insert(...)`

part).

```
pub fn insert(&mut self, position: Vec2, entity: Entity) {
let index = self.grid.index2d(position);
self.hashmap
.entry(index)
.or_insert(default())
.insert(entity, position);
}
```

Updating is only slightly more involved. In case the entity moved across a cell boundary, we need to remove it from the old cell and insert it into the new one, otherwise, updating the position of the entity in its cell is enough:

```
pub fn update(&mut self, entity: Entity, previous_position: Vec2, new_position: Vec2) {
let prev_index = self.grid.index2d(previous_position);
let new_index = self.grid.index2d(new_position);
if new_index != prev_index {
// If old cell exists, remove entry from it
self.hashmap.entry(prev_index).and_modify(|h| { h.remove(&entity); });
}
// Ensure new cell exists and insert entity into it
self.hashmap.entry(new_index)
.or_default()
.insert(entity, new_position);
}
```

In principle we want to allow different kinds of range query type: You might be interested in the square around the query point or the circle or some other weird shape (e.g. your collision polygon). This we encode with the `Query`

trait:

```
pub trait Query: Debug {
fn first_cell(&self, grid: Grid) -> IVec2;
fn next_cell(&self, cell: IVec2, grid: Grid) -> Option<IVec2>;
fn in_range(&self, position: Vec2) -> bool;
}
```

The implementing struct will hold the query position and whatever extra parameters (eg radius) it needs and tell us:

- The first grid cell to search (
`first_cell`

) - How to find the next cell to investigate from a given cell, and when to stop (
`next_cell`

) - And, for any precise position, whether it is considered within range or not.

For doing our actual range query, we iterate over all candidate cells with `first_cell`

and `next_cell`

and check each entity in these cells with `in_range`

. The perhaps most straight forward solution here would be to allocate a vector and fill it with all those found entities. However we expect this code to be rather performance critical and are trying to beat a highly cache optimized ECS implementation so we avoid the memory allocation here and define our own `Iterator`

:

```
pub struct SpatialHashmapIterator<'a, Q: Query> {
query: Q,
current_cell: IVec2,
entity_iterator: Option<hash_map::Iter<'a, Entity, Vec2>>,
shm: &'a HashMap<IVec2, HashMap<Entity, Vec2>>,
grid: Grid,
}
impl<'a, Q> Iterator for SpatialHashmapIterator<'a, Q> where Q: Query {
type Item = (Entity, Vec2);
fn next(&mut self) -> Option<Self::Item> {
// If we have an entity iterator (i.e. we are in a valid cell),
// iterate until we find an entity that is in range or the iterator is exhausted.
if let Some(mut it) = self.entity_iterator.take() {
while let Some((&entity, &pos)) = it.next() {
if self.query.in_range(pos) {
// put back the iterator for next time
self.entity_iterator = Some(it);
return Some((entity, pos));
}
}
// No point in putting back an exhausted iterator
//self.entity_iterator = Some(it);
}
// Is there a next cell we should be looking at?
while let Some(next_cell) = self.query.next_cell(self.current_cell, self.grid) {
self.current_cell = next_cell;
if let Some(entities) = self.shm.get(&self.current_cell) {
self.entity_iterator = Some(entities.iter());
return self.next();
}
}
// No cells left to check, we have seen it all!
None
}
}
```

Its not quite something I would dare publish on crates.io yet, but if you are curious, you can find the full source code (including the plotting scripts for the comparison below) at https://github.com/Droggelbecher/bevy-spatial-hashing .

Method | Insert | Update | Query |
---|---|---|---|

ECS | \(\Theta(1)\) amortized | \(\Theta(1)\) | \(\Theta(n)\) but very cache local |

2-D-Tree [wp] | \(\Theta(\log n)\) on average | \(\Theta(\log n)\) on average | \(\mathcal{O}(\sqrt{n} + k)\) worst case |

Spatial Hashing | \(\Theta(1)\) amortized | \(\Theta(1)\) amortized | \(\Theta(k + r^2 \cdot d)\) \(\approx \Theta(k)\) for \(r = const.\) and homogeneous density \(d\) |

Table 1 shows a quick overview of the asymptotic complexities of some approaches. For ECS we assume basically a flat vector with entity coordinates and some \(\mathcal{O(1)}\) mechanism to get vector indices from entity IDs, so the costs boil down to appending to that vector (**Insert**), updating an element (**Update**) and scanning the whole vector (**Query**).

For the K-D-Tree with K=2, **Insert **and **Update **are \(\mathcal{O}(\log n)\) on average as it is a self-balancing tree. **Query **complexity is taken from the linked Wikipedia article. Note that \(k\) here refers to the number of found points and is just there to express that in case \(k \ge \sqrt{n}\), \(k\) would of course dominate the query time.

For Spatial Hashing we assume insert and update of the utilized hash maps to be \(\Theta(1)\) in the average case as is generally the case for hash maps. For **Query**, we for sure need to iterate through \(k\) found points, visit \(\Theta (r^2)\) grid cells, each containing some \(d\) (for *Density*) points. We assume on average that the fraction of query area over the area of totally queried cells is constant and thus is the fraction of found points over considered points is, assuming the density is homogeneous on average, thus, for a constant query radius \(r\), this boils down to \(\Theta (k)\).

Using Bevy 0.9 we created a simulation of 10000 points uniformly distributed on a rectangular 2000×1000 rectangular area. Points were continuously moving with a uniform random speed chosen from `(-10, -10)..(10, 10)`

, “bouncing off” the area boundaries when they touch them. In order to get rendering costs out of the equation, the simulation was completely headless (inspired by Bevy’s headless.rs example). Each frame, for each of the 10k points, all points within a \(2r \times 2r\) square centered around that point were computed with either scanning the ECS or the Spatial Hashing method. For each found point, `std::hint::black_box`

was called in order to prevent the optimizer from discarding the relevant loop code. Spatial Hashing used a configurable grid size \(h\). For reproducibility, the random seed was initialized to a constant at simulation start.

The app was compiled in release mode for the `x86_64-pc-windows-gnu`

target. After one second, the average frame duration was computed and written to `stdout`

together with the simulation parameters and the simulation for this combination of parameters ended. The simulation was run multiple times for different values of the query radius \(r\), the grid size \(h\) and whether or not to use Spatial Hashing.

Fig. 6 shows frame times on a logarithmic scale dependent on query square radius \(r\) and grid size \(h\).

With all methods a smaller query square radius leads to faster frame times due to simply fewer points being queried for. The ECS methods performance varies little with query square radius compared to Spatial Hashing. This is to be expected since the number of points to iterate through for this method does not change, but only the number of points processed.

Spatial Hashing in contrast is very dependent on query square radius. For all tested grid sizes, smaller query square radii were smaller. This effect is more pronounced than with the ECS method which is to be expected (there is fewer both fewer cells to check and fewer points to process). Perhaps a bit more surprising is that the optimal grid size for most radii seems to be around \(h=40.0\), rather than eg \(h=2r\). This suggests that in these observations the frame time is not dominated by the query process (which should behave optimal for a certain \(\frac{r}{h}\)), but rather the updating process.

Figure 7 plots the frame time by grid size rather than query radius and sheds some more light on this effect. Points move with an average absolute speed of \(\sqrt{5^2 + 5^2} = \sqrt{2}\cdot 5 \approx 7\), so for grid sizes that are close to (or even below) that value, a lot of points will change cells in every frame which costs a lot of time ^{1}. Then again for very large grid sizes, we spend more time during querying, as we have to iterate through a lot of points. In this case, for a uniform random speed in `(-10, -10)..(10, 10)`

, the sweet spot seems to be around \(h=40\), likely dependent on the point density (lower density mans we can “afford” larger cells as they will contain fewer points).

Figure 8 illustrates this effect: Lower densities / point counts lead to larger optimal grid sizes; the effect seems to be relatively weak however (i.e. moving from \(n=10000\) to \(n=80000\) only halves the optimal grid size). Of course in any real game the point distributions and movements speeds will not be this uniform so in practice you’ll have to do your own benchmarks. We can however still collect some takeaways that may transfer to real life to some degree:

- Spatial Hashing can give you a decent speed up over just scanning your ECS, especially when you have a lot of points to query.
- Choosing a good grid size can have a huge impact on performance.
- If you have few points, larger cells are optimal, but also the effect of cell size is not so strong so its ok to choose a too small grid size.
- If you have many (moving) points, the optimum grid size is mostly determined by point speed. Choose your grid size such that points do not cross cell boundaries too frequently, as the slope on the right of Figure 7 is shallow
^{2}, rather err on the side of a slightly too large grid.

1 A detailed probabilistic analysis is left as an exercise for the reader.

2 Unless the query radius is small compared to the average speed, see Fig. 7 for low \(r\).

]]>Nothing really changed here recently but since so far I didn’t really show this, here some pictures of the various solenoids that are driving the playing field action. (Including a rather creative solution for a missing pop bumper switch plastic)

Here some control electronics overview:

“Main control” controls the overall game logic (implemented in D) and sends commands via SPI to the other modules including the display modules (see below). Most flatband cables are SPI (incl. SS cables), the thick wires are mostly for solenoids and other higher-power consumers. I recon its far from ideal to put them together like this, but thanks to CRC and frequent resending, so far I had no practical problems with data loss (the longer cable towards the display is actually shielded).

The display boards extract bitmap information via SPI and pass on the part of the frame they are not responsible for to the next module. After a day of testing and fine-tuning timing and line termination, this now actually can be controlled by the main control board to display around 30 frames per second. There is still some flickering going on as I did not manage yet to automatically phase-align the LED matrix switching with the SPI “framerate”, maybe something for next time.

Some playing field updates. Last time we had already seen the LED stripe in action. New is in particular the Millenium Falcon, a repainted lamp from the same series as the death star.

The “stay on target” runlight from the back. In a way the 328p is certainly overkill, however for me that was the easier & more flexible solution compared to a 555 or similar.

Last, but certainly not least, we now finally have proper legs and aluminum borders

We found a linear model that could explain the costs to some degree, however it struggled to get right the more complex cost structure around eg. 0-mana or many-colered cards. In this post we make an attempt at a neural network model of MTG mana costs in order to address these nonlinearities.

In the previous post, I made a point out of wanting a model that is interpretable. Even though the relationship does not seem to be linear, there could be some approaches to make this happen such as including quadratic terms in the linear regression, or try to build a more informed/complex model that would try to capture our mental intuition on the mana price structure (and still makes sense to a human).

As it is somewhat easier and I had the emotional urge to want to try it, this post will not do any of those but try to learn a tensorflow model.

As we learned in the last post, the same card could have a cost of “one mana of each of the five colors” or lets say “2 green plus 5 of any color” and they would be in the same ballpark of “cheapness” (if this doesn’t make sense to you, take another look at that article). That is, if we are to make a model that from a description of the card would try to predict its mana cost directly it would have to make multiple predictions for different color combinations or, alternatively get the desired color combination as an additional input.

As we want to keep it simple, instead we will predict in a slightly different way: The model input will be a description of the card *including* the mana cost and the output will be a number in the range -1 (“too cheap”) to +1 (“too expensive”).

We build a simple dense network like so:

```
model = tf.keras.models.Sequential([
tf.keras.layers.Flatten(input_shape=(40,)),
tf.keras.layers.Dense(40, activation=tf.nn.relu),
tf.keras.layers.Dense(40, activation=tf.nn.relu),
tf.keras.layers.Dense(1, activation=tf.nn.tanh)
])
model.compile(optimizer='adam',
loss='mse',
metrics=['mse', 'mean_absolute_error'])
```

The mean squared error of the test set is 0.0827. Lets look at some concrete test set predictions (ordered by loss):

Name | loss | Prediction | Ground Truth |
---|---|---|---|

Knight of Meadowgrain-white | 0.00 | -1.00 | -1 |

Feral Abomination-black | 0.00 | -1.00 | -1 |

Bitterbow Sharpshooters-green | 0.00 | -1.00 | -1 |

Risen Sanctuary-green | 0.00 | -1.00 | -1 |

Ambush Viper-green | 0.00 | -1.00 | -1 |

… | |||

Raging Bull | 0.01 | 0.08 | 0 |

Risen Sanctuary+green | 0.01 | 0.92 | 1 |

Alaborn Grenadier | 0.01 | 0.09 | 0 |

Coral Eel | 0.01 | 0.10 | 0 |

… | |||

Golgari Longlegs-green | 0.23 | -0.52 | -1 |

Hobgoblin Dragoon | 0.29 | 0.54 | 0 |

Minotaur Aggressor-red | 0.41 | -0.36 | -1 |

Goblin Champion | 0.41 | 0.64 | 0 |

Feral Abomination | 0.56 | -0.75 | 0 |

Phyrexian Walker | 0.88 | -0.94 | 0 |

Ornithopter | 0.96 | -0.98 | 0 |

As before, lets take a look at a few of the worst predicted cards:

Pred.: -0.98 (cheap) GT: 0

Pred.: -0.94 (cheap) GT: 0

Pred.: -0.75 (cheap) GT: 0

Ornithopter, again! As with our linear model, this one seems incredible hard to predict. Indeed if we take a look at all the creatures with cost 0, both Ornithopter and Phyrexian Walker are rather good value for the money, so we can not really blame our model (note “Shield Sphere” was not considered as it has more complex ruling text). Similarly, with Feral Abomination, there are few examples for “big” deathtouch creatures.

Sorry for the weird query string in this URL, it was the easiest method I could find to exclude the many cards with complex rulings that were not considered.

It so happens that out of these four the only comparable card (Vampire Champion) is also in the test set, so the data sparsity problem strikes again. If we go farther up the list we already see the loss decreasing pretty quickly (Goblin Champion is already at 0.41), and since we did not spent a lot of time yet fine-tuning the model, there is probably a lot of potential to reduce these further.

Just out of curiosity, lets look what our model made out of some of the problem cases from last post. All these are taking with a grain of salt though as these ended up in the training set.

That is, the model could have memorized / overfit on these specific cards. The reason for the new train/test split is new magic cards came out in the mean while and the mtg-json API changed, so even with a fixed random seed I got a different train/test split.

Name | loss | Prediction | Ground Truth |
---|---|---|---|

Merfolk of the Depths-green | 0.12 | -0.65 | -1 (*) |

Merfolk of the Depths+white | 0.00 | 1.00 | +1 |

Merfolk of the Depths | 0.00 | -0.02 | 0 |

Wall of Torches-red | 0.00 | -0.96 | -1 |

Wall of Torches | 0.01 | 0.08 | +0 |

Wall of Torches+red | 0.00 | 0.99 | 1 |

Hussar Patrol+green | 0.00 | 0.99 | +1 |

Hussar Patrol-blue | 0.00 | -0.95 | -1 |

Hussar Patrol | 0.01 | -0.08 | 0 |

Fusion Elemental-green | 0.00 | -0.99 | -1 |

Fusion Elemental+white | 0.00 | 0.98 | +1 |

Fusion Elemental | 0.01 | 0.07 | 0 |

Kalonian Behemoth-green | 0.00 | -1.00 | -1 |

Kalonian Behemoth+blue | 0.00 | 0.96 | +1 |

Kalonian Behemoth | 0.13 | 0.36 | 0 |

*) I must admit this one might be a bit nonsensical. We’re encoding the split-mana cost like “green OR blue” as if it would cost both eg “green AND blue”, while the model still gets passed a CMC that counts these splits as just one. By just randomly removing the green part of a split mana cost encoded this way, we provide the model an opportunity to cheat. Overall I think this encoding is less than ideal, there are probably better ways to do it.

Now we have a model that can tell us whether a card is too expensive or too cheap, but how does that help us with the original question of determining the cost of flying? Well, in this case there is a surprisingly simple answer: Since the model is rather simple, the forward-evaluation is fast so we can just evaluate all configurations we are interested in with and without flying to see how that changes the flying cost.

That’s exactly what you see below, we pick a certain simple creature template (common colorless creature printed around April 2019). Then we first consider the non-flying case in which we vary power and toughness and for each such combination find the mana cost for which our model outputs the value closest to 0 (i.e. the approximately correct mana cost according to the model). Thats the value you see in the table cells below, followed by the model output in parenthesis. We then repeat the same for flying (unsurprisingly, the costs are somewhat higher). By subtracting the two tables from each other we can now obtain a picture of the cost of flying depending on power and toughness for our queried scenario:

**Cost without flying**

Pwr/Tough | 1 | 2 | 3 | 4 |
---|---|---|---|---|

0 | 0 (+.21) | 1 (+.06) | 2 (-.02) | 3 (-.03) |

1 | 2 (+.04) | 2 (+.05) | 3 (-.02) | 4 (-.03) |

2 | 3 (+.01) | 3 (+.04) | 4 (+.01) | 5 (-.02) |

3 | 3 (-.01) | 4 (+.07) | 5 (+.06) | 6 (+.02) |

4 | 4 (+.09) | 4 (-.01) | 5 (+.08) | 6 (+.02) |

**Cost with flying**

Pwr/Tough | 1 | 2 | 3 | 4 |
---|---|---|---|---|

0 | 1 (+.28) | 1 (+.11) | 2 (+.15) | 3 (+.16) |

1 | 3 (+.13) | 3 (+.11) | 3 (+.05) | 4 (+.10) |

2 | 5 (+.04) | 4 (-.04) | 5 (+.08) | 5 (+.03) |

3 | 5 (+.01) | 5 (-.01) | 6 (+.09) | 6 (+.06) |

4 | 6 (+.09) | 6 (+.07) | 7 (+.11) | 7 (+.09) |

**Difference**

Pwr/Tough | 1 | 2 | 3 | 4 |
---|---|---|---|---|

0 | 1 | 0 | 0 | 0 |

1 | 1 | 1 | 0 | 0 |

2 | 2 | 1 | 1 | 0 |

3 | 2 | 1 | 1 | 0 |

4 | 2 | 2 | 2 | 1 |

For interpretation of this alas, we have to be a bit careful still: According to this, e.g. a hypothetical “common” colorless 3/6 creature is worth 6 mana no matter whether it has flying or not, which does not seem right. Again, this issue is likely due to sparsity combined with card cheapness “noise”, as only few examples in that area exist [1] in our limited dataset of simple creatures. On the upside, we do get at least a vague idea of the nonlinear flying cost: It seems to be becoming more valuable with stronger creatures (which makes sense if you ever played the game).

*Wizards of the Coast, Magic: The Gathering, and their logos are trademarks of Wizards of the Coast LLC in the United States and other countries. © 2009 Wizards. All Rights Reserved. This web site is not affiliated with, endorsed, sponsored, or specifically approved by Wizards of the Coast LLC.*

At some point then I discovered TLC5940, groked the basics of KiCad, spent a bit of money and out came my first real PCB:

Actually a bit proud of this one

Oh and did I mention the empire found a spray can and now the death star is pure awesomeness?

Also a WS2812 now provides really awesome lighting to the whole thing. Best about it is that each LED’s RGB is controllable individually, all through the same 1-wire bus! An AVR Mega2560 cares for the animation, receiving commands from the Raspi via SPI like all other boards:

Also you surely noticed little darthy in the back there There is a bit more but alas I’m a bit short on photos, next time I’ll take some more, promised!

]]>There should also be a hole where the ball can come out (obviously necessary), and another hole at the top (pretty much invisible to the player) where the ball can come in without destroying it (think: “thats no moon, thats a space station!”).

This is the story (with lots of pictures) of the Pinball Death Star model we built (so far).

Original Death Star prototype built out of a plastic salad strainer and tape, featuring one light barrier. Originally had a diligently crafted paper-tape ridge structure on top, alas that turned out to be too fragile and the tape “blocks” would fall of despite strong glue and several other approaches.

By April 2018, we have put a lot of effort into this prototype, but still it turns out it will not work out. Apart from the decorative aspect, installation into the playing field is a nightmare and needs to be followed by long periods of calibrating the installation angle such that the interior floor surface is correctly in line with the playing field. This in the end was one of the major time-wasters that led to endlessly trying to recover status quo from several weeks ago.

**Needless to say the emperor was most displeased with this apparent lack of progress.**

Then miraculously, my father finds a Death Star lamp on Amazon (non-affiliated Amazon link, check out if you want to see the original lamp). Turns out this was exactly what we need to finally complete the mission! After some aligning of the plastic pieces, carefully cutting of holes and construction of the little “rain roofs” for them, I’m working on the light barriers:

Light barrier in new Death Star. Both sender and receiver built using (identical) IR LEDs I happen to have available. Turns out you can use pretty much any LED as a (low sensitivity) receiver for that exact wavelength (plenty of info about this on the net). Two transistors (“Darlington pair”) provide enough amplification to make this hack work (at least under lab conditions, yet to be tested in the wild).

You can also see one of the screws holding the two halves together (we ended up joining them the other way around than intended for the original lamp for a couple of reasons).

Six LEDs provide controllable illumination, letting the Death Star glow from inside in the dark. The green plastic piece in the background is backing of the main weapon to make it look laser-like. A single, well-angled LED illuminates that piece on demand so we can activate the weapon for dramatic effect.

Death Star with the two bottom holes (one for ramp-entry, the other for ground level exit) and (electronically trivial) switch board for connecting the death star to lamps module, switches module and power supply in a sane way. Also visible: One of the screws for mounting. In contrast to the early prototype we are not inserting it through the playing field from below anymore but actually mounting it on top so all levels/angles are fixed relative to the playing field.

New Death Star attached to the prototype playing field together with ramp. On the left side you see a bit of the side wall, Death Star was cut so we could neatly attach it.

Some work has gone into reproducing all the holes and cuts on the prototype playing field on a wood plate that we want to use as the final one. After painting everything black and letting it dry for a whole day and night, surface is still somewhat sticky, but time is almost up so we attach everything together for some final pictures. Turns out, the varnish was a bad choice (despite long discussions in the store), we’ll substitute this just-produced playing field with another wooden plate that comes with a robust black finish in our next session. Needless to say it was not the original plan to have three iterations of playing fields (not including the very early prototypes).

A lot of work here that is not visible was put into figuring out a broken coil and all kinds of software work, but thats another story and shall be told another time

]]>Somewhat unsatisfactory we ended up without getting an answer to the “flying” question, lets try to come up with a model now that can help with that.

Recently that seems to be the hammer that makes almost everything look like a nail, and I would be lying would I assure you that the recent advances in this area had not motivated this blog post. But, valued reader, keep in mind the question we are trying to answer: How much mana do abilities such as “flying” cost? How much does it cost to have a creature that has one unit more power or toughness? A (deep) neural network might be able to predict accurately the mana cost for any given card, but how would that answer our question? What we are actually after here is not approximating the mana cost of certain cards but understand how they are composed. While neural networks have recently proven to be useful function approximators for various tasks recently, they could not be farther away from something that provides a human-understandable model.

Let’s assume (for now at least), that all cards have the same base cost (bias) of some amount to which the cost for each ability of the card and costs per unit power/toughness are added. That is, we can think of our model as a linear function on our feature vectors that returns a vector of converted mana cost and devotion to each of the five colors.

Let us get straight what we can expect from such a model. There are actually creatures with nonzero strength/toughness out there that have zero mana cost, so the cost bias (that is, the cost of a hypothetical 0/0 creature card without abilities) will probably be negative. Then there are some abilities that are very color-bound which we would expect to show up in the devotion column. E.g. creatures with haste usually require red mana to play out, so we expect some component in the red devotion column for this ability. And last but not least we expect most abilities to cost some (positive) amount of mana, while very few (such as defender) are actually more of a downside and should make the creature cheaper, showing as negative values.

But now without further ado, lets run some regression on this bad boy and see what comes out of it. Scikit’s LinearRegression() finds the parameters you see in the right picture.

According to this model, a hypothetical 0/0 creature card without further abilities would cost -1 mana. There is a yearly inflation of 0.02 CMC mana points on average and power is more costly than toughness. Fear is black, haste is red, hexproof greed, flash blue and vigilance and enchantment creatures are a white concept. So far, the results seem pretty plausible. The impatient reader might also have discovered that the answer to our question on the price of “flying” is 0.79 mana points. So far, so good, but how accurate is our model, anyhow?

According to SciKit-Learn it has a score of s = 0.59… on the training (!) set. That doesn’t sound too god, but what exactly does it mean? The documentation reveals this score value is computed like this:

Name | loss | CMC | pred. |
---|---|---|---|

Lucent Liminid | 0.00 | 5 | 5.00 |

Vorapede | 0.00 | 5 | 5.00 |

Hussar Patrol | 0.00 | 4 | 4.00 |

Maritime Guard | 0.00 | 2 | 2.00 |

Bronze Sable | 0.00 | 2 | 2.00 |

… | |||

Shadow Rider | 0.38 | 4 | 3.62 |

Alpine Grizzly | 0.38 | 3 | 3.38 |

Tor Giant | 0.38 | 4 | 3.62 |

Phyrexian Hulk | 0.38 | 6 | 5.62 |

… | |||

Caravan Hurda | 1.58 | 5 | 3.42 |

Eldrazi Devastator | 1.63 | 8 | 9.63 |

Vorstclaw | 1.71 | 6 | 7.71 |

Quilled Slagwurm | 1.89 | 7 | 8.89 |

Plumeveil | 1.91 | 3 | 4.91 |

Zephid | 1.95 | 6 | 4.05 |

Merfolk of the Depths | 2.01 | 6 | 3.99 |

Phyrexian Walker | 2.02 | 0 | 2.02 |

Risen Sanctuary | 2.03 | 7 | 9.03 |

Ornithopter | 2.37 | 0 | 2.37 |

Kalonian Behemoth | 2.43 | 7 | 9.43 |

Fusion Elemental | 3.93 | 5 | 8.93 |

In the figure I’ve selected an extract that shows the few best, median and worst regression results. We see that the median error in is 0.38 CMC points which doesn’t seem all too bad, given that we are using a very straightforward model. In the worst case (Fusion Elemental) however this goes up to 3.93! Whats going on here? It turns out there are different causes for the wrong predictions, lets go through them one by one by example.

Fusion Elemental (as many other cards in the game), is quite hard to bring into play. Experienced players will immediately see why: Its not the fact that it has a CMC of 5, it is the fact that this 5 mana has to come from 5 different colors which is harder to achieve than 5 times the same color (I’ll save you and myself digging into the combinatoric possibilities of deck construction).

So why does this cause a bad prediction? Well, in the beginning we assumed that each keyword “costs” a certain (be it fractional) amount in terms of CMC, and maybe in specific colors.

What we totally neglected is that it *actually* costs “making the card somewhat harder to play”, which can manifest itself in multiple ways. Often through high CMC, but from time to time also through hard-to-play color combinations. To some degree our model seems to compensate for this effect (Hussar Patrol is estimated perfectly), but obviously not for 5-colored cards.

So in fact we need to consider a latent variable which expresses the cards “utility” (usefullness and hardness to bring into play), which can manifest itself in different cost configurations.

Our model estimated a 9/9 card with shroud to be worth about 9.43 mana, which (at least to me) doesn’t sound entirely unreasonable. Lets think a second about what “shroud” does: It forbids anyone to play spells on that creature. Usually this is a good thing (our model things its worth 0.21 increase in CMC), as it prevents your opponent from immobilizing your creature (e.g. pacifism) or destroying it (e.g. terror). But it also prevents you from equipping the creature with additional abilities. In this particular example of a creature with very high power value you might likely want to give her an ability like trample. Trample means when your creature is blocked, any “overkill” damage it deals will be received by the defending player. Without such an ability your mighty 9/9 creature can easily be blocked by sacrificing a cheap 1/1 (or similar), and will be much less useful.

What our model neglects here are shifts in usefulness of certain abilities like this, that make the value of the card depend from its features in a nonlinear way.

To be honest, I’m convinced this card is just too damn expensive for what it does. We can still learn something from this though: There is sometimes considerable “noise” in the mana costs of cards, that is cards may have cost-usefulness ratios that differ vastly for no measurable reason. Some cards are simply bad. There is no way to predict that, you can even actually find examples of two (or more) cards that do exactly the same thing, with one just being more expensive than the other. Unfortunately that means our predictions can never be perfect.

Both these cards have a CMC of 0, a power of 0 and our model is wrong by more than 2 CMC points. I found some other cases with power 0 wrongly predicted as well as some otehr cards with CMC 0 with wrong predictions (eg. Phyrexian Walker).

This can mean a number of things:

- Part of this can be attributed to noise (eg. the 0-mana-cost cards just happen to be better in terms of cost/utility ratio than average)
- A linear model is not sufficient for modelling Power/Toughness, at very least not when Power is 0. That is the “(Power, Toughness) to utility” relationship is not approximately linear.
- CMC behaves nonlinear even outside of the multicolor case. That is, the “utility to mana cost” relationship is not approximately linear.

We did get somewhat of an answer to our question (the cost of “flying” is approximately 0.79), but at the same time discovered a bunch of learning about our modelling:

- The “flying cost” question inherently assumed a linear relationship.
- However a closer look unveils that there are some nasty nonlinearities surrounding power, toughness, complex interactions of abilities.
- Moreover there is noise in the card design which makes it impossible to pin down costs for abilities exactly.
- We found that there is a latent variable which we called “utility” which can cause different mana cost configurations.

Putting all this together, our linear model is a good start. If we really want precise answers we would need to build a more complex model that allows for some non-linearities and can learn to treat different cost configurations as expressing the same utility. Or to put the other way around that can generate multiple cost configurations given a single utility value.

I think this is feasible albeit somewhat tedious, given that we can not just ramp up a network with X hidden layers to do some prediction (remember, we actually need to be able to understand the trained model in order to learn how the cost structure of MTG works!). I might go about to try this at some point in a later post, but for now I leave this as an exercise to the reader

P.S.: Random idea: Could we include community ratings from gatherer.wizards.com as a means to reduce noise?