1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
|
"""The types used by our R/qtl2 system."""
from enum import Enum
from pathlib import Path
from dataclasses import field, dataclass
from typing import (Union,
Literal,
Optional,
Sequence,
TypeAlias,
Annotated)
_ALLELE_COUNTS_BY_CROSSTYPE = {
"bc": 2, "f2": 2, "riself": 2, "risib": 2, "dh": 2, "haploid": 2, "ail": 2,
"ail3": 3,
"riself4": 4, "risib4": 4,
"dh6": 6,
"hs": 8, "do": 8, "riself8": 8, "risib8": 8,
"dof1": 9, # 8 DO founders + 1 base inbred line
"riself16": 16,
"magic19": 19
}
PathLike = Union[Path, str]
AlleleLabel: TypeAlias = str
GenoCodeAlleleValue = Union[
Literal[-1, 0, 1],
Literal[1, 2, 3],# T0D0: reconcile this to genocodes of 0, 1, 2 instead
Annotated[float, "Value must be between 0.0 and 2.0"]
]
GenoCode = dict[AlleleLabel, GenoCodeAlleleValue]
# Sex information
SexKey = Union[str, int]# codes used in the covariate file
@dataclass(frozen=True)
class ControlDataSex:
"""Sex information in the control file."""
covar: Optional[str] = None
file: Optional[PathLike] = None
female: Optional[SexKey] = None
male: Optional[SexKey] = None
# Cross information
CrossType = Literal[
# T0D0: Look at https://kbroman.org/qtl2/assets/vignettes/input_files.html#Backcross to implement appropriate QC dependent on the crosstype.
"bc",# Backcross
"f2",# F2 intercross
"riself",# RIL by self -- RIL: Recombinant Inbred Lines
"risib",# RIL by sib
"dh",# Double haploid
"haploid",# Haploid
"ail",# AIL -- Advanced Inbred Lines
"hs",# Heterogeneous stock
"do",# Diversity outbreds
"riself4",# Multi-parent recombinant inbred lines
"riself8",# Multi-parent recombinant inbred lines
"riself16",# Multi-parent recombinant inbred lines
"risib4",# Multi-parent recombinant inbred lines
"risib8",# Multi-parent recombinant inbred lines
"magic19",# 19-way MAGIC lines
"dh6",# 6-way doubled haploids
"dof1",# DOF1
"ail3",# 3-way advanced intercross lines
"genail",# General advanced intercross lines
"genril",# General recombinant inbred lines
]
class CrossDirection(Enum):
FORWARD = 0
REVERSE = 1
@dataclass(frozen=True)
class ShortCrossInfo:
"""Cross information in the control file."""
# "covar": indicates the name of the column in the covariate data
covar: Optional[str] = None
file: Optional[PathLike] = None
forward_crosses: Sequence[str] = field(default_factory=tuple)
reverse_crosses: Sequence[str] = field(default_factory=tuple)
def __post_init__(self):
"""Validate the cross info."""
# A valid short config MUST have either a `covar` or a `file` source
# but not both.
if not (self.covar and self.file):
raise ValueError("ShortCrossInfo requires either a 'covar' or a "
"'file' parameter.")
if self.covar and self.file:
raise ValueError("ShortCrossInfo cannot define both 'covar' and "
"'file' simultaneously.")
LongCrossInfo: TypeAlias = PathLike
CrossInfo = Union[LongCrossInfo, ShortCrossInfo]
def expected_allele_count_for_crosstype(
alleles: Sequence[str],
crosstype: CrossType,
expected_counts: dict[CrossType, int]
) -> bool:
"""Check that the number of alleles matches what crosstype expects."""
def __validate__(expected, alleles):
if expected is not None and len(alleles) != expected:
raise ValueError(
f"Cross type '{crosstype}' expects exactly {expected} allele "
f"labels. Received {len(alleles)}: {alleles}")
if crosstype.startswith(("genail", "genril")):
# genail and genril are dynamic (e.g., genail8)
# Extract the trailing digits to determine expected founder count
num_part = "".join(filter(str.isdigit, crosstype))
if num_part:
return __validate__(int(num_part), alleles)
return __validate__(expected_counts.get(crosstype), alleles)
@dataclass(frozen=True)
class ControlData:
"""Class for the R/qtl2 control data."""
# File names: Force listings, rather than singular strings
geno: Sequence[PathLike] = field(default_factory=tuple)
founder_geno: Sequence[PathLike] = field(default_factory=tuple)
pheno: Sequence[PathLike] = field(default_factory=tuple)
covar: Sequence[PathLike] = field(default_factory=tuple)
phenocovar: Sequence[PathLike] = field(default_factory=tuple)
gmap: Sequence[PathLike] = field(default_factory=tuple)
pmap: Sequence[PathLike] = field(default_factory=tuple)
# X Chromosome
x_chr: Optional[str] = None
# Allele labels
alleles: Sequence[str] = field(default_factory=tuple)
# Genotype codes
genotypes: GenoCode = field(default_factory=dict)
# sex
sex: Optional[ControlDataSex] = None
# Cross info
crosstype: Optional[CrossType] = None
cross_info: Optional[CrossInfo] = None
# CSV fields
na_strings: Sequence[str] = ("-", "NA", "N/A")
sep: str = ","
comment_char: Optional[str] = "#"
def __post_init__(self):
if self.alleles and self.crosstype:
expected_allele_count_for_crosstype(
self.alleles, self.crosstype, _ALLELE_COUNTS_BY_CROSSTYPE)
|