PedNet 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pednet/inheritance.py ADDED
@@ -0,0 +1,262 @@
1
+ """The mode of inheritance that best explains a family's phenotypes.
2
+
3
+ For each mode (autosomal dominant or recessive, X-linked recessive or dominant), PedNet works out the probability of
4
+ everyone's phenotype given that the proband has the disease genotype for that mode, summing over every genotype the
5
+ rest of the family could have. Conditioning on the proband takes account of how the family came to be seen, and
6
+ makes the answer depend little on how common the disease allele is. The penetrance isn't known, so the probability
7
+ is averaged over a range of penetrances, and if no proband is given it is averaged over each affected person as the
8
+ proband. With the modes' prior probabilities, Bayes' theorem then gives each mode's posterior probability.
9
+ """
10
+ from dataclasses import dataclass, field
11
+ from typing import Dict, List, Optional, Sequence
12
+
13
+ import numpy as np
14
+
15
+ from .engine import Network
16
+ from .models import MODE_NAMES, MODES, X_LINKED, Model, founder_prior, phenotype_factor
17
+ from .pedigree import Pedigree
18
+
19
+ # The penetrances averaged over, equally weighted, when it isn't known
20
+ DEFAULT_PENETRANCE = (0.6, 0.8, 0.95, 0.99)
21
+ DEFAULT_PHENOCOPY = 0.001
22
+ LN10 = np.log(10)
23
+
24
+
25
+ def _logsumexp(x, axis=None):
26
+ x = np.asarray(x, dtype=float)
27
+ peak = np.max(x, axis=axis, keepdims=True)
28
+ peak = np.where(np.isfinite(peak), peak, 0.0)
29
+ with np.errstate(divide='ignore'):
30
+ out = np.log(np.sum(np.exp(x - peak), axis=axis, keepdims=True)) + peak
31
+ return np.squeeze(out, axis=axis) if axis is not None else float(out.reshape(()))
32
+
33
+
34
+ def _weights(penetrance, weights) -> np.ndarray:
35
+ if np.isscalar(penetrance):
36
+ penetrance = (float(penetrance),)
37
+ w = np.ones(len(penetrance)) if weights is None else np.asarray(weights, dtype=float)
38
+ if len(w) != len(penetrance) or np.any(w < 0) or w.sum() == 0:
39
+ raise ValueError('penetrance_weights must be one non-negative weight for each penetrance')
40
+ return w / w.sum()
41
+
42
+
43
+ def _prior(modes, prior) -> Dict[str, float]:
44
+ if prior is None:
45
+ return {m: 1 / len(modes) for m in modes}
46
+ total = sum(prior.get(m, 0) for m in modes)
47
+ if total <= 0:
48
+ raise ValueError('The prior must give some probability to one of the modes')
49
+ return {m: prior.get(m, 0) / total for m in modes}
50
+
51
+
52
+ def _modes(modes) -> List[str]:
53
+ if isinstance(modes, str):
54
+ modes = [m.strip() for m in modes.replace(',', ' ').split()]
55
+ modes = [m.upper() for m in modes]
56
+ for m in modes:
57
+ if m not in MODES:
58
+ raise ValueError(f'Unknown mode of inheritance {m!r}: use one of {", ".join(MODES)}')
59
+ return modes
60
+
61
+
62
+ @dataclass
63
+ class ModeEvidence:
64
+ mode: str
65
+ posterior: float
66
+ log10_likelihood: float
67
+
68
+ @property
69
+ def name(self) -> str:
70
+ return MODE_NAMES[self.mode]
71
+
72
+
73
+ @dataclass
74
+ class InheritanceResult:
75
+ """
76
+ The modes, most probable first. call is the most probable mode if its posterior probability reaches threshold,
77
+ and 'uncertain' otherwise. log10_likelihood is the log10 probability of the phenotypes under each mode (given
78
+ the proband), whose differences are log10 likelihood ratios.
79
+ """
80
+ family: str
81
+ modes: List[ModeEvidence]
82
+ call: str
83
+ threshold: float
84
+ probands: List[str]
85
+ notes: List[str] = field(default_factory=list)
86
+
87
+ def posterior(self, mode: str) -> float:
88
+ return next((m.posterior for m in self.modes if m.mode == mode), 0.0)
89
+
90
+ @property
91
+ def dominant(self) -> float:
92
+ """The probability that the condition is dominant (AD or XLD), among the modes weighed up"""
93
+ return self.posterior('AD') + self.posterior('XLD')
94
+
95
+ @property
96
+ def recessive(self) -> float:
97
+ """The probability that it is recessive (AR or XLR)"""
98
+ return self.posterior('AR') + self.posterior('XLR')
99
+
100
+ @property
101
+ def x_linked(self) -> float:
102
+ """The probability that it is X-linked (XLR or XLD)"""
103
+ return self.posterior('XLR') + self.posterior('XLD')
104
+
105
+ def table(self) -> List[dict]:
106
+ return [{'family': self.family, 'mode': m.mode, 'posterior': m.posterior, 'log10_likelihood': m.log10_likelihood}
107
+ for m in self.modes]
108
+
109
+ def __str__(self):
110
+ lines = [f'Family {self.family}: {self.call if self.call == "uncertain" else MODE_NAMES[self.call]}']
111
+ for m in self.modes:
112
+ lines.append(f' {m.mode:4s} {m.posterior:6.3f} ({m.name})')
113
+ names = {m.mode for m in self.modes}
114
+ if names & {'AD', 'XLD'} and names & {'AR', 'XLR'}:
115
+ lines.append(f' dominant {self.dominant:.3f}, recessive {self.recessive:.3f}')
116
+ if names & {'XLR', 'XLD'} and names & {'AD', 'AR'}:
117
+ lines.append(f' X-linked {self.x_linked:.3f}, autosomal {1 - self.x_linked:.3f}')
118
+ lines.extend(f' note: {n}' for n in self.notes)
119
+ return '\n'.join(lines)
120
+
121
+
122
+ def mode_log_likelihoods(pedigree: Pedigree, modes: Sequence[str] = MODES,
123
+ penetrance=DEFAULT_PENETRANCE, penetrance_weights=None,
124
+ phenocopy: float = DEFAULT_PHENOCOPY, allele_frequency: Optional[Dict[str, float]] = None,
125
+ carrier_penetrance: Optional[float] = None, probands: Optional[Sequence[str]] = None,
126
+ networks: Optional[Dict[bool, Network]] = None) -> Dict[str, float]:
127
+ """
128
+ The natural log of P(phenotypes | proband has the disease genotype, mode) for each mode, averaged over the
129
+ penetrances (with penetrance_weights) and over the probands (by default everyone affected).
130
+ """
131
+ modes = _modes(modes)
132
+ pens = (float(penetrance),) if np.isscalar(penetrance) else tuple(float(f) for f in penetrance)
133
+ with np.errstate(divide='ignore'):
134
+ log_w = np.log(_weights(pens, penetrance_weights))
135
+ probands = list(probands) if probands else pedigree.affected()
136
+ if not probands:
137
+ return {m: 0.0 for m in modes}
138
+ networks = {} if networks is None else networks
139
+ allele_frequency = allele_frequency or {}
140
+ result = {}
141
+ for mode in modes:
142
+ x = X_LINKED[mode]
143
+ if x not in networks:
144
+ networks[x] = Network(pedigree, x)
145
+ net = networks[x]
146
+ model = Model(mode, allele_frequency.get(mode), pens[0], phenocopy, carrier_penetrance)
147
+ F, A = len(pens), len(probands)
148
+ positions = [net.index(p) for p in probands]
149
+ with_phenotypes, without = [], []
150
+ for i, (kind, person) in enumerate(zip(net.kinds, pedigree.people)):
151
+ base = founder_prior(kind, model.allele_frequency) if net.parents[i] is None else np.ones(len(model.causal(kind)))
152
+ u = np.tile(base, (F * A, 1))
153
+ for fi, f in enumerate(pens):
154
+ u[fi * A:(fi + 1) * A] *= phenotype_factor(model.with_(penetrance=f), kind, person.phenotype)
155
+ u0 = np.tile(base, (A, 1))
156
+ for ai, pos in enumerate(positions):
157
+ if pos == i:
158
+ u[ai::A] *= model.causal(kind)
159
+ u0[ai] *= model.causal(kind)
160
+ with_phenotypes.append(u)
161
+ without.append(u0)
162
+ joint = net.log_likelihood(with_phenotypes).reshape(F, A)
163
+ proband_causal = net.log_likelihood(without)
164
+ with np.errstate(invalid='ignore'):
165
+ conditional = np.where(np.isfinite(proband_causal)[None, :], joint - proband_causal[None, :], -np.inf)
166
+ result[mode] = _logsumexp(conditional + log_w[:, None] - np.log(A))
167
+ return result
168
+
169
+
170
+ def infer_mode(pedigree: Pedigree, modes: Sequence[str] = MODES, penetrance=DEFAULT_PENETRANCE,
171
+ penetrance_weights=None, phenocopy: float = DEFAULT_PHENOCOPY,
172
+ allele_frequency: Optional[Dict[str, float]] = None, carrier_penetrance: Optional[float] = None,
173
+ prior: Optional[Dict[str, float]] = None, proband: Optional[str] = None,
174
+ threshold: float = 0.8) -> InheritanceResult:
175
+ """
176
+ The posterior probability of each mode of inheritance for a pedigree.
177
+
178
+ pedigree: a Pedigree (see read_ped)
179
+ modes: the modes to weigh up, from 'AD', 'AR', 'XLR' and 'XLD'
180
+ penetrance: the penetrance, or several to average over (by default 0.6, 0.8, 0.95 and 0.99, equally weighted)
181
+ penetrance_weights: the weight of each penetrance, if not equal
182
+ phenocopy: the chance that someone without the disease genotype is affected
183
+ allele_frequency: the disease allele frequency for each mode, if not the defaults
184
+ carrier_penetrance: for X-linked recessive, the chance that a woman with one copy is affected
185
+ prior: the prior probability of each mode (equal by default)
186
+ proband: the person through whom the family was found; by default each affected person in turn
187
+ threshold: the posterior probability the most probable mode needs for the call not to be 'uncertain'
188
+ """
189
+ modes = _modes(modes)
190
+ priors = _prior(modes, prior)
191
+ notes = list(pedigree.notes)
192
+ affected = pedigree.affected()
193
+ if proband is not None:
194
+ if proband not in pedigree:
195
+ raise ValueError(f'{proband} is not in family {pedigree.family}')
196
+ if not pedigree[proband].affected:
197
+ notes.append(f'the proband, {proband}, is not recorded as affected')
198
+ probands = [proband]
199
+ else:
200
+ probands = affected
201
+ if not affected:
202
+ notes.append('no one is affected, so the phenotypes say nothing about the mode of inheritance')
203
+ elif len(affected) == 1:
204
+ notes.append('only one person is affected: a new dominant mutation, which PedNet doesn\'t model, would '
205
+ 'explain that as well as a recessive condition does')
206
+ x_needed = any(X_LINKED[m] for m in modes)
207
+ unknown_sex = [p.id for p in pedigree.people if p.sex == 0 and not p.placeholder]
208
+ if x_needed and unknown_sex:
209
+ notes.append(f'the sex of {", ".join(unknown_sex[:5])}{" and others" if len(unknown_sex) > 5 else ""} '
210
+ 'is unknown, and is taken as equally likely to be either for the X-linked modes')
211
+ if probands:
212
+ logs = mode_log_likelihoods(pedigree, modes, penetrance, penetrance_weights, phenocopy, allele_frequency,
213
+ carrier_penetrance, probands)
214
+ else:
215
+ logs = {m: 0.0 for m in modes}
216
+ log_post = np.array([np.log(priors[m]) + logs[m] if priors[m] > 0 else -np.inf for m in modes])
217
+ total = _logsumexp(log_post)
218
+ possible = bool(np.isfinite(total))
219
+ post = np.exp(log_post - total) if possible else np.array([priors[m] for m in modes])
220
+ if probands and not possible:
221
+ notes.append('none of the modes allowed can give these phenotypes with this penetrance and phenocopy rate, '
222
+ 'so the probabilities shown are the priors')
223
+ evidence = [ModeEvidence(m, float(post[k]), float(logs[m] / LN10)) for k, m in enumerate(modes)]
224
+ evidence.sort(key=lambda e: -e.posterior)
225
+ call = evidence[0].mode if probands and possible and evidence[0].posterior >= threshold else 'uncertain'
226
+ return InheritanceResult(pedigree.family, evidence, call, threshold, probands, notes)
227
+
228
+
229
+ def infer_shared_mode(pedigrees, modes: Sequence[str] = MODES, penetrance=DEFAULT_PENETRANCE,
230
+ penetrance_weights=None, phenocopy: float = DEFAULT_PHENOCOPY,
231
+ allele_frequency: Optional[Dict[str, float]] = None, carrier_penetrance: Optional[float] = None,
232
+ prior: Optional[Dict[str, float]] = None, threshold: float = 0.8) -> InheritanceResult:
233
+ """
234
+ The posterior probability of each mode of inheritance for a condition that several families share, from the
235
+ product of the families' likelihoods (they are taken to be unrelated, with the same mode of inheritance, though
236
+ not necessarily the same gene). pedigrees is a dict of family to Pedigree, as read_ped gives, or a list of
237
+ pedigrees; the other arguments are as for infer_mode.
238
+ """
239
+ if isinstance(pedigrees, dict):
240
+ pedigrees = list(pedigrees.values())
241
+ modes = _modes(modes)
242
+ priors = _prior(modes, prior)
243
+ totals = {m: 0.0 for m in modes}
244
+ notes, probands = [], []
245
+ for pedigree in pedigrees:
246
+ result = infer_mode(pedigree, modes, penetrance, penetrance_weights, phenocopy, allele_frequency,
247
+ carrier_penetrance, None, None, threshold)
248
+ for m in result.modes:
249
+ totals[m.mode] += m.log10_likelihood * LN10
250
+ notes.extend(f'family {pedigree.family}: {n}' for n in result.notes)
251
+ probands.extend(result.probands)
252
+ log_post = np.array([np.log(priors[m]) + totals[m] if priors[m] > 0 else -np.inf for m in modes])
253
+ total = _logsumexp(log_post)
254
+ possible = bool(np.isfinite(total))
255
+ post = np.exp(log_post - total) if possible else np.array([priors[m] for m in modes])
256
+ if probands and not possible:
257
+ notes.append('none of the modes allowed can give these phenotypes with this penetrance and phenocopy rate, '
258
+ 'so the probabilities shown are the priors')
259
+ evidence = [ModeEvidence(m, float(post[k]), float(totals[m] / LN10)) for k, m in enumerate(modes)]
260
+ evidence.sort(key=lambda e: -e.posterior)
261
+ call = evidence[0].mode if probands and possible and evidence[0].posterior >= threshold else 'uncertain'
262
+ return InheritanceResult(', '.join(p.family for p in pedigrees), evidence, call, threshold, probands, notes)
pednet/models.py ADDED
@@ -0,0 +1,209 @@
1
+ """Modes of inheritance, and the genotype probabilities they give.
2
+
3
+ Each person's genotype at the locus is a variable with a few states. For an autosomal locus these are the number of
4
+ copies of the alternate (disease) allele, 0, 1 or 2 (kind "A"). On the X chromosome a man has one copy, 0 or 1
5
+ (kind "M"), and a woman two (kind "F"). Someone of unknown sex who has no children has the man's two states and the
6
+ woman's three, each equally likely beforehand (kind "U"); a parent's sex is always known from their role.
7
+ """
8
+ from dataclasses import dataclass, replace
9
+ from functools import lru_cache
10
+ from typing import Dict, Optional
11
+
12
+ import numpy as np
13
+
14
+ MODES = ('AD', 'AR', 'XLR', 'XLD')
15
+ MODE_NAMES = {'AD': 'autosomal dominant', 'AR': 'autosomal recessive',
16
+ 'XLR': 'X-linked recessive', 'XLD': 'X-linked dominant'}
17
+ X_LINKED = {'AD': False, 'AR': False, 'XLR': True, 'XLD': True}
18
+
19
+ # the number of copies of the alternate allele in each state, and whether the state is a man's
20
+ ALT_COPIES = {'A': (0, 1, 2), 'M': (0, 1), 'F': (0, 1, 2), 'U': (0, 1, 0, 1, 2)}
21
+ IS_MALE = {'A': (None, None, None), 'M': (True, True), 'F': (False, False, False),
22
+ 'U': (True, True, False, False, False)}
23
+
24
+ # Default disease allele frequencies. The mode of inheritance is judged given that the proband carries the disease
25
+ # genotype, which makes the results depend little on these, as long as the allele is rare.
26
+ DEFAULT_FREQUENCY = {'AD': 1e-4, 'AR': 1e-3, 'XLR': 5e-4, 'XLD': 1e-4}
27
+
28
+
29
+ def domain(kind: str) -> int:
30
+ return len(ALT_COPIES[kind])
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Model:
35
+ """
36
+ A mode of inheritance with its parameters:
37
+
38
+ mode: 'AD', 'AR', 'XLR' or 'XLD'
39
+ allele_frequency: the disease allele's frequency in the population
40
+ penetrance: the chance that someone with the disease genotype is affected
41
+ phenocopy: the chance that someone without it is affected all the same
42
+ carrier_penetrance: for XLR only, the chance that a woman with one copy is affected (by default the phenocopy rate)
43
+ """
44
+ mode: str
45
+ allele_frequency: Optional[float] = None
46
+ penetrance: float = 0.95
47
+ phenocopy: float = 0.001
48
+ carrier_penetrance: Optional[float] = None
49
+
50
+ def __post_init__(self):
51
+ if self.mode not in MODES:
52
+ raise ValueError(f'Unknown mode of inheritance {self.mode!r}: use one of {", ".join(MODES)}')
53
+ if self.allele_frequency is None:
54
+ object.__setattr__(self, 'allele_frequency', DEFAULT_FREQUENCY[self.mode])
55
+ for name in ('allele_frequency', 'penetrance', 'phenocopy'):
56
+ value = getattr(self, name)
57
+ if not 0 <= value <= 1:
58
+ raise ValueError(f'{name} must be between 0 and 1, not {value}')
59
+
60
+ def with_(self, **changes) -> 'Model':
61
+ return replace(self, **changes)
62
+
63
+ @property
64
+ def x_linked(self) -> bool:
65
+ return X_LINKED[self.mode]
66
+
67
+ def kinds(self):
68
+ return ('M', 'F', 'U') if self.x_linked else ('A',)
69
+
70
+ def affected_probability(self, kind: str) -> np.ndarray:
71
+ """P(affected) for each state of a genotype of this kind"""
72
+ f, p = self.penetrance, self.phenocopy
73
+ h = p if self.carrier_penetrance is None else self.carrier_penetrance
74
+ if self.mode == 'AD':
75
+ return np.array([p, f, f])
76
+ if self.mode == 'AR':
77
+ return np.array([p, p, f])
78
+ male = np.array([p, f])
79
+ female = np.array([p, h, f]) if self.mode == 'XLR' else np.array([p, f, f])
80
+ return {'M': male, 'F': female, 'U': np.concatenate([male, female])}[kind]
81
+
82
+ def causal(self, kind: str) -> np.ndarray:
83
+ """Which states of a genotype of this kind are the disease genotype (as 1.0 or 0.0)"""
84
+ if self.mode == 'AD':
85
+ return np.array([0., 1., 1.])
86
+ if self.mode == 'AR':
87
+ return np.array([0., 0., 1.])
88
+ male = np.array([0., 1.])
89
+ female = np.array([0., 0., 1.]) if self.mode == 'XLR' else np.array([0., 1., 1.])
90
+ return {'M': male, 'F': female, 'U': np.concatenate([male, female])}[kind]
91
+
92
+
93
+ def founder_prior(kind: str, q) -> np.ndarray:
94
+ """
95
+ The genotype probabilities of someone with no parents in the pedigree, in Hardy-Weinberg proportions.
96
+ q may be a single allele frequency or an array of them (one per batch), giving shape (batch, states).
97
+ """
98
+ q = np.asarray(q, dtype=float)
99
+ p = 1 - q
100
+ diploid = np.stack([p * p, 2 * p * q, q * q], axis=-1)
101
+ haploid = np.stack([p, q], axis=-1)
102
+ if kind in ('A', 'F'):
103
+ return diploid
104
+ if kind == 'M':
105
+ return haploid
106
+ return np.concatenate([0.5 * haploid, 0.5 * diploid], axis=-1)
107
+
108
+
109
+ def phenotype_factor(model: Model, kind: str, phenotype: int) -> np.ndarray:
110
+ """P(the person's phenotype) for each state: 2 affected, 1 unaffected, anything else unknown"""
111
+ affected = model.affected_probability(kind)
112
+ if phenotype == 2:
113
+ return affected
114
+ if phenotype == 1:
115
+ return 1 - affected
116
+ return np.ones(domain(kind))
117
+
118
+
119
+ def genotype_factor(kind: str, alt_copies: Optional[int], error_rate: float = 1e-4) -> np.ndarray:
120
+ """
121
+ P(the observed genotype) for each state, allowing for genotyping errors. alt_copies is the number of copies of the
122
+ alternate allele called (0, 1 or 2), or None if the person wasn't genotyped.
123
+
124
+ A call one copy out (a heterozygote called homozygous, or the other way) happens with probability error_rate,
125
+ and one two copies out (a homozygote called the other homozygote) with probability error_rate squared, so that a
126
+ rare variant called homozygous in two affected siblings isn't put down to two errors. A man's X-chromosome call
127
+ counts as the alternate allele if it has any copy of it; calling his allele when he doesn't have it is a single
128
+ error, and missing it when he does is a double one, since he has nothing else at that position.
129
+ """
130
+ states = ALT_COPIES[kind]
131
+ if alt_copies is None:
132
+ return np.ones(len(states))
133
+ return np.array([call_probability(copies, male, alt_copies, error_rate)
134
+ for copies, male in zip(states, IS_MALE[kind])])
135
+
136
+
137
+ @lru_cache(maxsize=None)
138
+ def genotype_table(kind: str, error_rate: float) -> np.ndarray:
139
+ """
140
+ genotype_factor for every call, as rows: not genotyped, then 0, 1 and 2 copies called, to be indexed by
141
+ call_codes. The table is shared, so it is read-only.
142
+ """
143
+ table = np.stack([genotype_factor(kind, called, error_rate) for called in (None, 0, 1, 2)])
144
+ table.flags.writeable = False
145
+ return table
146
+
147
+
148
+ def call_codes(variants, person_id: str) -> np.ndarray:
149
+ """Each variant's genotype call in one person as a row of genotype_table: 0 if not genotyped, else copies + 1"""
150
+ return np.array([0 if g is None else min(int(g), 2) + 1
151
+ for g in (v.genotypes.get(person_id) for v in variants)], dtype=np.intp)
152
+
153
+
154
+ def call_probability(copies: int, male: Optional[bool], called: int, error_rate: float) -> float:
155
+ """
156
+ The chance of a genotype call of called copies of the alternate allele from someone with copies of it, for a
157
+ man's X chromosome (male True) or a pair of chromosomes (see genotype_factor)
158
+ """
159
+ e = error_rate
160
+ if male:
161
+ if called >= 1:
162
+ return 1 - e * e if copies == 1 else e
163
+ return 1 - e if copies == 0 else e * e
164
+ off = abs(called - copies)
165
+ if off == 0:
166
+ return 1 - 2 * e if copies == 1 else 1 - e - e * e
167
+ return e if off == 1 else e * e
168
+
169
+
170
+ def _passes(copies: int) -> float:
171
+ """The chance that a parent with this many copies passes one on (a man passes his X to every daughter)"""
172
+ return copies / 2
173
+
174
+
175
+ def transmission(kind: str) -> np.ndarray:
176
+ """
177
+ P(child's state | father's state, mother's state), as an array indexed [child, father, mother].
178
+ For an autosomal locus all three are of kind A. On the X chromosome, the father is of kind M and the mother of
179
+ kind F, and the child of the kind given (M a son, F a daughter, U unknown sex).
180
+ """
181
+ if kind == 'A':
182
+ t = np.zeros((3, 3, 3))
183
+ for f in range(3):
184
+ for m in range(3):
185
+ a, b = _passes(f), _passes(m)
186
+ t[2, f, m] = a * b
187
+ t[1, f, m] = a * (1 - b) + (1 - a) * b
188
+ t[0, f, m] = (1 - a) * (1 - b)
189
+ return t
190
+ son = np.zeros((2, 2, 3))
191
+ daughter = np.zeros((3, 2, 3))
192
+ for h in range(2):
193
+ for m in range(3):
194
+ b = _passes(m)
195
+ son[1, h, m], son[0, h, m] = b, 1 - b
196
+ # a daughter has her father's only X, and one of her mother's two
197
+ daughter[h + 1, h, m] += b
198
+ daughter[h, h, m] += 1 - b
199
+ if kind == 'M':
200
+ return son
201
+ if kind == 'F':
202
+ return daughter
203
+ return np.concatenate([0.5 * son, 0.5 * daughter], axis=0)
204
+
205
+
206
+ def default_models(modes=MODES, penetrance: float = 0.95, phenocopy: float = 0.001,
207
+ allele_frequency: Optional[Dict[str, float]] = None) -> Dict[str, Model]:
208
+ allele_frequency = allele_frequency or {}
209
+ return {m: Model(m, allele_frequency.get(m), penetrance, phenocopy) for m in modes}