gwas-sumstats-parser 0.5.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.
@@ -0,0 +1,33 @@
1
+ """Shared vocabulary, parsing and standardisation for GWAS summary-statistics
2
+ files.
3
+
4
+ This package is the common ingest layer consumed by both ``merit`` and
5
+ ``gwas-norm``. It owns the column vocabulary, the value parsers and the
6
+ route table that says which quantity is derived from which columns; the
7
+ consumers own everything downstream of a standardised row.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ # The "as" form marks this as a deliberate re-export, so callers can read
13
+ # gwas_sumstats_parser.__version__ while the literal lives only in _version.py
14
+ from ._version import __version__ as __version__
15
+ from .assembly import AssemblyResult, check_assembly, load_probe_table
16
+ from .reader import DroppingReader, Reader
17
+ from .errors import ParseError
18
+ from .vocabulary import COLUMNS, Column
19
+
20
+ # Import examples into the main package
21
+ from .example_data import examples
22
+
23
+ __all__ = [
24
+ "AssemblyResult",
25
+ "COLUMNS",
26
+ "Column",
27
+ "DroppingReader",
28
+ "ParseError",
29
+ "Reader",
30
+ "check_assembly",
31
+ "examples",
32
+ "load_probe_table",
33
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0"
@@ -0,0 +1,463 @@
1
+ """Check which of two genome builds an indexed file's coordinates follow.
2
+
3
+ The check is standalone: parsing never invokes it. It looks up a table of
4
+ probe variants, each with a position in build A and in build B, through the
5
+ reader's indexed region query, and reports how many probes it found at each
6
+ build's position together with a decision that may be undecided.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import csv
12
+ from dataclasses import dataclass
13
+ from importlib import resources
14
+ from os import PathLike
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from .errors import ParseError
19
+ from .opening import detect_index, open_source
20
+ from .parsers import (
21
+ compile_allele_pattern,
22
+ parse_allele,
23
+ parse_chromosome,
24
+ parse_number,
25
+ parse_position,
26
+ )
27
+ from .reader import DroppingReader, Region
28
+
29
+ SPECIES_KEY = "#species"
30
+ BUILDS_KEY = "#builds"
31
+ PROBE_COLUMNS = (
32
+ "probe_id",
33
+ "chr_a",
34
+ "pos_a",
35
+ "chr_b",
36
+ "pos_b",
37
+ "ref",
38
+ "alt",
39
+ "coverage_score",
40
+ )
41
+ BUNDLED_PROBES = ("probe_data", "assembly_probes.tsv")
42
+ UNKNOWN = "unknown"
43
+ AMBIGUOUS = "ambiguous"
44
+ # Reader settings the check decides itself (specification 001, A-008): a
45
+ # probe match needs both alleles, and the widest alphabet loses no row.
46
+ FIXED_READER_SETTINGS: dict[str, object] = {
47
+ "allele_level": "iupac_indel",
48
+ "require_other_allele": True,
49
+ }
50
+ _PROBE_ALLELE_PATTERN = compile_allele_pattern("iupac_indel")
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class Probe:
55
+ """One probe variant with its position in each of the two builds.
56
+
57
+ Parameters
58
+ ----------
59
+ probe_id : str
60
+ Identifier of the probe.
61
+ chr_a, chr_b : str
62
+ Chromosome in build A and in build B, as written in the table.
63
+ pos_a, pos_b : int
64
+ One-based position in build A and in build B.
65
+ ref, alt : str
66
+ The two alleles; matching ignores which is which.
67
+ coverage_score : float
68
+ How commonly the probe appears in files; carried, not used in scoring.
69
+ """
70
+
71
+ probe_id: str
72
+ chr_a: str
73
+ pos_a: int
74
+ chr_b: str
75
+ pos_b: int
76
+ ref: str
77
+ alt: str
78
+ coverage_score: float
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class ProbeTable:
83
+ """A species, its two builds in A then B order, and the probes.
84
+
85
+ Parameters
86
+ ----------
87
+ species : str
88
+ Species label from the table's species comment.
89
+ builds : tuple[str, str]
90
+ Build names from the table's builds comment, A first.
91
+ probes : tuple[Probe, ...]
92
+ Probe records in file order.
93
+ """
94
+
95
+ species: str
96
+ builds: tuple[str, str]
97
+ probes: tuple[Probe, ...]
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class AssemblyResult:
102
+ """Evidence and decision of one assembly check.
103
+
104
+ Parameters
105
+ ----------
106
+ species : str
107
+ Species label of the probe table used.
108
+ builds : tuple[str, str]
109
+ The two build names examined, A first.
110
+ decision : str
111
+ One of the two build names, ``"ambiguous"`` when both builds score
112
+ well and too close together, or ``"unknown"`` when too few probes
113
+ were found or neither build scores well enough.
114
+ hits : dict[str, int]
115
+ Per build, the probes found at that build's position with the
116
+ probe's two alleles.
117
+ scores : dict[str, float]
118
+ Per build, its hits divided by the examined count; zero when nothing
119
+ was examined.
120
+ examined : int
121
+ Probes found at their position in either build, whatever the alleles.
122
+ failed_rows : int
123
+ Fetched rows that failed to parse and were skipped as no evidence.
124
+ """
125
+
126
+ species: str
127
+ builds: tuple[str, str]
128
+ decision: str
129
+ hits: dict[str, int]
130
+ scores: dict[str, float]
131
+ examined: int
132
+ failed_rows: int
133
+
134
+
135
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
136
+ def load_probe_table(path: str | PathLike[str] | None = None) -> ProbeTable:
137
+ """Read and validate a probe table; without a path, the bundled human one.
138
+
139
+ Parameters
140
+ ----------
141
+ path : str or PathLike[str] or None
142
+ Tab-separated probe table. ``None`` reads the human GRCh37/GRCh38
143
+ table shipped inside the package.
144
+
145
+ Returns
146
+ -------
147
+ ProbeTable
148
+ Species, builds and probes.
149
+
150
+ Raises
151
+ ------
152
+ ParseError
153
+ If the species or builds comment is absent or malformed, a required
154
+ column is absent, there are no probes, or a cell is invalid. A
155
+ supplied table never falls back to the human labels.
156
+
157
+ Notes
158
+ -----
159
+ The first line is ``#species``, a tab, and the species. The second is
160
+ ``#builds``, a tab, and exactly two different build names separated by a
161
+ tab, A then B. The third is a header holding at least ``probe_id``,
162
+ ``chr_a``, ``pos_a``, ``chr_b``, ``pos_b``, ``ref``, ``alt`` and
163
+ ``coverage_score``, in any order. Alleles use the widest allele alphabet,
164
+ the one the check reads file rows with.
165
+ """
166
+ if path is None:
167
+ text = (
168
+ resources.files(__package__).joinpath(*BUNDLED_PROBES).read_text()
169
+ )
170
+ else:
171
+ text = Path(path).read_text()
172
+ lines = text.splitlines()
173
+ species = _comment_values(lines, 0, SPECIES_KEY)
174
+ if len(species) != 1:
175
+ raise ParseError(
176
+ "probe table must begin with a #species comment naming one species",
177
+ error_func=load_probe_table,
178
+ error_value=lines[0] if lines else "",
179
+ )
180
+ builds = _comment_values(lines, 1, BUILDS_KEY)
181
+ if len(builds) != 2 or builds[0] == builds[1]:
182
+ raise ParseError(
183
+ "probe table needs a #builds comment naming exactly two builds",
184
+ error_func=load_probe_table,
185
+ error_value=lines[1] if len(lines) > 1 else "",
186
+ )
187
+ records = csv.DictReader(lines[2:], delimiter="\t")
188
+ header = records.fieldnames or []
189
+ for name in PROBE_COLUMNS:
190
+ if name not in header:
191
+ raise ParseError(
192
+ "probe table lacks a required column",
193
+ error_func=load_probe_table,
194
+ error_value=name,
195
+ )
196
+ probes = tuple(_probe(record) for record in records)
197
+ if not probes:
198
+ raise ParseError(
199
+ "probe table holds no probes",
200
+ error_func=load_probe_table,
201
+ error_value=path,
202
+ )
203
+ return ProbeTable(species[0], (builds[0], builds[1]), probes)
204
+
205
+
206
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
207
+ def _comment_values(lines: list[str], index: int, key: str) -> list[str]:
208
+ """Return the non-empty values of the comment expected on one line.
209
+
210
+ An absent line, or a line that does not start with the key, gives an
211
+ empty list, which no caller accepts.
212
+ """
213
+ if index >= len(lines):
214
+ return []
215
+ cells = lines[index].split("\t")
216
+ if cells[0] != key:
217
+ return []
218
+ return [cell.strip() for cell in cells[1:] if cell.strip()]
219
+
220
+
221
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
222
+ def _probe(record: dict[str, str | None]) -> Probe:
223
+ """Convert one table record, reporting the first invalid cell."""
224
+ for name in PROBE_COLUMNS:
225
+ if record[name] is None:
226
+ raise ParseError(
227
+ "probe record has too few cells",
228
+ error_func=load_probe_table,
229
+ error_value=name,
230
+ )
231
+ cells: dict[str, Any] = record
232
+ return Probe(
233
+ probe_id=cells["probe_id"].strip(),
234
+ chr_a=cells["chr_a"].strip(),
235
+ pos_a=parse_position(cells["pos_a"]),
236
+ chr_b=cells["chr_b"].strip(),
237
+ pos_b=parse_position(cells["pos_b"]),
238
+ ref=parse_allele(cells["ref"], _PROBE_ALLELE_PATTERN),
239
+ alt=parse_allele(cells["alt"], _PROBE_ALLELE_PATTERN),
240
+ coverage_score=float(parse_number(cells["coverage_score"])),
241
+ )
242
+
243
+
244
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
245
+ def score_assembly(
246
+ species: str,
247
+ builds: tuple[str, str],
248
+ hits: tuple[int, int],
249
+ examined: int,
250
+ *,
251
+ failed_rows: int = 0,
252
+ match_threshold: float = 0.8,
253
+ confidence_delta_threshold: float = 0.1,
254
+ min_examined: int = 5,
255
+ ) -> AssemblyResult:
256
+ """Turn hit counts into per-build scores and a decision.
257
+
258
+ Parameters
259
+ ----------
260
+ species : str
261
+ Species label to report.
262
+ builds : tuple[str, str]
263
+ Build names, A first.
264
+ hits : tuple[int, int]
265
+ Probes matched at the build A position and at the build B position.
266
+ examined : int
267
+ Probes found at their position in either build.
268
+ failed_rows : int
269
+ Fetched rows that failed to parse, reported unchanged.
270
+ match_threshold : float
271
+ Minimum score of the better build.
272
+ confidence_delta_threshold : float
273
+ Minimum difference between the two scores.
274
+ min_examined : int
275
+ Minimum examined probes.
276
+
277
+ Returns
278
+ -------
279
+ AssemblyResult
280
+ Evidence and decision.
281
+
282
+ Notes
283
+ -----
284
+ The decision is ``"unknown"`` when ``examined`` is below its minimum or
285
+ the better score is below its minimum; otherwise ``"ambiguous"`` when the
286
+ score difference is below its minimum; otherwise the better build. A
287
+ value equal to its minimum passes. The difference is the difference of
288
+ the hit counts divided by ``examined``, which is exact where subtracting
289
+ the two rounded scores is not. When the scores are level and the minimum
290
+ difference is zero, build B is returned.
291
+ """
292
+ hits_a, hits_b = hits
293
+ if examined > 0:
294
+ score_a = hits_a / examined
295
+ score_b = hits_b / examined
296
+ difference = abs(hits_a - hits_b) / examined
297
+ else:
298
+ score_a = score_b = difference = 0.0
299
+ if examined < min_examined or max(score_a, score_b) < match_threshold:
300
+ decision = UNKNOWN
301
+ elif difference < confidence_delta_threshold:
302
+ decision = AMBIGUOUS
303
+ elif hits_a > hits_b:
304
+ decision = builds[0]
305
+ else:
306
+ decision = builds[1]
307
+ return AssemblyResult(
308
+ species=species,
309
+ builds=builds,
310
+ decision=decision,
311
+ hits={builds[0]: hits_a, builds[1]: hits_b},
312
+ scores={builds[0]: score_a, builds[1]: score_b},
313
+ examined=examined,
314
+ failed_rows=failed_rows,
315
+ )
316
+
317
+
318
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
319
+ def check_assembly(
320
+ path: str | PathLike[str],
321
+ mapping: dict[str, str],
322
+ *,
323
+ effect_type: str,
324
+ probes: str | PathLike[str] | None = None,
325
+ match_threshold: float = 0.8,
326
+ confidence_delta_threshold: float = 0.1,
327
+ min_examined: int = 5,
328
+ **reader_settings: Any,
329
+ ) -> AssemblyResult:
330
+ """Check an indexed file against the two builds of a probe table.
331
+
332
+ Parameters
333
+ ----------
334
+ path : str or PathLike[str]
335
+ Block-compressed file with a neighbouring tabix index.
336
+ mapping : dict[str, str]
337
+ Vocabulary names mapped to source headers, as for the reader. The
338
+ reader's required quantities apply, and the other allele is always
339
+ required.
340
+ effect_type : str
341
+ Interpretation of the effect column, as for the reader.
342
+ probes : str or PathLike[str] or None
343
+ Probe table for any species; ``None`` uses the bundled human
344
+ GRCh37/GRCh38 table. See :func:`load_probe_table` for the format.
345
+ match_threshold : float
346
+ Minimum score of the better build.
347
+ confidence_delta_threshold : float
348
+ Minimum difference between the two scores.
349
+ min_examined : int
350
+ Minimum probes found at their position in either build.
351
+ **reader_settings : object
352
+ Further reader settings describing the file, such as
353
+ ``logged_pvalue``, ``chrpos_spec`` or ``chromosome_synonyms``, handed
354
+ unchanged to the reader. ``allele_level`` and
355
+ ``require_other_allele`` are fixed by the check and refused.
356
+
357
+ Returns
358
+ -------
359
+ AssemblyResult
360
+ Species, builds, decision, per-build hits and scores, the examined
361
+ count and the number of rows skipped because they failed to parse.
362
+
363
+ Raises
364
+ ------
365
+ ParseError
366
+ If a fixed reader setting is supplied, the probe table is invalid,
367
+ the file has no index, or the reader refuses the configuration.
368
+
369
+ Notes
370
+ -----
371
+ Rows are read with the widest allele alphabet and unusable rows are
372
+ dropped, so one malformed row never prevents a decision; it counts as no
373
+ evidence and is reported in ``failed_rows``. Probe and file chromosomes
374
+ are normalised alike, so ``chr1`` and ``1`` agree. A probe whose
375
+ chromosome the supplied synonyms reject is not looked up. See
376
+ :func:`score_assembly` for the decision rule.
377
+ """
378
+ for setting in FIXED_READER_SETTINGS:
379
+ if setting in reader_settings:
380
+ raise ParseError(
381
+ "this reader setting is fixed by the assembly check",
382
+ error_func=check_assembly,
383
+ error_value=setting,
384
+ )
385
+ if detect_index(path) is None:
386
+ raise ParseError(
387
+ "an index is required for the assembly check",
388
+ error_func=check_assembly,
389
+ error_value=path,
390
+ )
391
+ table = load_probe_table(probes)
392
+ with open_source(path) as source:
393
+ reader = DroppingReader(
394
+ source,
395
+ mapping,
396
+ effect_type=effect_type,
397
+ **FIXED_READER_SETTINGS,
398
+ **reader_settings,
399
+ )
400
+ hits, examined = _count(reader, table.probes)
401
+ failed_rows = len(reader.failures)
402
+ return score_assembly(
403
+ table.species,
404
+ table.builds,
405
+ hits,
406
+ examined,
407
+ failed_rows=failed_rows,
408
+ match_threshold=match_threshold,
409
+ confidence_delta_threshold=confidence_delta_threshold,
410
+ min_examined=min_examined,
411
+ )
412
+
413
+
414
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
415
+ def _count(
416
+ reader: DroppingReader, probes: tuple[Probe, ...]
417
+ ) -> tuple[tuple[int, int], int]:
418
+ """Fetch every probe position once and count hits and examined probes.
419
+
420
+ Returns the build A and build B hit counts, then the examined count.
421
+ """
422
+ # The reader's own normalised synonym table, so a probe chromosome is
423
+ # spelled exactly as the reader spells a row's chromosome.
424
+ synonyms = reader.chromosome_synonyms
425
+ places: list[tuple[Probe, tuple[str, int] | None, tuple[str, int] | None]]
426
+ places = []
427
+ regions: list[Region] = []
428
+ for probe in probes:
429
+ keys: list[tuple[str, int] | None] = []
430
+ for chromosome, position in (
431
+ (probe.chr_a, probe.pos_a),
432
+ (probe.chr_b, probe.pos_b),
433
+ ):
434
+ try:
435
+ name = parse_chromosome(chromosome, synonyms)
436
+ except ParseError:
437
+ keys.append(None)
438
+ continue
439
+ keys.append((name, position))
440
+ regions.append((chromosome, position, position))
441
+ places.append((probe, keys[0], keys[1]))
442
+ found: set[tuple[str, int]] = set()
443
+ matched: set[tuple[str, int, frozenset[str]]] = set()
444
+ if regions: # A query without regions would scan the whole file.
445
+ chromosome_column = reader.column_indices["chr_name"]
446
+ position_column = reader.column_indices["start_pos"]
447
+ effect_column = reader.column_indices["effect_allele"]
448
+ other_column = reader.column_indices["other_allele"]
449
+ for row in reader.fetch(*regions):
450
+ place = (row[chromosome_column], row[position_column])
451
+ found.add(place) # type: ignore[arg-type]
452
+ alleles = frozenset((row[effect_column], row[other_column]))
453
+ matched.add((*place, alleles)) # type: ignore[arg-type]
454
+ hits_a = hits_b = examined = 0
455
+ for probe, key_a, key_b in places:
456
+ alleles = frozenset((probe.ref, probe.alt))
457
+ if key_a in found or key_b in found:
458
+ examined += 1
459
+ if key_a is not None and (*key_a, alleles) in matched:
460
+ hits_a += 1
461
+ if key_b is not None and (*key_b, alleles) in matched:
462
+ hits_b += 1
463
+ return (hits_a, hits_b), examined
@@ -0,0 +1,139 @@
1
+ """Reference-calibrated scalar effects and standalone calibration estimation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from math import sqrt
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+
11
+ from .errors import ParseError
12
+ from .resolver import Route
13
+ from .stats import _number, set_effect_size_pvalue
14
+
15
+
16
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
+ def average_variation(
18
+ standard_errors: Sequence[float], frequencies: Sequence[float]
19
+ ) -> float:
20
+ """Estimate calibration from supplied reference-study numeric sequences.
21
+
22
+ Parameters
23
+ ----------
24
+ standard_errors, frequencies : Sequence[float]
25
+ Paired reference errors and allele frequencies from the same study
26
+ and effect scale as the target variants.
27
+
28
+ Returns
29
+ -------
30
+ float
31
+ Mean finite SE*sqrt(f*(1-f)) term, or NaN when none survive.
32
+ This calibration incorporates study precision; it is not the
33
+ phenotype standard deviation. Comparable reference and target
34
+ precision, including sufficiently similar sample sizes, is assumed.
35
+ """
36
+ errors = np.asarray(standard_errors, dtype=float)
37
+ frequency = np.asarray(frequencies, dtype=float)
38
+ with np.errstate(invalid="ignore", over="ignore"):
39
+ terms = errors * np.sqrt(frequency * (1 - frequency))
40
+ finite = terms[np.isfinite(terms)]
41
+ return float(np.mean(finite)) if finite.size else float("nan")
42
+
43
+
44
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
45
+ def set_standard_error_burgess(frequency: Any, average_variation: Any) -> float:
46
+ """Estimate error from reference calibration on the target effect scale.
47
+
48
+ Parameters
49
+ ----------
50
+ frequency, average_variation : object
51
+ Frequency strictly between zero and one, and positive finite
52
+ calibration from the same study and effect scale. Calibration
53
+ incorporates study precision, assuming comparable reference and
54
+ target precision including sufficiently similar sample sizes.
55
+
56
+ Returns
57
+ -------
58
+ float
59
+ Calibration divided by sqrt(f*(1-f)); no sample count is used.
60
+ """
61
+ method = set_standard_error_burgess
62
+ f = _number(frequency, method)
63
+ calibration = _number(average_variation, method)
64
+ if not 0 < f < 1:
65
+ raise ParseError(
66
+ "Burgess frequency must be strictly between zero and one",
67
+ error_func=method,
68
+ error_value=frequency,
69
+ )
70
+ if calibration <= 0:
71
+ raise ParseError(
72
+ "average_variation must be positive",
73
+ error_func=method,
74
+ error_value=average_variation,
75
+ )
76
+ return _number(calibration / sqrt(f * (1 - f)), method)
77
+
78
+
79
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
80
+ def set_effect_size_burgess(
81
+ logarithm: Any, frequency: Any, average_variation: Any, direction: Any
82
+ ) -> float:
83
+ """Estimate a signed linear or log-odds effect using reference precision.
84
+
85
+ Parameters
86
+ ----------
87
+ logarithm, frequency, average_variation, direction : object
88
+ Negative base-ten probability log, allele frequency, same-study
89
+ calibration on the target effect scale, and explicit sign (+1/-1).
90
+ Calibration already incorporates study precision; comparable
91
+ reference and target precision is assumed.
92
+
93
+ Returns
94
+ -------
95
+ float
96
+ Signed normal score times the calibrated error. The reference scale
97
+ determines linear units or log odds; no sample count is used.
98
+ """
99
+ try:
100
+ error = set_standard_error_burgess(frequency, average_variation)
101
+ return set_effect_size_pvalue(error, logarithm, direction)
102
+ except ParseError as failure:
103
+ failure.error_func = set_effect_size_burgess
104
+ raise
105
+
106
+
107
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
108
+ def burgess_routes(effect_type: str) -> tuple[Route, ...]:
109
+ """Supply fallback routes for explicitly declared effect directions.
110
+
111
+ Parameters
112
+ ----------
113
+ effect_type : str
114
+ Source interpretation, restricted to linear or log-odds direction.
115
+
116
+ Returns
117
+ -------
118
+ tuple[Route, ...]
119
+ Reference-calibrated routes to append after ordinary alternatives.
120
+ """
121
+ if effect_type not in {"direction_beta", "direction_log_or"}:
122
+ return ()
123
+ return (
124
+ Route(
125
+ "standard_error",
126
+ ("effect_allele_freq", "average_variation"),
127
+ set_standard_error_burgess,
128
+ ),
129
+ Route(
130
+ "effect_size",
131
+ (
132
+ "mlog10_pvalue",
133
+ "effect_allele_freq",
134
+ "average_variation",
135
+ "effect_direction",
136
+ ),
137
+ set_effect_size_burgess,
138
+ ),
139
+ )