Skip to content

Commit

Permalink
Add basic documentation for fwdpy11.conditinal_models (#878)
Browse files Browse the repository at this point in the history
* Change attribute names of ConditionalModelOutput.
  • Loading branch information
molpopgen authored Dec 27, 2021
1 parent dd050e8 commit 145a917
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 71 deletions.
2 changes: 1 addition & 1 deletion doc/_toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ parts:
- file: short_vignettes/demography_vignettes_intro
- file: short_vignettes/demes_vignette
- file: short_vignettes/demography_debugger_vignette
- caption: Short vignettes - special case models
- caption: Short vignettes - conditional models
chapters:
- file: short_vignettes/trackmutationfates
- file: short_vignettes/selective_sweep
Expand Down
13 changes: 13 additions & 0 deletions doc/misc/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ updates to latest `fwdpp` version, etc.

## 0.17.0

New release!
This release contains everything from the alpha release series plus the changes noted below.

Documentation:

* Add basic documentation for `fwdpy11.conditional_models`.
PR {pr}`878`.

Breaking changes with respect to previous alpha releases:

* Change attribute names in object returned by conditional models.
PR {pr}`878`.

Dependencies

* pin `numpy` in `setup.cfg`.
Expand Down
6 changes: 3 additions & 3 deletions doc/short_vignettes/selective_sweep.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ assert pop.generation == 0
```

```{code-cell} python
print(output.pop.mutations[output.index])
print(output.pop.mutations[output.mutation_index])
```

```{code-cell}
Expand Down Expand Up @@ -135,7 +135,7 @@ output = fwdpy11.conditional_models.selective_sweep(

```{code-cell} python
assert len(output.pop.ancient_sample_nodes) == 2 * output.pop.N
assert output.pop.fixation_times[output.index] == FIXATION_TIME
assert output.pop.fixation_times[output.mutation_index] == FIXATION_TIME
```


Expand All @@ -144,7 +144,7 @@ node_array = np.array(output.pop.tables.nodes, copy=False)
ancient_sample_node_times = \
node_array["time"][output.pop.ancient_sample_nodes]
assert np.all([ancient_sample_node_times == \
output.pop.fixation_times[output.index]])
output.pop.fixation_times[output.mutation_index]])
```

## From a standing variant
Expand Down
12 changes: 6 additions & 6 deletions doc/short_vignettes/trackmutationfates.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ kernelspec:

(tracking_mutation_fates)=

# Need title
# Tracking user-specified new mutations

```{code-cell} python
---
Expand Down Expand Up @@ -64,7 +64,7 @@ def setup(prune_selected=False):
return pop, params
```

## Need title
## Tracking a mutation for a specified number of generations

```{code-cell} python
ALPHA = -10.0
Expand Down Expand Up @@ -100,9 +100,9 @@ When tracking deleterious variants, it is unlikely that they will be around at t

```{code-cell} python
try:
print(output.pop.mutations[output.index])
print(output.pop.mutations[output.mutation_index])
except IndexError as _:
print(f"mutation {output.index} is no longer in the population!")
print(f"mutation {output.mutation_index} is no longer in the population!")
```

### Recording all generations of the mutation's sojourn
Expand Down Expand Up @@ -132,9 +132,9 @@ Let's try to print it again:

```{code-cell} python
try:
print(output.pop.mutations[output.index])
print(output.pop.mutations[output.mutation_index])
except IndexError as _:
output.index(f"mutation {output.index} is no longer in the population!")
output.mutation_index(f"mutation {output.mutation_index} is no longer in the population!")
```

Let's track this variant's frequency at each time point:
Expand Down
135 changes: 116 additions & 19 deletions fwdpy11/conditional_models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

"""
Defining models with specific outcomes.
The API of this module may be subjet to change.
Hence, the documentation is minimal.
"""

import typing
Expand All @@ -38,87 +41,156 @@ class OutOfAttempts(Exception):


class AncientSamplePolicy(Enum):
"""
When to record "preserved" or "ancient" samples:
"""

NEVER = 0
"""
Never.
"""
DURATION = 1
"""
During the entire sojourn of the mutation from when it is first
added to when the terminiation condition is first met.
"""
COMPLETION = 2
"""
Only record the generation when the terminiation condition is first met.
"""


def non_negative_value(_, attribute, value):
def _non_negative_value(_, attribute, value):
if value < 0:
raise ValueError(f"{attribute.name} must be >= 0, got {value}")


def greater_than_zero(_, attribute, value):
def _greater_than_zero(_, attribute, value):
if value < 0:
raise ValueError(f"{attribute.name} must be > 0")


def maximum_greater_minimum(instance, attribute, value):
def _maximum_greater_minimum(instance, attribute, value):
if value <= instance.minimum:
raise ValueError(f"{attribute.name} must be > minimum value")


def right_greater_left(instance, attribute, value):
def _right_greater_left(instance, attribute, value):
if value <= instance.left:
raise ValueError(f"{attribute.name} must be > left value")


def is_polymorphic_frequency(_, attribute, value):
def _is_polymorphic_frequency(_, attribute, value):
if not 0.0 < value < 1.0:
raise ValueError(f"{attribute.name} must be 0.0 < {attribute.name} < 1.0")


@attr.s(auto_attribs=True, frozen=True)
class AlleleCount:
"""
Specify a *number* of copies of a mutation.
:param count: Initial number of copies of a mutation.
This value must be `> 0`.
:type count: int
"""

count: int = attr.ib(
validator=[attr.validators.instance_of(int), greater_than_zero]
validator=[attr.validators.instance_of(int), _greater_than_zero]
)


@attr.s(auto_attribs=True, frozen=True)
class AlleleCountRange:
"""
Specify a range for a *number* of copies of a mutation.
:param minimum: Minimum number of copies of a mutation.
This value must be `> 0`.
:type minimum: int
:param maximum: Maximum number of copies of a mutation.
This value must be `> minimum`.
:type maximum: int
"""

minimum: int = attr.ib(
validator=[attr.validators.instance_of(int), greater_than_zero]
validator=[attr.validators.instance_of(int), _greater_than_zero]
)
maximum: int = attr.ib(
validator=[
attr.validators.instance_of(int),
greater_than_zero,
maximum_greater_minimum,
_greater_than_zero,
_maximum_greater_minimum,
]
)


@attr.s(auto_attribs=True, frozen=True)
class FrequencyRange:
"""
Specify a range for the initial *frequency* of a mutation
:param minimum: Minimum frequency of a mutation.
This value must be `> 0.0` and `< 1.0`.
:type minimum: float
:param maximum: Maximum frequency of a mutation.
This value must be `> minimum`.
:type maximum: float
"""

minimum: float = attr.ib(
validator=[attr.validators.instance_of(float), is_polymorphic_frequency]
validator=[attr.validators.instance_of(float), _is_polymorphic_frequency]
)
maximum: float = attr.ib(
validator=[
attr.validators.instance_of(float),
is_polymorphic_frequency,
maximum_greater_minimum,
_is_polymorphic_frequency,
_maximum_greater_minimum,
]
)


@attr.s(auto_attribs=True, kw_only=True, frozen=True)
class PositionRange:
"""
Specify a half-open interval within which to place a mutation.
:param left: The left edge, inclusive, of the interval
:type left: float
:param right: The right edge, exclusive, of the interval
:type right: float
"""

left: float = attr.ib(
validator=[attr.validators.instance_of(float), non_negative_value]
validator=[attr.validators.instance_of(float), _non_negative_value]
)
right: float = attr.ib(
validator=[
attr.validators.instance_of(float),
greater_than_zero,
right_greater_left,
_greater_than_zero,
_right_greater_left,
]
)


@attr.s(auto_attribs=True, kw_only=True, frozen=True)
class NewMutationParameters:
"""
Details of a new mutation to add to a population.
Class instances are created via keyword arguments that
become attribute names:
:param deme: The id of the deme in which to add the mutation
:type deme: Optional[int]
:param frequency: The frequency of the new mutation.
:type frequency: Union[AlleleCount, AlleleCountRange, FrequencyRange]
:param position: Where to put the new mutation
:type position: PositionRange
:param data: The specifics of the new mutation
:type data: :class:`fwdpy11.NewMutationData`
"""

deme: typing.Optional[int] = None
frequency: typing.Union[AlleleCount, AlleleCountRange, FrequencyRange] = attr.ib(
validator=attr.validators.instance_of(
Expand All @@ -135,19 +207,36 @@ class NewMutationParameters:

@attr.s(frozen=True)
class SimulationStatus:
"""
The return value of a stopping condition callable.
:param should_terminate: Set to `True` if the simulation should be terminated.
:param condition_met: Set to `True` if the stopping condition has been met.
For examples, see implementations of :class:`GlobalFixation` and :class:`FocalDemeFixation`.
"""
should_terminate: bool = attr.ib(validator=attr.validators.instance_of(bool))
condition_met: bool = attr.ib(validator=attr.validators.instance_of(bool))


@attr.s(auto_attribs=True, kw_only=True)
class ConditionalModelOutput:
pop: fwdpy11.DiploidPopulation
"""The population with the added mutation"""
params: fwdpy11.ModelParams
index: int
num_nodes: int
"""The evolved model parameters"""
mutation_index: int
"""The index of the new mutation in pop.mutations"""
num_descendant_nodes: int
"""The number of alive nodes initially containing the new mutation"""


class GlobalFixation(object):
"""
A terminiation condition monitoring for global fixation of
the mutation.
"""

def __call__(
self, pop: fwdpy11.DiploidPopulation, index: int, key: tuple
) -> SimulationStatus:
Expand All @@ -162,8 +251,12 @@ def __call__(

@attr.s(auto_attribs=True, frozen=True)
class FocalDemeFixation:
"""
A terminiation condition checking for fixation in a specific deme.
"""

deme: int = attr.ib(
validator=[attr.validators.instance_of(int), non_negative_value]
validator=[attr.validators.instance_of(int), _non_negative_value]
)

def __call__(self, pop: fwdpy11.DiploidPopulation, index, key) -> SimulationStatus:
Expand Down Expand Up @@ -195,8 +288,12 @@ def __call__(self, pop: fwdpy11.DiploidPopulation, index, key) -> SimulationStat
@attr_class_to_from_dict
@attr.s(frozen=True)
class EvolveOptions:
"""
Options to pass on too :func:`fwdpy11.evolvets`.
"""

simplification_interval: int = attr.ib(
validator=[attr.validators.instance_of(int), non_negative_value], default=100
validator=[attr.validators.instance_of(int), _non_negative_value], default=100
)
suppress_table_indexing: bool = attr.ib(
validator=attr.validators.instance_of(bool), default=True
Expand Down
15 changes: 10 additions & 5 deletions fwdpy11/conditional_models/_selective_sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@
import typing

import fwdpy11
from fwdpy11.conditional_models import (
NewMutationParameters,
SimulationStatus,
ConditionalModelOutput,
)
from fwdpy11.conditional_models import (ConditionalModelOutput,
NewMutationParameters,
SimulationStatus)

from ._track_added_mutation import _track_added_mutation

Expand All @@ -46,6 +44,13 @@ def _selective_sweep(
],
**kwargs,
) -> ConditionalModelOutput:
"""
This function is a wrapper around :func:`fwdpy11.conditional_models.track_added_mutation`.
This function requires a `stopping_condition`.
If `when` is not given as a keyword argument, it is assumed to be 0.
The keyword argument `until` is not allowed.
"""
if "when" not in kwargs or kwargs["when"] is None:
kwargs["when"] = 0

Expand Down
Loading

0 comments on commit 145a917

Please sign in to comment.