-
Notifications
You must be signed in to change notification settings - Fork 1
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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), | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? Something like "The main changes from the |
||||||
|
||||||
Both methods are linear because they rely on a pre-computed map of local density. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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` | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). | ||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)`. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This last sentence, "Since...", is a sentence fragment. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.) | ||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
initialize() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. :-> There was a problem hiding this comment. Choose a reason for hiding this commentThe 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("==========================="); | ||
} |
There was a problem hiding this comment.
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...