Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fast map-based example #35

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ The scripts in each folder follow the same format but have some elements that a
## The examples:

- [`minimal.slim`](minimal.html): a minimal example, that explains the general structure of the scripts.
- [`minimal_high_density.slim`](minimal_high_density.html): essentially the same as `minimal.slim`, but with some computational tricks that let it scale to much higher density
- [`maps/`](maps/README.html): use a simple map of a mountain to model heterogenous carrying capacity distribution in space
- [`adult_movement/`](adult_movement/README.html) (Individuals continue to move around throughout its lifetime. Appropriate for animals, rather than plants)
- [`mate_choice/`](mate_choice/README.html) (Dioecious population, offspring dispersed from female parents)
Expand Down
103 changes: 103 additions & 0 deletions minimal_high_density.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# `minimal_high_density.slim`: a minimal model suitable for high-density simulations

**code:** [minimal_high_density.slim](minimal_high_density.slim); on [github](https://github.com/kr-colab/spatial_sims_standard/blob/main/minimal_high_density.slim)

This code implements the same model as [`minimal.slim`](minimal.html),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is "minimal.html" here? I don't see that file in the repo...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This code implements the same model as [`minimal.slim`](minimal.html),
This code implements the same model as [`minimal.slim`](https://github.com/kr-colab/spatial_sims_standard/blob/main/minimal.slim),

but implements some "map-based" alternative computational methods so that runtime is linear in density
instead of quadratic.
However, these methods are somewhat approximate and may lead to discretization artifacts if density is low,
so if neighborhood sizes are small (below, say 5 or 10), then the methods should probably not be used.

There are two aspects of the "standard" methods demonstrated in `minimal.slim` that are quadratic in population density
(i.e., quadratic in the parameter `K`): local density computation, and mate choice.
The two methods can be used independently - for instance, maybe your simulation
has small interaction neighborhood size but large mating neighborhood size,
so you'd use these methods for mating but not density computations.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little vague what "these methods" refers to. Maybe it would be helpful to actually give the method shown in this example a name, so you can refer to it? Otherwise, "the alternative method shown here" instead of "these methods", I guess.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or just plop down the method/function names here to clarify the important differences? smooth and sampleNearbyPoint, maybe others?

Something like "The main changes from the minimal.slim example are use of the SpatialMap methods smooth and sampleNearbyPoint, which are described in more detail below."


Both methods are linear because they rely on a pre-computed map of local density.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And now "both methods" is also vague/confusing here. I thought at first that it referred to both the "standard" method and the "alternative" method, but that didn't make sense, so I guess the "alternative" method is actually two methods? Anyhow, the usage of this vague term "method" needs to be cleaned up.

The basic computation to do this is
```
raw = summarizeIndividuals(p1.individuals, GRID_DIMS, p1.spatialBounds, operation="individuals.size();", perUnitArea=T);
```
This makes a grid of dimensions `GRID_DIMS`, counts up the number of individuals in each grid square,
and divides by the area of the square to get a density per unit area.
What is an appropriate size for these grid squares?
Well, we'd like these to be as big as is reasonable,
because computation will scale with the number of these squares.
But, this grid is the source of any discretization artifacts:
the smaller the grid squares are, the closer this model is to `minimal.slim`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or even worse, potentially! could be much worse if you make them really small!

In both measuring local density and choosing mates we average over a kernel
with a certain characteristic scale: `SX` for local density (for "interactions") and `SM` for mating.
So, as long as our grid squares are sufficiently smaller than these,
any discretization will be smoothed out by those operations.
So, we set the size of the grid cells to be approximately `min(SX,SM)/2`:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps i will be obvious to most readers but of course if you're only using the method for one or the other purpose, you could customize the grid to either SX or SM; and you could conceivably make two different grids, too, if SX and SM are very different, right?

```
grid_dims = asInteger(2 * (p1.spatialBounds[c(2,3)] - p1.spatialBounds[c(0,1)]) / min(SX, SM));
```

The `raw` values are put in two maps: `RAW_DENSITY` and `DENSITY`;
the first is used for mate choice and the second for interactions.

## Local smoothed density

Local density used as an input for mortality is computed in `minimal.slim`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

due to interactions

by averaging over a Gaussian kernel with standard deviation `SX`.
To approximate this, we simply smooth the "raw" gridded density by that same kernel:
```
DENSITY.smooth(SX * 3, "n", SX);
```
and then look up the local, smoothed density at the location of every individual:
```
competition = p1.spatialMapValue(DENSITY, inds.spatialPosition);
```
This could be made more precise by taking into account that in computing the "raw" density
also involves some smoothing (the size of the grid squares).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The word "in" seems stray here. More importantly, I'm not sure what you're suggesting here. How would you take that fact into account? What would that look like in script?

## Mate choice

Mate choice in `minimal.slim` picks a nearby individual proportional to a weight
assigned from a Gaussian kernel with standard deviation `SM`.
To approximate this procedure, we choose a nearby location proportional to raw density
multiplied by the same Gaussian kernel,
and then return the nearest individual to that location:
```
mate_location = RAW_DENSITY.sampleNearbyPoint(individual.spatialPosition, 3 * SM, "n", SM);
mate = i2.nearestNeighborsOfPoint(mate_location, p1, 1);
```

## Possible pitfalls

The two lines in the "mate choice" snippet can each cause their own problems if care is not taken.

First, the call to `sampleNearbyPoint` works by rejection sampling:
it picks a location nearby from the provided kernel,
and then retains the point with probability proportional to the value of the `RAW_DENSITY` map at that point.
The way this is implemented means that if values of density in some parts of the map are much higher than in other parts of the map,
this may do a large number of rejections.
Concretely, if the density within a circle of radius `3 * SM` around a given individual
is at most, say $10^{-6}$ times the maximum density over the entire map,
then choosing a mate for that individual will require sampling millions of locations.
The solution to this might be biological: first, identify individuals with sufficent possible mates
to be able to mate successfully; and then only choose mates for those.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, OK, but how would you "identify individuals with sufficient possible mates to be able to mate successfully" if not doing a spatial search of the type we're trying to avoid? Also, note that "sufficient" is misspelled.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like really the guidance ought to be: if density in your simulation varies that dramatically, maybe this approach is not for you. :-> Also: we could fix this problem, couldn't we? Assess the max value within the specific grid squares being sampled from and use that max for the rejection sampling? If that makes sense, maybe you can open an issue on it with whatever discussion you see as needed for me to implement it?


Second, the call to `i2.nearestNeighborsOfPoint` will fail, clearly, if there *are* no neighbors.
Since this call uses the interaction `i2`,
which was set up at the start of the simulation
```
initializeInteractionType(2, "xy", reciprocal=T, maxDistance=5/sqrt(K));
```
to have maximum interaction distance `5/sqrt(K)`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This last sentence, "Since...", is a sentence fragment.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it needs explanation why the max distance is "5/sqrt(K)" – where does that come from?

If the density is roughly $K$, this means there should be around 25 individuals in each circle of that radius;
however, if density is sufficiently nonuniform, or if you are simulating some nonequilibrium situation,
then this may fail.
The code is robust to this:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about "The code in this example handles this issue, by doing:". I misread "The code is robust to this:" as saying "The code in the model is robust to the following two lines of code:" which had me scratching my head for a good thirty seconds! As usual, "this" creates ambiguity and confusion. :->

```
if (mate.size())
subpop.addCrossed(individual, mate, count=rpois(1, FECUN));
```
means that if an individual has no neighbors, they will not reproduce,
but this is over a much smaller distance than our nominal mating distance, `SM`,
so we don't want this to happen very much.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a bit of rewriting. "means that..." seems like it starts a sentence in the middle, and "this is over" and "we don't want this" both use "this" in an ambiguous way that I am honestly unable to puzzle out.

(Indeed, if `SM` was not much larger than `5/sqrt(K)` then we wouldn't be saving any computation at all.)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ends rather abruptly; some kind of closing sentence, a conclusion, a take-home point?


130 changes: 130 additions & 0 deletions minimal_high_density.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
initialize() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't try actually running the model, but I don't see any problems. :->

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Runs fine on my end!

initializeSLiMModelType("nonWF");
initializeSLiMOptions(dimensionality="xy");

defaults = Dictionary(
"SEED", getSeed(),
"SD", 0.3, // sigma_D, dispersal distance
"SX", 0.3, // sigma_X, interaction distance for measuring local density
"SM", 0.3, // sigma_M, mate choice distance
"K", 50, // carrying capacity per unit area
"LIFETIME", 4, // average life span
"WIDTH", 25.0, // width of the simulated area
"HEIGHT", 25.0, // height of the simulated area
"RUNTIME", 200, // total number of ticks to run the simulation for
"L", 1e8, // genome length
"R", 1e-8, // recombination rate
"MU", 0 // mutation rate
);

// Set up parameters with a user-defined function
setupParams(defaults);

// Set up constants that depend on externally defined parameters
defineConstant("FECUN", 1 / LIFETIME);
defineConstant("RHO", FECUN / ((1 + FECUN) * K));
defineConstant("PARAMS", defaults);

setSeed(SEED);

// basic neutral genetics
initializeMutationRate(MU);
initializeMutationType("m1", 0.5, "f", 0.0);
initializeGenomicElementType("g1", m1, 1.0);
initializeGenomicElement(g1, 0, L-1);
initializeRecombinationRate(R);

// spatial interaction used to pick mates
initializeInteractionType(2, "xy", reciprocal=T, maxDistance=5/sqrt(K));
i2.setInteractionFunction("f", 1.0);
}

1 first() {
sim.addSubpop("p1", asInteger(K * WIDTH * HEIGHT));
p1.setSpatialBounds(c(0, 0, WIDTH, HEIGHT));
p1.individuals.setSpatialPosition(p1.pointUniform(p1.individualCount));

// set up a map of density
grid_dims = asInteger(2 * (p1.spatialBounds[c(2,3)] - p1.spatialBounds[c(0,1)]) / min(SX, SM));
raw = summarizeIndividuals(p1.individuals, grid_dims, p1.spatialBounds,
operation="individuals.size();", perUnitArea=T);
raw_density_map = p1.defineSpatialMap("raw_density", "xy", raw);
density_map = p1.defineSpatialMap("density", "xy", raw);
density_map.smooth(SX * 3, "n", SX);
defineGlobal("GRID_DIMS", grid_dims);
defineGlobal("RAW_DENSITY", raw_density_map);
defineGlobal("DENSITY", density_map);
}

first() {
// preparation for the reproduction() callback
i2.evaluate(p1);

// update map of density
raw = summarizeIndividuals(p1.individuals, GRID_DIMS, p1.spatialBounds, operation="individuals.size();", perUnitArea=T);
RAW_DENSITY.changeValues(raw);
}

reproduction() {
mate_location = RAW_DENSITY.sampleNearbyPoint(individual.spatialPosition, 3 * SM, "n", SM);
mate = i2.nearestNeighborsOfPoint(mate_location, p1, 1);
if (mate.size())
subpop.addCrossed(individual, mate, count=rpois(1, FECUN));
}

early() {
// update map of density
raw = summarizeIndividuals(p1.individuals, GRID_DIMS, p1.spatialBounds, operation="individuals.size();", perUnitArea=T);
DENSITY.changeValues(raw);
DENSITY.smooth(SX * 3, "n", SX);
}

early() {
// Disperse offspring
offspring = p1.subsetIndividuals(maxAge=0);
p1.deviatePositions(offspring, "reprising", INF, "n", SD);

// Measure local density and use it for density regulation
inds = p1.individuals;
competition = p1.spatialMapValue(DENSITY, inds.spatialPosition);
inds.fitnessScaling = 1 / (1 + RHO * competition);
}

late() {
if (p1.individualCount == 0) {
catn("Population went extinct! Ending the simulation.");
sim.simulationFinished();
}
}

RUNTIME late() {
catn("End of simulation (run time reached)");
sim.simulationFinished();
}

function (void)setupParams(object<Dictionary>$ defaults)
{
if (!exists("PARAMFILE")) defineConstant("PARAMFILE", "./params.json");
if (!exists("OUTDIR")) defineConstant("OUTDIR", ".");
defaults.addKeysAndValuesFrom(Dictionary("PARAMFILE", PARAMFILE, "OUTDIR", OUTDIR));

if (fileExists(PARAMFILE)) {
defaults.addKeysAndValuesFrom(Dictionary(readFile(PARAMFILE)));
defaults.setValue("READ_FROM_PARAMFILE", PARAMFILE);
}

defaults.setValue("OUTBASE", OUTDIR + "/out_" + defaults.getValue("SEED"));
defaults.setValue("OUTPATH", defaults.getValue("OUTBASE") + ".trees");

for (k in defaults.allKeys) {
if (!exists(k))
defineConstant(k, defaults.getValue(k));
else
defaults.setValue(k, executeLambda(k + ";"));
}

// print out default values
catn("===========================");
catn("Model constants: " + defaults.serialize("pretty"));
catn("===========================");
}