33"""
44
55import attr
6+ import collections .abc
67import numpy as np
78
89
910# TODO: somewhere we need to check that the number of traits
1011# is consistent between Phenotypes, Environments, FitnessFunctions,
1112# MultivariateMutationTypes, DistributionofMutationEffects (DMEs), etc.
1213
14+
1315# TODO: this was moved from dfe.py so technically
1416# this would break backward compatibility, but
1517# I believe this method is no longer used
@@ -22,7 +24,7 @@ def _copy_converter(x):
2224 return x
2325
2426
25- class Traits (object ):
27+ class TraitsModel (object ):
2628 def __init__ (self , phenotypes ):
2729 """
2830 A collection of genetically determined traits,
@@ -183,25 +185,33 @@ def __attrs_post_init__(self):
183185
184186class FitnessFunction (object ):
185187 """
186- Class to store a fitness function.
188+ TODO WRITE THIS BETTER
189+ Class to store a model of a component of fitness:
190+ each such component maps a collection of phenotypes
191+ to a value that multiplies the fitness.
192+ Also contained here is when and where this component applies.
193+
194+ Options for ``function_type``, and corresponding ``function_args``, are:
195+
196+ TODO
187197
188- :ivar traits: List of trait names or indices, as initialized in Traits object .
198+ :ivar traits: List of phenotype IDs .
189199 :vartype traits: list
190- :ivar function_type: One-letter string corresponding to fitness function type
200+ :ivar function_type: String corresponding to fitness function type
191201 :vartype function_type: str
192202 :ivar function_args: Tuple containing parameters for the fitness function
193203 :vartype function_args: str
194- :ivar spacetime: Generations and populations for which this fitness function applies
195- :vartype spacetime: list of tuples (?)
196204 """
197205
206+ # :ivar spacetime: Generations and populations
207+ # for which this fitness function applies
208+ # :vartype spacetime: list of tuples (?)
209+
198210 # TODO check function_args depending on function_type
199211 # TODO check dimensions of traits against dimensions of function_args,
200212 # depending on function_type - plus check dimensions >=1
201213 # TODO much later - check spacetime is formatted correctly
202214
203- supported_function_types = [] # TODO
204-
205215 traits = attr .ib (type = list )
206216 function_type = attr .ib (type = str )
207217 function_args = attr .ib (type = tuple )
@@ -210,20 +220,14 @@ class FitnessFunction(object):
210220 def __attrs_post_init__ (self ):
211221 if len (self .traits ) < 1 :
212222 raise ValueError ("At least one trait must be specified." )
213- if self .function_type not in self .supported_function_types :
214- raise ValueError ("Proposed fitness function not supported at this time." )
223+ # _check_function_types(self.function_type, self.function_args, len(self.traits))
215224
216225
217- @attr .s (kw_only = True )
218- class MultivariateEffectSizeDistribution :
219- def __attrs_post_init__ (self ):
220- # Make sure fitness is not affected
221- if 0 in self .supported_indices :
222- raise ValueError (
223- "distributions cannot simultaneously directly affect fitness "
224- "and traits. that is, if 0 (fitness) is a supported index "
225- "no other index can also be supported in supported_indices."
226- )
226+ def _check_distribution (distribution_type , distribution_args , dim ):
227+ if dim > 1 :
228+ _check_multivariate_distribution (distribution_type , distribution_args , dim )
229+ else :
230+ _check_univariate_distribution (distribution_type , distribution_args )
227231
228232
229233def _check_multivariate_distribution (distribution_type , distribution_args , dim ):
@@ -240,7 +244,6 @@ def _check_multivariate_distribution(distribution_type, distribution_args, dim):
240244
241245
242246def _check_multivariate_normal_args (distribution_args , dim ):
243- # TODO: I don't think we need the "list of dimensions that are not zero"
244247 # Multivariate Normal distribution with
245248 # (mean, covariance, indices) parameterization.
246249 if len (distribution_args ) != 3 :
@@ -372,9 +375,13 @@ def _check_univariate_distribution(distribution_type, distribution_args):
372375class MultivariateMutationType (object ):
373376 """
374377 Class representing a "type" of mutation, allowing the mutation to affect
375- fitness and/or trait(s). This design closely mirrors SLiM's MutationType
376- class.
378+ fitness and/or trait(s). This design closely mirrors :class: MutationType.
379+
380+ TODO: is "dominance_coeff_list" still the way we want to do things?
381+ SLiM is more flexible in this now.
377382
383+ :ivar phenotype_ids: A list of IDs of phenotypes this mutation type affects.
384+ :vartype phenotype_ids: list
378385 :ivar distribution_type: A str abbreviation for the distribution of
379386 effects that each new mutation of this type draws from (see above).
380387 :vartype distribution_type: str
@@ -399,114 +406,93 @@ class MultivariateMutationType(object):
399406 :vartype dominance_coeff_breaks: list of floats
400407 """
401408
402- trait_distribution_type = attr .ib (default = None , type = str )
403- trait_distribution_args = attr .ib (
409+ phenotype_ids = attr .ib ()
410+ distribution_type = attr .ib (default = None , type = str )
411+ distribution_args = attr .ib (
404412 factory = lambda : [0 ], type = list , converter = _copy_converter
405413 )
406-
407- fitness_dominance_coeff = attr .ib (default = None , type = float )
408- fitness_distribution_type = attr .ib (default = "f" , type = str )
409- fitness_distribution_args = attr .ib (
410- factory = lambda : [0 ], type = list , converter = _copy_converter
411- )
412- # TODO: is this okay if something affects traits
414+ dominance_coeff = attr .ib (default = None , type = float )
413415 convert_to_substitution = attr .ib (default = True , type = bool )
414- fitness_dominance_coeff_list = attr .ib (
415- default = None , type = list , converter = _copy_converter
416- )
417- fitness_dominance_coeff_breaks = attr .ib (
418- default = None , type = list , converter = _copy_converter
419- )
416+ dominance_coeff_list = attr .ib (default = None , type = list , converter = _copy_converter )
417+ dominance_coeff_breaks = attr .ib (default = None , type = list , converter = _copy_converter )
420418
421419 def __attrs_post_init__ (self ):
422- if (
423- self .fitness_dominance_coeff is None
424- and self .fitness_dominance_coeff_list is None
425- ):
426- self .fitness_dominance_coeff = 0.5
427-
428- if self .fitness_dominance_coeff is not None :
429- if (self .fitness_dominance_coeff_list is not None ) or (
430- self .fitness_dominance_coeff_breaks is not None
420+ if self .dominance_coeff is None and self .dominance_coeff_list is None :
421+ self .dominance_coeff = 0.5
422+
423+ if self .dominance_coeff is not None :
424+ if (self .dominance_coeff_list is not None ) or (
425+ self .dominance_coeff_breaks is not None
431426 ):
432427 raise ValueError (
433428 "Cannot specify both fitness_dominance_coeff "
434429 "and fitness_dominance_coeff_list."
435430 )
436- if not isinstance (self .fitness_dominance_coeff , (float , int )):
437- raise ValueError ("fitness_dominance_coeff must be a number." )
438- if not np .isfinite (self .fitness_dominance_coeff ):
431+ if not isinstance (self .dominance_coeff , (float , int )):
432+ raise ValueError ("dominance_coeff must be a number." )
433+ if not np .isfinite (self .dominance_coeff ):
439434 raise ValueError (
440- "Invalid fitness dominance "
441- f"coefficient { self .fitness_dominance_coeff } ."
435+ f"Invalid dominance coefficient { self .dominance_coeff } ."
442436 )
443437
444- if self .fitness_dominance_coeff_list is not None :
438+ if self .dominance_coeff_list is not None :
445439 # disallow the inefficient and annoying length-one case
446- if len (self .fitness_dominance_coeff_list ) < 2 :
447- raise ValueError (
448- "fitness_dominance_coeff_list must have at least 2 elements."
449- )
450- for h in self .fitness_dominance_coeff_list :
440+ if len (self .dominance_coeff_list ) < 2 :
441+ raise ValueError ("dominance_coeff_list must have at least 2 elements." )
442+ for h in self .dominance_coeff_list :
451443 if not isinstance (h , (float , int )):
452- raise ValueError (
453- "fitness_dominance_coeff_list must be a list of numbers."
454- )
444+ raise ValueError ("dominance_coeff_list must be a list of numbers." )
455445 if not np .isfinite (h ):
456- raise ValueError (f"Invalid fitness dominance coefficient { h } ." )
457- if self .fitness_dominance_coeff_breaks is None :
446+ raise ValueError (f"Invalid dominance coefficient { h } ." )
447+ if self .dominance_coeff_breaks is None :
458448 raise ValueError (
459- "A list of fitness dominance coefficients provided but no breaks."
449+ "A list of dominance coefficients provided but no breaks."
460450 )
461- if (
462- len (self .fitness_dominance_coeff_list )
463- != len (self .fitness_dominance_coeff_breaks ) + 1
464- ):
451+ if len (self .dominance_coeff_list ) != len (self .dominance_coeff_breaks ) + 1 :
465452 raise ValueError (
466- "len(fitness_dominance_coeff_list ) must be equal "
467- "to len(fitness_dominance_coeff_breaks ) + 1"
453+ "len(dominance_coeff_list ) must be equal "
454+ "to len(dominance_coeff_breaks ) + 1"
468455 )
469456 lb = - 1 * np .inf
470- for b in self .fitness_dominance_coeff_breaks :
457+ for b in self .dominance_coeff_breaks :
471458 if not isinstance (b , (float , int )):
472459 raise ValueError (
473- "fitness_dominance_coeff_breaks must be a list of numbers."
460+ "dominance_coeff_breaks must be a list of numbers."
474461 )
475462 if not np .isfinite (b ):
476- raise ValueError (
477- f"Invalid fitness dominance coefficient break { b } ."
478- )
463+ raise ValueError (f"Invalid dominance coefficient break { b } ." )
479464 if b < lb :
480- raise ValueError (
481- "fitness_dominance_coeff_breaks must be nondecreasing."
482- )
465+ raise ValueError ("dominance_coeff_breaks must be nondecreasing." )
483466 lb = b
484467
485- _check_univariate_distribution (
486- self .fitness_distribution_type , self .fitness_distribution_args
487- )
488-
489468 if not isinstance (self .convert_to_substitution , bool ):
490469 raise ValueError ("convert_to_substitution must be bool." )
491470
492- _check_multivariate_distribution (
493- self .trait_distribution_type , self .trait_distribution_args
471+ _check_distribution (
472+ self .distribution_type , self .distribution_args , len ( self . phenotype_ids )
494473 )
495474
475+ # TODO
496476 # The index(s) of the param in the distribution_args list that should be
497477 # multiplied by Q when using --slim-scaling-factor Q.
498- self . fitness_Q_scaled_index = {
478+ scaling_factor_index_lookup = {
499479 "e" : [0 ], # mean
500480 "f" : [0 ], # fixed value
501481 "g" : [0 ], # mean
502482 "n" : [0 , 1 ], # mean and sd
503483 "w" : [0 ], # scale
504484 "s" : [], # script types should just printout arguments
505- }[self .fitness_distribution_type ]
485+ }
486+ assert self .distribution_type in scaling_factor_index_lookup
487+ self .fitness_Q_scaled_index = scaling_factor_index_lookup [
488+ self .distribution_type
489+ ]
506490
507491 @property
508492 def directly_affects_fitness (self ):
509493 """
494+ TODO: do we need this?
495+
510496 Tests whether the mutation type has direct effects on fitness. This is
511497 defined here to be of type "f" and with fitness effect 0.0, and so
512498 excludes other situations that also produce only neutral mutations
@@ -518,8 +504,82 @@ def directly_affects_fitness(self):
518504 )
519505
520506
521- # superclass of DFE --> DME
507+ # at least conceptually a superclass of DFE, so we call it DME
522508class DistributionOfMutationEffects (object ):
509+ """
510+ Class representing all mutations that affect a given segment of genome,
511+ and hence contains a list of :class:`.MultivariateMutationType`
512+ and corresponding list of proportions,
513+ that gives the proportions of mutations falling in this region
514+ that are of the corresponding mutation type.
515+
516+ ``proportions`` and ``mutation_types`` must be lists of the same length,
517+ and ``proportions`` should be nonnegative numbers summing to 1.
518+
519+ TODO: do we want id, description, long_description?
520+
521+ :ivar ~.mutation_types: A list of :class:`.MultivariateMutationType`
522+ objects associated with the DFE. Defaults to an empty list.
523+ :vartype ~.mutation_types: list
524+ :ivar ~.proportions: A list of the proportions of new mutations that
525+ fall in to each of the mutation types (must sum to 1).
526+ :vartype ~.proportions: list
527+ :ivar ~.id: The unique identifier for this model. DFE IDs should be
528+ short and memorable, and conform to the stdpopsim
529+ :ref:`naming conventions <sec_development_naming_conventions>`
530+ for DFE models.
531+ :vartype ~.id: str
532+ :ivar ~.description: A short description of this model as it would be used in
533+ written text, e.g., "Lognormal DFE". This should
534+ describe the DFE itself and not contain author or year information.
535+ :vartype ~.description: str
536+ :ivar long_description: A concise, but detailed, summary of the DFE model.
537+ :vartype long_description: str
538+ """
539+
540+ # TODO: what's this note about?
523541 # Remember to make sure none of the components MutationTypes are converting
524542 # to substitutions unless they only affect fitness
525- pass
543+
544+ id = attr .ib ()
545+ description = attr .ib ()
546+ long_description = attr .ib ()
547+ mutation_types = attr .ib (default = None )
548+ proportions = attr .ib (default = None )
549+ # citations = attr.ib(default=None)
550+ # qc_dfe = attr.ib(default=None)
551+
552+ # TODO: repetition here with DFE
553+ def __attrs_post_init__ (self ):
554+ self .mutation_types = [] if self .mutation_types is None else self .mutation_types
555+ if self .proportions is None and len (self .mutation_types ) == 0 :
556+ self .proportions = []
557+ elif self .proportions is None :
558+ # will error below if this doesn't make sense
559+ self .proportions = [1 ]
560+
561+ if not (isinstance (self .proportions , (collections .abc .Sequence , np .ndarray ))):
562+ raise ValueError ("proportions must be a list or numpy array." )
563+
564+ if not (isinstance (self .mutation_types , list )):
565+ raise ValueError ("mutation_types must be a list." )
566+
567+ if not (len (self .proportions ) == len (self .mutation_types )):
568+ raise ValueError (
569+ "proportions and mutation_types must be lists of the same length."
570+ )
571+
572+ for p in self .proportions :
573+ if not isinstance (p , (float , int )) or p < 0 :
574+ raise ValueError ("proportions must be nonnegative numbers." )
575+
576+ if len (self .proportions ) > 0 :
577+ sum_p = sum (self .proportions )
578+ if not np .isclose (sum_p , 1 ):
579+ raise ValueError ("proportions must sum to 1.0." )
580+
581+ for m in self .mutation_types :
582+ if not isinstance (m , MultivariateMutationType ):
583+ raise ValueError (
584+ "mutation_types must be a list of MultivariateMutationType objects."
585+ )
0 commit comments