Skip to content

Commit c354d45

Browse files
authored
Improve early detection of invalid optimum values. (#1299)
See #1217 for context. * Add new custom exception class * Detect setup errors when constructing ModelParams * Update tests and manual pages. This PR breaks Python API for "gentic value" objects. Such object must now provide a "validation" function that is run by ModelParams Note: deprecated classes are not updated to be compatible with the new validation API.
1 parent e90ec09 commit c354d45

15 files changed

+201
-34
lines changed

doc/pages/mvdes.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ migrations:
6060
"""
6161
6262
graph = demes.loads(yaml)
63-
demography = fwdpy11.discrete_demography.from_demes(graph, 1)
63+
demography = fwdpy11.ForwardDemesGraph.from_demes(graph, 1)
6464
65+
optima = [fwdpy11.Optimum(when=0, optimum=0.0, VS=10.0)]
6566
pdict = {
6667
"nregions": [],
6768
"recregions": [],
@@ -74,7 +75,8 @@ pdict = {
7475
"demography": demography,
7576
"simlen": 100,
7677
"gvalue": fwdpy11.Additive(
77-
ndemes=2, scaling=2, gvalue_to_fitness=fwdpy11.GSS(optimum=0.0, VS=10.0)
78+
ndemes=2, scaling=2,
79+
gvalue_to_fitness=fwdpy11.GaussianStabilizingSelection.single_trait(optima)
7880
),
7981
"prune_selected": False,
8082
}

doc/short_vignettes/multivariate_gaussian_effect_sizes_across_demes.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ migrations:
5959
rate: 0.10
6060
"""
6161
g = demes.loads(yaml)
62-
model = fwdpy11.discrete_demography.from_demes(g, burnin=1)
62+
model = fwdpy11.ForwardDemesGraph.from_demes(g, burnin=1)
6363
demesdraw.tubes(g);
6464
```
6565

@@ -76,7 +76,7 @@ pdict = {
7676
],
7777
"rates": (0, 2.5e-3, None),
7878
"demography": model,
79-
"simlen": model.metadata["total_simulation_length"],
79+
"simlen": model.final_generation,
8080
"gvalue": fwdpy11.Additive(
8181
ndemes=3, scaling=2,
8282
gvalue_to_fitness=fwdpy11.GaussianStabilizingSelection.single_trait([fwdpy11.Optimum(optimum=0.0, VS=10.0, when=0)])
@@ -105,7 +105,7 @@ Let's evolve the model now:
105105
params = fwdpy11.ModelParams(**pdict)
106106
# TODO: update this once we have a function to pull the sizes
107107
# automatically from demes-derived models:
108-
initial_sizes = [v for v in model.metadata["initial_sizes"].values()]
108+
initial_sizes = model.initial_sizes
109109
pop = fwdpy11.DiploidPopulation(initial_sizes, 1.0)
110110
rng = fwdpy11.GSLrng(1010)
111111
fwdpy11.evolvets(rng, pop, params, 10)
@@ -138,7 +138,7 @@ pdict["sregions"] = [
138138
params = fwdpy11.ModelParams(**pdict)
139139
# TODO: update this once we have a function to pull the sizes
140140
# automatically from demes-derived models:
141-
initial_sizes = [v for v in model.metadata["initial_sizes"].values()]
141+
initial_sizes = model.initial_sizes
142142
pop = fwdpy11.DiploidPopulation(initial_sizes, 1.0)
143143
fwdpy11.evolvets(rng, pop, params, 10)
144144
for i in pop.tables.mutations:

doc/short_vignettes/mutationdominance_vignette.md

+7-7
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,18 @@ pop = fwdpy11.DiploidPopulation(500, 1.0)
4848
4949
rng = fwdpy11.GSLrng(54321)
5050
51-
GSSmo = fwdpy11.GSSmo(
52-
[
53-
fwdpy11.Optimum(when=0, optimum=0.0, VS=1.0),
54-
fwdpy11.Optimum(when=10 * pop.N - 200, optimum=1.0, VS=1.0),
55-
]
56-
)
51+
optima = [
52+
fwdpy11.Optimum(when=0, optimum=0.0, VS=1.0),
53+
fwdpy11.Optimum(when=10 * pop.N - 200, optimum=1.0, VS=1.0),
54+
]
55+
56+
GSS = fwdpy11.GaussianStabilizingSelection.single_trait(optima)
5757
5858
rho = 1000.
5959
6060
p = {
6161
"nregions": [],
62-
"gvalue": fwdpy11.Additive(2.0, GSSmo),
62+
"gvalue": fwdpy11.Additive(2.0, GSS),
6363
"sregions": [des],
6464
"recregions": [fwdpy11.PoissonInterval(0, 1., rho / float(4 * pop.N))],
6565
"rates": (0.0, 1e-3, None),

doc/short_vignettes/poptour_vignette.md

+7-7
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,18 @@ pop = fwdpy11.DiploidPopulation(500, 1.0)
3030
3131
rng = fwdpy11.GSLrng(54321)
3232
33-
GSSmo = fwdpy11.GSSmo(
34-
[
35-
fwdpy11.Optimum(when=0, optimum=0.0, VS=1.0),
36-
fwdpy11.Optimum(when=10 * pop.N - 200, optimum=1.0, VS=1.0),
37-
]
38-
)
33+
optima = [
34+
fwdpy11.Optimum(when=0, optimum=0.0, VS=1.0),
35+
fwdpy11.Optimum(when=10 * pop.N - 200, optimum=1.0, VS=1.0),
36+
]
37+
38+
GSS = fwdpy11.GaussianStabilizingSelection.single_trait(optima)
3939
4040
rho = 1000.
4141
4242
p = {
4343
"nregions": [],
44-
"gvalue": fwdpy11.Additive(2.0, GSSmo),
44+
"gvalue": fwdpy11.Additive(2.0, GSS),
4545
"sregions": [fwdpy11.GaussianS(0, 1., 1, 0.1)],
4646
"recregions": [fwdpy11.PoissonInterval(0, 1., rho / float(4 * pop.N))],
4747
"rates": (0.0, 1e-3, None),

doc/short_vignettes/recorders_vignette.md

+8-8
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,12 @@ pop = fwdpy11.DiploidPopulation(500, 1.0)
9191
9292
rng = fwdpy11.GSLrng(54321)
9393
94-
GSSmo = fwdpy11.GSSmo(
95-
[
96-
fwdpy11.Optimum(when=0, optimum=0.0, VS=1.0),
97-
fwdpy11.Optimum(when=pop.N - 200, optimum=1.0, VS=1.0),
98-
]
99-
)
94+
optima= [
95+
fwdpy11.Optimum(when=0, optimum=0.0, VS=1.0),
96+
fwdpy11.Optimum(when=pop.N - 200, optimum=1.0, VS=1.0),
97+
]
98+
99+
GSS = fwdpy11.GaussianStabilizingSelection.single_trait(optima)
100100
101101
rho = 1000.
102102
@@ -105,7 +105,7 @@ des = fwdpy11.GaussianS(beg=0, end=1, weight=1, sd=0.1,
105105
106106
p = {
107107
"nregions": [],
108-
"gvalue": fwdpy11.Additive(2.0, GSSmo),
108+
"gvalue": fwdpy11.Additive(2.0, GSS),
109109
"sregions": [des],
110110
"recregions": [fwdpy11.PoissonInterval(0, 1., rho / float(4 * pop.N))],
111111
"rates": (0.0, 1e-3, None),
@@ -169,7 +169,7 @@ Thus, we need to rebuild the `gvalue` field.
169169
import pickle
170170
pop = fwdpy11.DiploidPopulation(500, 1.0)
171171
rng = fwdpy11.GSLrng(54321)
172-
p['gvalue'] = fwdpy11.Additive(2.0, GSSmo)
172+
p['gvalue'] = fwdpy11.Additive(2.0, GSS)
173173
params = fwdpy11.ModelParams(**p)
174174
recorder = RandomSamples(5)
175175
fwdpy11.evolvets(rng, pop, params, recorder=recorder, simplification_interval=100)

doc/short_vignettes/workingexample_trait.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ p = {
3737
"rates": (mu_neutral, mu_selected, None),
3838
# Keep mutations at frequency 1 in the pop if they affect fitness.
3939
"prune_selected": False,
40-
"demography": None,
40+
"demography": fwdpy11.ForwardDemesGraph.tubes([N], burnin=1),
4141
"simlen": 10 * N,
4242
}
4343
params = fwdpy11.ModelParams(**p)

examples/plugin/test_plugin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"""
1414

1515
graph = demes.loads(yaml)
16-
demography = fwdpy11.discrete_demography.from_demes(graph, 1)
16+
demography = fwdpy11.ForwardDemesGraph.from_demes(graph, 1)
1717

1818
pdict = {
1919
"demography": demography,

fwdpy11/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
StrictAdditiveMultivariateEffects,
6262
AdditivePleiotropy,
6363
PyDiploidGeneticValue,
64+
TimingError,
6465
)
6566

6667
from ._types import ( # NOQA

fwdpy11/_types/model_params.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
# along with fwdpy11. If not, see <http://www.gnu.org/licenses/>.
1818
#
1919

20+
import abc
2021
import typing
2122
import warnings
2223

2324
import attr
25+
import demes
2426
import fwdpy11
2527
import numpy as np
2628
from fwdpy11.class_decorators import (
@@ -96,6 +98,20 @@ def _convert_rates(value):
9698
return MutationAndRecombinationRates(**value)
9799

98100

101+
@typing.runtime_checkable
102+
class ValidateEventTimings(typing.Protocol):
103+
@abc.abstractmethod
104+
def validate_timings(self, deme: int, demography: ForwardDemesGraph) -> None:
105+
raise NotImplementedError
106+
107+
108+
def validate_timings(
109+
gvalue: ValidateEventTimings, deme: int, demography: ForwardDemesGraph
110+
) -> None:
111+
assert isinstance(gvalue, ValidateEventTimings)
112+
return gvalue.validate_timings(deme, demography)
113+
114+
99115
@attr_add_asblack
100116
@attr_class_to_from_dict_no_recurse
101117
@attr.s(kw_only=True, frozen=True, slots=True, repr_ns="fwdpy11")
@@ -299,15 +315,17 @@ def rates_validator(self, attribute, value):
299315
@gvalue.validator
300316
def validate_gvalue(self, attribute, value):
301317
try:
302-
for i in value:
318+
for deme, i in enumerate(value):
303319
attr.validators.instance_of(fwdpy11.DiploidGeneticValue)(
304320
self, attribute, i
305321
)
322+
validate_timings(i, deme, self.demography)
306323
except TypeError:
307324
try:
308325
attr.validators.instance_of(fwdpy11.DiploidGeneticValue)(
309326
self, attribute, value
310327
)
328+
validate_timings(value, 0, self.demography)
311329
except TypeError:
312330
raise
313331

fwdpy11/genetic_values.py

+47
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
import warnings
2323

2424
import attr
25+
import demes
2526
import numpy as np
2627

2728
from deprecated import deprecated
2829

30+
2931
from ._fwdpy11 import (
3032
GeneticValueIsTrait,
3133
GeneticValueNoise,
@@ -49,6 +51,12 @@
4951
attr_class_to_from_dict_no_recurse,
5052
)
5153

54+
from ._types.forward_demes_graph import ForwardDemesGraph
55+
56+
57+
class TimingError(Exception):
58+
pass
59+
5260

5361
@attr_add_asblack
5462
@attr_class_pickle_with_super
@@ -204,6 +212,20 @@ def asdict(self):
204212
def fromdict(cls, data):
205213
return cls(**data)
206214

215+
def validate_timings(self, deme: int, demography: ForwardDemesGraph) -> None:
216+
graph = demography.demes_graph
217+
deme_obj = graph.demes[deme]
218+
if deme_obj.start_time == float("inf"):
219+
start_time = demography.to_backwards_time(0)
220+
else:
221+
start_time = deme_obj.start_time
222+
for i in self.optima[:1]:
223+
btime = demography.to_backwards_time(i.when)
224+
if btime < start_time:
225+
msg = f"deme {deme_obj.name}, event {i} is at time {btime} in the past "
226+
msg += f", but the deme starts at {int(start_time)}"
227+
raise TimingError(msg)
228+
207229

208230
@attr_add_asblack
209231
@attr_class_to_from_dict_no_recurse
@@ -508,6 +530,11 @@ def __attrs_post_init__(self):
508530
self.scaling, self.gvalue_to_fitness, self.noise, self.ndemes
509531
)
510532

533+
def validate_timings(self, deme: int, demography: ForwardDemesGraph) -> None:
534+
if self.gvalue_to_fitness is None:
535+
return
536+
self.gvalue_to_fitness.validate_timings(deme, demography)
537+
511538

512539
@attr_add_asblack
513540
@attr_class_pickle_with_super
@@ -555,6 +582,11 @@ def __attrs_post_init__(self):
555582
self.scaling, self.gvalue_to_fitness, self.noise, self.ndemes
556583
)
557584

585+
def validate_timings(self, deme: int, demography: ForwardDemesGraph) -> None:
586+
if self.gvalue_to_fitness is None:
587+
return
588+
self.gvalue_to_fitness.validate_timings(deme, demography)
589+
558590

559591
@attr_add_asblack
560592
@attr_class_pickle_with_super
@@ -591,6 +623,11 @@ class GBR(_ll_GBR):
591623
def __attrs_post_init__(self):
592624
super(GBR, self).__init__(self.gvalue_to_fitness, self.noise)
593625

626+
def validate_timings(self, deme: int, demography: ForwardDemesGraph) -> None:
627+
if self.gvalue_to_fitness is None:
628+
return
629+
self.gvalue_to_fitness.validate_timings(deme, demography)
630+
594631

595632
@attr_add_asblack
596633
@attr_class_pickle_with_super
@@ -637,6 +674,11 @@ def __attrs_post_init__(self):
637674
self.ndimensions, self.focal_trait, self.gvalue_to_fitness, self.noise
638675
)
639676

677+
def validate_timings(self, deme: int, demography: ForwardDemesGraph) -> None:
678+
if self.gvalue_to_fitness is None:
679+
return
680+
self.gvalue_to_fitness.validate_timings(deme, demography)
681+
640682

641683
@deprecated(reason="Use AdditivePleiotropy instead.")
642684
class StrictAdditiveMultivariateEffects(AdditivePleiotropy):
@@ -652,3 +694,8 @@ def __init__(self, ndim: int, gvalue_to_fitness=None, noise=None):
652694
gvalue_to_fitness,
653695
noise,
654696
)
697+
698+
def validate_timings(self, deme: int, demography: ForwardDemesGraph) -> None:
699+
if self.gvalue_to_fitness is None:
700+
return
701+
self.gvalue_to_fitness.validate_timings(deme, demography)

tests/pygss.py

+8
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import fwdpy11
2525
import fwdpy11.custom_genetic_value_decorators
2626

27+
from fwdpy11._types import ForwardDemesGraph
28+
2729

2830
@fwdpy11.custom_genetic_value_decorators.default_update
2931
@fwdpy11.custom_genetic_value_decorators.genetic_value_is_trait_default_clone()
@@ -41,6 +43,9 @@ def __call__(self, data: fwdpy11.DiploidGeneticValueToFitnessData) -> float:
4143
/ (2 * self.VS)
4244
)
4345

46+
def validate_timings(self, deme: int, demography: ForwardDemesGraph) -> None:
47+
pass
48+
4449

4550
@fwdpy11.custom_genetic_value_decorators.genetic_value_is_trait_default_clone()
4651
@attr.s()
@@ -65,3 +70,6 @@ def update(self, pop: fwdpy11.DiploidPopulation) -> None:
6570
1,
6671
)[0]
6772
self.optima.append((pop.generation, self.opt))
73+
74+
def validate_timings(self, deme: int, demography: ForwardDemesGraph) -> None:
75+
pass

0 commit comments

Comments
 (0)