MesserLab / SLiM

SLiM is a genetically explicit forward simulation software package for population genetics and evolutionary biology. It is highly flexible, with a built-in scripting language, and has a cross-platform graphical modeling environment called SLiMgui.
https://messerlab.org/slim/
GNU General Public License v3.0
160 stars 33 forks source link

method to choose a location nearby proportional to a map value #384

Closed petrelharp closed 1 year ago

petrelharp commented 1 year ago

It'd be very useful for a number of things to have a method that allows an individual to choose a location nearby but proportional to map value. For instance: mosquitos lay eggs in water, so they could choose a location from a map of "water". Or, individuals could disperse only through grassland, so we could have a map of "grassland".

Currently I do this kind of thing by choosing a bunch of points, evaluating the map at these locations, and picking one of those points proportional to the map value at those points.

So, this is probably a method of an Interaction, maybe

i1.sampleFromHabitat(point)

Note that the argument could be individual, but there's no reason for it to be, I think? And, note that conceptually this isn't very consistent if we think about 'interaction' as being between pairs of individuals, but really we're using "interaction" in the more general sense of "interacting with other stuff around".

As for implementation: it's easy to do by rejection sampling if you have the denominator, which is exactly what we are proposing computing and having reference to in https://github.com/MesserLab/SLiM/issues/380#issuecomment-1666297272 .

petrelharp commented 1 year ago

Note that over in https://github.com/MesserLab/SLiM/issues/362 you (@bhaller) are making the argument that interactions are fundamentally between pairs of individuals. As I mentioned above I don't think that's the only way to think about interactions; in particular, maybe we need to disambiguate "between pairs of individuals" interactions and the concept of a interaction kernel (which could be thought of as how much time do you spend nearby and thus possibly interacting)? I vote to just not be so strict about what an interaction means (ie not just pairwise), myself.

bhaller commented 1 year ago

@petrelharp Yes, that's an unresolved debate. To me the language of "receiver" and "exerter" is very useful, and an "interaction" is, after all, about things that interact – i.e., individuals interacting with each other. As I've pointed out before, it would also break the design of interaction() callbacks, since they are, at the moment, fundamentally tied to an exerter and a receiver; I'm not sure how much I care about that (I'm not sure this callback type has been very widely used). Overall, moving away from "an interaction is between a receiver and an exerter" would be a huge paradigm shift, and would require rewriting an enormous amount of documentation, changing most of the spatial APIs (since most of them refer to exerters and receivers), and might end up being a much less clear conceptual model in the end, in my opinion. I'm really not a fan of it, at first blush. We need to think beyond the immediate demands of "we need functionality X" to make sure the broader design is harmonious and consistent and clear (while also, of course, ultimately satisfying the need for functionality X).

As far as this issue goes, what you're talking about here doesn't really have anything to do with "interactions", as far as I can tell; it's really a kernel-based operation on a spatial map, having nothing to do with even one individual, as you note, much less two. The only reason to make it a method of InteractionType, it seems to me, is that InteractionType happens to define a spatial kernel. But it wouldn't use any of the internal facilities that InteractionType provides – the k-d tree, the SparseVector machinery – at all, and it would use a concept that presently doesn't appear in InteractionType's code at all, that of a spatial map. So I don't think InteractionType is where this method belongs. I'd put it on SpatialMap, if SpatialMap were a class; and maybe SpatialMap does need to become a class, since we're talking about adding more and more functionality related to it. As things are designed right now, since there is no SpatialMap class, it would be a new method on Subpopulation, taking the name of the spatial map, like spatialMapValue() and such, but adding more and more methods like that gets ugly and suggests that it really needs to be its own class. Either way, the parameters that define the kernel could simply be passed to this method, and that provides more flexibility than using InteractionType. For example, you might want an individual's spatial search for good habitat to have a radius that varies depending upon the age or condition of the individual, so you might want the kernel used to vary. That's easy if the kernel parameters are passed in; it's extremely inconvenient if the kernel is defined by an existing InteractionType. It's also possible that it makes sense to have a new SpatialKernel class, since we seem to be talking about doing more and more things that depend upon a kernel in some way, but are not related to InteractionType.

Anyhow, don't worry, I'm going to think hard about all of these various spatial feature requests and whether/how SLiM's design ought to be modified to accommodate them. :-> I'll come back with a proposal once I've had time to do that. At the moment I'm really a bit overwhelmed by stuff.

bhaller commented 1 year ago

@petrelharp This sort of discussion is probably another agenda item for your visit. :->

petrelharp commented 1 year ago

probably another agenda item for your visit

Sounds good; my first question for you will be "if not through Interactions, then how?"

bhaller commented 1 year ago

@petrelharp Perhaps I will have a good answer by then. :->

jiseonmin commented 1 year ago

I agree with Peter that this could be done by 'interactions'. For instance, in this scenario of choosing a dispersal location based on a map value, could "setInteractionFunction" read in neighboring map value and weight whatever map-agnostic kernel by the map value? As you pointed out before, this isn't exactly interaction between two individuals, but just because dispersion is controlled by interaction function, I am guessing adding map value to the interaction function as a good way to implement this. (I don't know the details of how the function actually works, so I might be completely wrong)

bhaller commented 1 year ago

Note that over in #380 I have now made SpatialMap into its own user-visible class, which supports things like smoothing out a spatial map with a kernel, etc. I think it would be straightforward to now add a drawPointWithKernel() type of method to SpatialMap that would do what I think is being proposed here. The work for #380 has also led to the creation of a SpatialKernel class in C++ that is now used internally by both InteractionType and SpatialMap; it is not yet user-visible in Eidos, since there has not yet been a real need for it to be, but it could be, if it would make this issue easier to resolve. Personally, I'd prefer not to add another user-visible class unless really necessary, though; it's starting to feel a bit complex, how all these classes fit together and work together. I think probably drawPointWithKernel() could just take the usual kernel-defining parameters, as setInteractionFunction() does. @petrelharp this stuff is all ready to work though once you're here. :->

bhaller commented 1 year ago

This work is now done, in the smooth branch, apart from parallelization (which will be dealt with later). The methods added are:

– (float)sampleImprovedNearbyPoint(float point, float$ maxDistance, string$ functionType, ...) This variant of sampleNearbyPoint() samples a Metropolis–Hastings move on the spatial map. See sampleNearbyPoint() for discussion of the basic idea. This method proposes a nearby point drawn from the given kernel. If the drawn point has a larger map value than the original point, the new point is returned. If the drawn point has a smaller map value than the original point, it is returned with a probability equal to the ratio between its map value and the original map value, otherwise the original point is returned. The distribution of individuals that move (or not) to new locations governed by this method will converge upon the map itself, in a similar manner to how MCMC converges upon the posterior distribution (assuming no other forces, such as birth or death, influence the distribution of individuals). Movement governed by this method is “improved” in the sense that individuals will tend to remain where they are unless the new sampled point is an improvement for them – a higher map value. Note that unlike sampleNearbyPoint(), this method requires that all map values are non-negative.

– (float)sampleNearbyPoint(float point, float$ maxDistance, string$ functionType, ...) For a spatial point supplied in point, returns a nearby point sampled from a kernel weighted by the spatial map’s values. Only points within the maximum distance of the kernel, maxDistance, will be chosen, and the probability that a given point is chosen will be proportional to the density of the kernel at that point multiplied by the value of the map at that point (interpolated, if interpolation is enabled for the map). Negative values of the map will be treated as zero. The kernel is specified with a kernel shape, functionType, followed by zero or more ellipsis arguments; see smooth() for further information. For this method, at present only kernel types "f", "l", "e", "n", and "t" are supported, and type "t" is not presently supported for 3D kernels. This method can be used to find points in the vicinity of individuals that are favorable – possessing more resources, or better environmental conditions, etc. It can also be used to guide the dispersal or foraging behavior of individuals. See sampleImprovedNearbyPoint() for a variant that may be useful for directed movement across a landscape.

Closing.