microarray 0.1.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.
- microarray/__init__.py +15 -0
- microarray/_version.py +3 -0
- microarray/datasets/__init__.py +3 -0
- microarray/datasets/_arrayexpress.py +1 -0
- microarray/datasets/_cdf_files.py +35 -0
- microarray/datasets/_geo.py +1 -0
- microarray/datasets/_utils.py +143 -0
- microarray/io/__init__.py +17 -0
- microarray/io/_anndata_converter.py +198 -0
- microarray/io/_cdf.py +575 -0
- microarray/io/_cel.py +591 -0
- microarray/io/_read.py +127 -0
- microarray/plotting/__init__.py +28 -0
- microarray/plotting/_base.py +253 -0
- microarray/plotting/_cel.py +75 -0
- microarray/plotting/_de_plots.py +239 -0
- microarray/plotting/_diagnostic_plots.py +268 -0
- microarray/plotting/_heatmap.py +279 -0
- microarray/plotting/_ma_plots.py +136 -0
- microarray/plotting/_pca.py +320 -0
- microarray/plotting/_qc_plots.py +335 -0
- microarray/plotting/_score.py +38 -0
- microarray/plotting/_top_table_heatmap.py +98 -0
- microarray/plotting/_utils.py +280 -0
- microarray/preprocessing/__init__.py +39 -0
- microarray/preprocessing/_background.py +862 -0
- microarray/preprocessing/_log2.py +77 -0
- microarray/preprocessing/_normalize.py +1292 -0
- microarray/preprocessing/_rma.py +243 -0
- microarray/preprocessing/_robust.py +170 -0
- microarray/preprocessing/_summarize.py +318 -0
- microarray/py.typed +0 -0
- microarray/tools/__init__.py +26 -0
- microarray/tools/_biomart.py +416 -0
- microarray/tools/_empirical_bayes.py +401 -0
- microarray/tools/_fdist.py +171 -0
- microarray/tools/_linear_models.py +387 -0
- microarray/tools/_mds.py +101 -0
- microarray/tools/_pca.py +88 -0
- microarray/tools/_score.py +86 -0
- microarray/tools/_toptable.py +360 -0
- microarray-0.1.0.dist-info/METADATA +75 -0
- microarray-0.1.0.dist-info/RECORD +44 -0
- microarray-0.1.0.dist-info/WHEEL +4 -0
microarray/io/_cdf.py
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
import re
|
|
3
|
+
import warnings
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
# Complement base mapping used for PM/MM probe type determination
|
|
10
|
+
_BASE_COMPLEMENTS: dict[str, str] = {"A": "T", "T": "A", "G": "C", "C": "G"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CdfCell:
|
|
15
|
+
"""Represents a single cell (probe) on the microarray."""
|
|
16
|
+
|
|
17
|
+
x: int
|
|
18
|
+
y: int
|
|
19
|
+
probe: str
|
|
20
|
+
plen: int | None = None
|
|
21
|
+
atom: int | None = None
|
|
22
|
+
index: int | None = None
|
|
23
|
+
match: int | None = None
|
|
24
|
+
bg: int | None = None
|
|
25
|
+
# Additional fields for regular units
|
|
26
|
+
feat: str | None = None
|
|
27
|
+
qual: str | None = None
|
|
28
|
+
expos: int | None = None
|
|
29
|
+
pos: int | None = None
|
|
30
|
+
cbase: str | None = None
|
|
31
|
+
pbase: str | None = None
|
|
32
|
+
tbase: str | None = None
|
|
33
|
+
codonind: int | None = None
|
|
34
|
+
codon: int | None = None
|
|
35
|
+
regiontype: int | None = None
|
|
36
|
+
region: str | None = None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_pm(self) -> bool | None:
|
|
40
|
+
"""Return True if this is a Perfect Match (PM) probe, False for Mismatch (MM).
|
|
41
|
+
|
|
42
|
+
For regular unit cells, the probe type is determined by comparing the probe
|
|
43
|
+
base (``pbase``) to the complement of the context base (``cbase``). A probe
|
|
44
|
+
is PM when ``pbase`` is the Watson-Crick complement of ``cbase`` (A↔T, G↔C).
|
|
45
|
+
|
|
46
|
+
For QC cells the ``match`` field is used directly (1 = PM, 0 = MM).
|
|
47
|
+
|
|
48
|
+
Returns ``None`` when neither source of information is available.
|
|
49
|
+
"""
|
|
50
|
+
if self.pbase is not None and self.cbase is not None:
|
|
51
|
+
complement = _BASE_COMPLEMENTS.get(self.cbase.upper())
|
|
52
|
+
if complement is not None:
|
|
53
|
+
return self.pbase.upper() == complement
|
|
54
|
+
# Fall back to inequality check when base is non-standard
|
|
55
|
+
return self.pbase.upper() != self.cbase.upper()
|
|
56
|
+
if self.match is not None:
|
|
57
|
+
return self.match == 1
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class CdfBlock:
|
|
63
|
+
"""Represents a block within a unit (probeset)."""
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
block_number: int
|
|
67
|
+
num_atoms: int
|
|
68
|
+
num_cells: int
|
|
69
|
+
start_position: int
|
|
70
|
+
stop_position: int
|
|
71
|
+
cells: list[CdfCell] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def pm_cells(self) -> list[CdfCell]:
|
|
75
|
+
"""Return all Perfect Match (PM) probe cells in this block."""
|
|
76
|
+
return [c for c in self.cells if c.is_pm is True]
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def mm_cells(self) -> list[CdfCell]:
|
|
80
|
+
"""Return all Mismatch (MM) probe cells in this block."""
|
|
81
|
+
return [c for c in self.cells if c.is_pm is False]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class CdfUnit:
|
|
86
|
+
"""Represents a unit (probeset) in the CDF file."""
|
|
87
|
+
|
|
88
|
+
name: str
|
|
89
|
+
unit_number: int
|
|
90
|
+
direction: int | None = None
|
|
91
|
+
num_atoms: int = 0
|
|
92
|
+
num_cells: int = 0
|
|
93
|
+
unit_type: int | None = None
|
|
94
|
+
number_blocks: int = 0
|
|
95
|
+
blocks: list[CdfBlock] = field(default_factory=list)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class CdfQCUnit:
|
|
100
|
+
"""Represents a quality control unit."""
|
|
101
|
+
|
|
102
|
+
qc_number: int
|
|
103
|
+
unit_type: int
|
|
104
|
+
number_cells: int
|
|
105
|
+
cells: list[CdfCell] = field(default_factory=list)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class CdfChipInfo:
|
|
110
|
+
"""Chip metadata from the CDF file."""
|
|
111
|
+
|
|
112
|
+
name: str
|
|
113
|
+
rows: int
|
|
114
|
+
cols: int
|
|
115
|
+
number_of_units: int
|
|
116
|
+
max_unit: int
|
|
117
|
+
num_qc_units: int
|
|
118
|
+
chip_reference: str = ""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class CdfFile:
|
|
122
|
+
"""Parser and container for CDF (Chip Definition File) data."""
|
|
123
|
+
|
|
124
|
+
def __init__(self):
|
|
125
|
+
self.version: str = ""
|
|
126
|
+
self.chip_info: CdfChipInfo | None = None
|
|
127
|
+
self.qc_units: list[CdfQCUnit] = []
|
|
128
|
+
self.units: list[CdfUnit] = []
|
|
129
|
+
self.units_by_name: dict[str, CdfUnit] = {}
|
|
130
|
+
self.probeset_info: pd.DataFrame | None = None
|
|
131
|
+
self.recognized_suffixes: set[str] = set()
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def read(cls, filepath: str) -> "CdfFile":
|
|
135
|
+
"""Read and parse a CDF file (plain text or gzipped).
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
filepath : str
|
|
140
|
+
Path to the CDF file (.cdf or .cdf.gz)
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
-------
|
|
144
|
+
CdfFile
|
|
145
|
+
Parsed CDF file object
|
|
146
|
+
"""
|
|
147
|
+
cdf = cls()
|
|
148
|
+
|
|
149
|
+
# Open file (handle gzipped files)
|
|
150
|
+
if filepath.endswith(".gz"):
|
|
151
|
+
with gzip.open(filepath, "rt") as f:
|
|
152
|
+
content = f.read()
|
|
153
|
+
else:
|
|
154
|
+
with open(filepath) as f:
|
|
155
|
+
content = f.read()
|
|
156
|
+
|
|
157
|
+
cdf._parse(content)
|
|
158
|
+
cdf._create_probeset_info()
|
|
159
|
+
return cdf
|
|
160
|
+
|
|
161
|
+
def _parse(self, content: str):
|
|
162
|
+
"""Parse the CDF file content."""
|
|
163
|
+
lines = content.split("\n")
|
|
164
|
+
i = 0
|
|
165
|
+
|
|
166
|
+
while i < len(lines):
|
|
167
|
+
line = lines[i].strip()
|
|
168
|
+
|
|
169
|
+
# Parse CDF section
|
|
170
|
+
if line == "[CDF]":
|
|
171
|
+
i = self._parse_cdf_section(lines, i + 1)
|
|
172
|
+
|
|
173
|
+
# Parse Chip section
|
|
174
|
+
elif line == "[Chip]":
|
|
175
|
+
i = self._parse_chip_section(lines, i + 1)
|
|
176
|
+
|
|
177
|
+
# Parse QC units
|
|
178
|
+
elif line.startswith("[QC"):
|
|
179
|
+
i = self._parse_qc_unit(lines, i)
|
|
180
|
+
|
|
181
|
+
# Parse regular units
|
|
182
|
+
elif line.startswith("[Unit") and "_Block" not in line:
|
|
183
|
+
i = self._parse_unit(lines, i)
|
|
184
|
+
|
|
185
|
+
else:
|
|
186
|
+
i += 1
|
|
187
|
+
|
|
188
|
+
def _create_probeset_info(self):
|
|
189
|
+
"""Create a DataFrame with probeset information including parsed suffixes.
|
|
190
|
+
|
|
191
|
+
Extracts gene annotation (prefix) and probe type suffix from probe set names.
|
|
192
|
+
Common Affymetrix suffixes include:
|
|
193
|
+
- '_at': standard probe set
|
|
194
|
+
- '_s_at': consensus/similar sequences
|
|
195
|
+
- '_x_at': cross-hybridizing sequences
|
|
196
|
+
- '_a_at': alternative/ambiguous sequences
|
|
197
|
+
- '_g_at': gene-level probe set
|
|
198
|
+
- '_i_at': intronic probe set
|
|
199
|
+
|
|
200
|
+
Warnings:
|
|
201
|
+
--------
|
|
202
|
+
Issues warnings if probes with unrecognized suffixes are detected.
|
|
203
|
+
"""
|
|
204
|
+
probeset_data = []
|
|
205
|
+
recognized_count = 0
|
|
206
|
+
unrecognized_probes = []
|
|
207
|
+
|
|
208
|
+
# Known Affymetrix probe set suffixes (ordered by specificity)
|
|
209
|
+
# More specific patterns first (e.g., _s_at before _at)
|
|
210
|
+
suffix_patterns = [
|
|
211
|
+
r"(_s_at)$", # similar/consensus
|
|
212
|
+
r"(_x_at)$", # cross-hybridizing
|
|
213
|
+
r"(_a_at)$", # alternative/ambiguous
|
|
214
|
+
r"(_g_at)$", # gene-level
|
|
215
|
+
r"(_i_at)$", # intronic
|
|
216
|
+
r"(_st)$", # sense target (newer arrays)
|
|
217
|
+
r"(_at)$", # standard (check last)
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
for probeset_name in self.units_by_name.keys():
|
|
221
|
+
if probeset_name == "NONE":
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
gene_id = probeset_name
|
|
225
|
+
suffix = ""
|
|
226
|
+
|
|
227
|
+
# Try to match known suffixes
|
|
228
|
+
for pattern in suffix_patterns:
|
|
229
|
+
match = re.search(pattern, probeset_name)
|
|
230
|
+
if match:
|
|
231
|
+
suffix = match.group(1)
|
|
232
|
+
gene_id = probeset_name[: match.start()]
|
|
233
|
+
recognized_count += 1
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
# Track unrecognized probes (those without a suffix)
|
|
237
|
+
if not suffix:
|
|
238
|
+
unrecognized_probes.append(probeset_name)
|
|
239
|
+
|
|
240
|
+
probeset_data.append({"probe_id": probeset_name, "gene_id": gene_id, "suffix": suffix})
|
|
241
|
+
|
|
242
|
+
self.probeset_info = pd.DataFrame(probeset_data)
|
|
243
|
+
|
|
244
|
+
# Set probe_id as index for fast lookup
|
|
245
|
+
self.probeset_info = self.probeset_info.set_index("probe_id", drop=False)
|
|
246
|
+
|
|
247
|
+
# Store the set of recognized suffixes that were actually found
|
|
248
|
+
self.recognized_suffixes = set(self.probeset_info[self.probeset_info["suffix"] != ""]["suffix"].unique())
|
|
249
|
+
|
|
250
|
+
# Warn if there are unrecognized suffixes
|
|
251
|
+
total_probes = len(probeset_data)
|
|
252
|
+
unrecognized_count = len(unrecognized_probes)
|
|
253
|
+
|
|
254
|
+
if unrecognized_count > 0:
|
|
255
|
+
percentage = (unrecognized_count / total_probes) * 100
|
|
256
|
+
warnings.warn(
|
|
257
|
+
f"Found {unrecognized_count} probe(s) ({percentage:.1f}%) with unrecognized suffixes. "
|
|
258
|
+
f"Examples: {unrecognized_probes[:5]}. "
|
|
259
|
+
f"Recognized suffixes in this CDF: {sorted(self.recognized_suffixes)}",
|
|
260
|
+
category=UserWarning,
|
|
261
|
+
stacklevel=3,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def _parse_cdf_section(self, lines: list[str], start_idx: int) -> int:
|
|
265
|
+
"""Parse the [CDF] section."""
|
|
266
|
+
i = start_idx
|
|
267
|
+
while i < len(lines) and not lines[i].startswith("["):
|
|
268
|
+
line = lines[i].strip()
|
|
269
|
+
if line.startswith("Version="):
|
|
270
|
+
self.version = line.split("=", 1)[1]
|
|
271
|
+
i += 1
|
|
272
|
+
return i
|
|
273
|
+
|
|
274
|
+
def _parse_chip_section(self, lines: list[str], start_idx: int) -> int:
|
|
275
|
+
"""Parse the [Chip] section."""
|
|
276
|
+
i = start_idx
|
|
277
|
+
chip_data = {}
|
|
278
|
+
|
|
279
|
+
while i < len(lines) and not lines[i].startswith("["):
|
|
280
|
+
line = lines[i].strip()
|
|
281
|
+
if "=" in line:
|
|
282
|
+
key, value = line.split("=", 1)
|
|
283
|
+
chip_data[key] = value
|
|
284
|
+
i += 1
|
|
285
|
+
|
|
286
|
+
self.chip_info = CdfChipInfo(
|
|
287
|
+
name=chip_data.get("Name", ""),
|
|
288
|
+
rows=int(chip_data.get("Rows", 0)),
|
|
289
|
+
cols=int(chip_data.get("Cols", 0)),
|
|
290
|
+
number_of_units=int(chip_data.get("NumberOfUnits", 0)),
|
|
291
|
+
max_unit=int(chip_data.get("MaxUnit", 0)),
|
|
292
|
+
num_qc_units=int(chip_data.get("NumQCUnits", 0)),
|
|
293
|
+
chip_reference=chip_data.get("ChipReference", ""),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return i
|
|
297
|
+
|
|
298
|
+
def _parse_qc_unit(self, lines: list[str], start_idx: int) -> int:
|
|
299
|
+
"""Parse a QC unit section."""
|
|
300
|
+
i = start_idx
|
|
301
|
+
line = lines[i].strip()
|
|
302
|
+
|
|
303
|
+
# Extract QC number from [QC1], [QC2], etc.
|
|
304
|
+
qc_num = int(line[3:-1])
|
|
305
|
+
i += 1
|
|
306
|
+
|
|
307
|
+
qc_data = {}
|
|
308
|
+
while i < len(lines) and not lines[i].startswith("["):
|
|
309
|
+
line = lines[i].strip()
|
|
310
|
+
if "=" in line:
|
|
311
|
+
key, value = line.split("=", 1)
|
|
312
|
+
qc_data[key] = value
|
|
313
|
+
i += 1
|
|
314
|
+
|
|
315
|
+
qc_unit = CdfQCUnit(
|
|
316
|
+
qc_number=qc_num, unit_type=int(qc_data.get("Type", 0)), number_cells=int(qc_data.get("NumberCells", 0))
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Parse cells
|
|
320
|
+
cell_header = qc_data.get("CellHeader", "").split()
|
|
321
|
+
for key, value in qc_data.items():
|
|
322
|
+
if key.startswith("Cell") and key[4:].isdigit():
|
|
323
|
+
qc_unit.cells.append(self._parse_cell(value, cell_header, is_qc=True))
|
|
324
|
+
|
|
325
|
+
self.qc_units.append(qc_unit)
|
|
326
|
+
return i
|
|
327
|
+
|
|
328
|
+
def _parse_unit(self, lines: list[str], start_idx: int) -> int:
|
|
329
|
+
"""Parse a regular unit section and its blocks."""
|
|
330
|
+
i = start_idx
|
|
331
|
+
line = lines[i].strip()
|
|
332
|
+
|
|
333
|
+
# Extract unit number from [Unit1], [Unit2], etc.
|
|
334
|
+
unit_num_str = line[5:-1] # Remove [Unit and ]
|
|
335
|
+
unit_num = int(unit_num_str)
|
|
336
|
+
i += 1
|
|
337
|
+
|
|
338
|
+
# Parse unit properties
|
|
339
|
+
unit_data = {}
|
|
340
|
+
while i < len(lines) and not lines[i].startswith("["):
|
|
341
|
+
line = lines[i].strip()
|
|
342
|
+
if "=" in line:
|
|
343
|
+
key, value = line.split("=", 1)
|
|
344
|
+
unit_data[key] = value
|
|
345
|
+
i += 1
|
|
346
|
+
|
|
347
|
+
unit = CdfUnit(
|
|
348
|
+
name=unit_data.get("Name", "NONE"),
|
|
349
|
+
unit_number=unit_num,
|
|
350
|
+
direction=int(unit_data["Direction"]) if "Direction" in unit_data else None,
|
|
351
|
+
num_atoms=int(unit_data.get("NumAtoms", 0)),
|
|
352
|
+
num_cells=int(unit_data.get("NumCells", 0)),
|
|
353
|
+
unit_type=int(unit_data["UnitType"]) if "UnitType" in unit_data else None,
|
|
354
|
+
number_blocks=int(unit_data.get("NumberBlocks", 0)),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# Parse blocks for this unit
|
|
358
|
+
while i < len(lines) and lines[i].startswith(f"[Unit{unit_num}_Block"):
|
|
359
|
+
i = self._parse_block(lines, i, unit)
|
|
360
|
+
|
|
361
|
+
self.units.append(unit)
|
|
362
|
+
|
|
363
|
+
# Index by block name (probeset name)
|
|
364
|
+
for block in unit.blocks:
|
|
365
|
+
if block.name != "NONE":
|
|
366
|
+
self.units_by_name[block.name] = unit
|
|
367
|
+
|
|
368
|
+
return i
|
|
369
|
+
|
|
370
|
+
def _parse_block(self, lines: list[str], start_idx: int, unit: CdfUnit) -> int:
|
|
371
|
+
"""Parse a unit block section."""
|
|
372
|
+
i = start_idx + 1
|
|
373
|
+
|
|
374
|
+
block_data = {}
|
|
375
|
+
while i < len(lines) and not lines[i].startswith("["):
|
|
376
|
+
line = lines[i].strip()
|
|
377
|
+
if "=" in line:
|
|
378
|
+
key, value = line.split("=", 1)
|
|
379
|
+
block_data[key] = value
|
|
380
|
+
i += 1
|
|
381
|
+
|
|
382
|
+
block = CdfBlock(
|
|
383
|
+
name=block_data.get("Name", ""),
|
|
384
|
+
block_number=int(block_data.get("BlockNumber", 0)),
|
|
385
|
+
num_atoms=int(block_data.get("NumAtoms", 0)),
|
|
386
|
+
num_cells=int(block_data.get("NumCells", 0)),
|
|
387
|
+
start_position=int(block_data.get("StartPosition", 0)),
|
|
388
|
+
stop_position=int(block_data.get("StopPosition", 0)),
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Parse cells
|
|
392
|
+
cell_header = block_data.get("CellHeader", "").split()
|
|
393
|
+
for key, value in block_data.items():
|
|
394
|
+
if key.startswith("Cell") and key[4:].isdigit():
|
|
395
|
+
block.cells.append(self._parse_cell(value, cell_header, is_qc=False))
|
|
396
|
+
|
|
397
|
+
unit.blocks.append(block)
|
|
398
|
+
return i
|
|
399
|
+
|
|
400
|
+
def _parse_cell(self, cell_line: str, header: list[str], is_qc: bool) -> CdfCell:
|
|
401
|
+
"""Parse a cell line into a CdfCell object."""
|
|
402
|
+
parts = cell_line.split("\t")
|
|
403
|
+
if len(parts) < len(header):
|
|
404
|
+
parts = cell_line.split()
|
|
405
|
+
|
|
406
|
+
cell_dict = {header[i].lower(): parts[i] if i < len(parts) else None for i in range(len(header))}
|
|
407
|
+
|
|
408
|
+
# Create cell with common fields
|
|
409
|
+
cell = CdfCell(x=int(cell_dict.get("x", 0)), y=int(cell_dict.get("y", 0)), probe=cell_dict.get("probe", "N"))
|
|
410
|
+
|
|
411
|
+
# Add QC-specific fields
|
|
412
|
+
if is_qc:
|
|
413
|
+
if "plen" in cell_dict and cell_dict["plen"]:
|
|
414
|
+
cell.plen = int(cell_dict["plen"])
|
|
415
|
+
if "atom" in cell_dict and cell_dict["atom"]:
|
|
416
|
+
cell.atom = int(cell_dict["atom"])
|
|
417
|
+
if "index" in cell_dict and cell_dict["index"]:
|
|
418
|
+
cell.index = int(cell_dict["index"])
|
|
419
|
+
if "match" in cell_dict and cell_dict["match"]:
|
|
420
|
+
cell.match = int(cell_dict["match"])
|
|
421
|
+
if "bg" in cell_dict and cell_dict["bg"]:
|
|
422
|
+
cell.bg = int(cell_dict["bg"])
|
|
423
|
+
|
|
424
|
+
# Add regular unit fields
|
|
425
|
+
else:
|
|
426
|
+
if "feat" in cell_dict and cell_dict["feat"]:
|
|
427
|
+
cell.feat = cell_dict["feat"]
|
|
428
|
+
if "qual" in cell_dict and cell_dict["qual"]:
|
|
429
|
+
cell.qual = cell_dict["qual"]
|
|
430
|
+
if "expos" in cell_dict and cell_dict["expos"]:
|
|
431
|
+
cell.expos = int(cell_dict["expos"])
|
|
432
|
+
if "pos" in cell_dict and cell_dict["pos"]:
|
|
433
|
+
cell.pos = int(cell_dict["pos"])
|
|
434
|
+
if "cbase" in cell_dict and cell_dict["cbase"]:
|
|
435
|
+
cell.cbase = cell_dict["cbase"]
|
|
436
|
+
if "pbase" in cell_dict and cell_dict["pbase"]:
|
|
437
|
+
cell.pbase = cell_dict["pbase"]
|
|
438
|
+
if "tbase" in cell_dict and cell_dict["tbase"]:
|
|
439
|
+
cell.tbase = cell_dict["tbase"]
|
|
440
|
+
if "atom" in cell_dict and cell_dict["atom"]:
|
|
441
|
+
cell.atom = int(cell_dict["atom"])
|
|
442
|
+
if "index" in cell_dict and cell_dict["index"]:
|
|
443
|
+
cell.index = int(cell_dict["index"])
|
|
444
|
+
if "codonind" in cell_dict and cell_dict["codonind"]:
|
|
445
|
+
cell.codonind = int(cell_dict["codonind"])
|
|
446
|
+
if "codon" in cell_dict and cell_dict["codon"]:
|
|
447
|
+
cell.codon = int(cell_dict["codon"])
|
|
448
|
+
if "regiontype" in cell_dict and cell_dict["regiontype"]:
|
|
449
|
+
cell.regiontype = int(cell_dict["regiontype"])
|
|
450
|
+
if "region" in cell_dict and cell_dict["region"]:
|
|
451
|
+
cell.region = cell_dict["region"]
|
|
452
|
+
|
|
453
|
+
return cell
|
|
454
|
+
|
|
455
|
+
def get_probeset_indices(self, probeset_name: str) -> list[tuple]:
|
|
456
|
+
"""Get all (X, Y) positions for a given probeset name.
|
|
457
|
+
|
|
458
|
+
Parameters
|
|
459
|
+
----------
|
|
460
|
+
probeset_name : str
|
|
461
|
+
Name of the probeset (e.g., 'AFFX-BioB-5_at')
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
-------
|
|
465
|
+
List[tuple]
|
|
466
|
+
List of (x, y) coordinate tuples
|
|
467
|
+
"""
|
|
468
|
+
if probeset_name in self.units_by_name:
|
|
469
|
+
unit = self.units_by_name[probeset_name]
|
|
470
|
+
positions = []
|
|
471
|
+
for block in unit.blocks:
|
|
472
|
+
for cell in block.cells:
|
|
473
|
+
positions.append((cell.x, cell.y))
|
|
474
|
+
return positions
|
|
475
|
+
return []
|
|
476
|
+
|
|
477
|
+
def get_pm_indices(self, probeset_name: str) -> list[tuple]:
|
|
478
|
+
"""Get (X, Y) positions of all Perfect Match (PM) probes for a probeset.
|
|
479
|
+
|
|
480
|
+
Parameters
|
|
481
|
+
----------
|
|
482
|
+
probeset_name : str
|
|
483
|
+
Name of the probeset (e.g., 'AFFX-BioB-5_at')
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
-------
|
|
487
|
+
list[tuple]
|
|
488
|
+
List of (x, y) coordinate tuples for PM probes.
|
|
489
|
+
"""
|
|
490
|
+
if probeset_name not in self.units_by_name:
|
|
491
|
+
return []
|
|
492
|
+
unit = self.units_by_name[probeset_name]
|
|
493
|
+
return [(c.x, c.y) for block in unit.blocks for c in block.pm_cells]
|
|
494
|
+
|
|
495
|
+
def get_mm_indices(self, probeset_name: str) -> list[tuple]:
|
|
496
|
+
"""Get (X, Y) positions of all Mismatch (MM) probes for a probeset.
|
|
497
|
+
|
|
498
|
+
Parameters
|
|
499
|
+
----------
|
|
500
|
+
probeset_name : str
|
|
501
|
+
Name of the probeset (e.g., 'AFFX-BioB-5_at')
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
-------
|
|
505
|
+
list[tuple]
|
|
506
|
+
List of (x, y) coordinate tuples for MM probes.
|
|
507
|
+
"""
|
|
508
|
+
if probeset_name not in self.units_by_name:
|
|
509
|
+
return []
|
|
510
|
+
unit = self.units_by_name[probeset_name]
|
|
511
|
+
return [(c.x, c.y) for block in unit.blocks for c in block.mm_cells]
|
|
512
|
+
|
|
513
|
+
def get_pm_mm_map(self) -> dict[tuple, str]:
|
|
514
|
+
"""Create a mapping from (X, Y) positions to probe type ('pm' or 'mm').
|
|
515
|
+
|
|
516
|
+
Only positions where the probe type can be determined are included.
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
-------
|
|
520
|
+
dict[tuple, str]
|
|
521
|
+
Dictionary mapping (x, y) tuples to ``'pm'`` or ``'mm'``.
|
|
522
|
+
"""
|
|
523
|
+
pm_mm: dict[tuple, str] = {}
|
|
524
|
+
for unit in self.units:
|
|
525
|
+
for block in unit.blocks:
|
|
526
|
+
for cell in block.cells:
|
|
527
|
+
if cell.is_pm is True:
|
|
528
|
+
pm_mm[(cell.x, cell.y)] = "pm"
|
|
529
|
+
elif cell.is_pm is False:
|
|
530
|
+
pm_mm[(cell.x, cell.y)] = "mm"
|
|
531
|
+
return pm_mm
|
|
532
|
+
|
|
533
|
+
def get_xy_to_probeset_map(self) -> dict[tuple, str]:
|
|
534
|
+
"""Create a mapping from (X, Y) positions to probeset names.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
-------
|
|
538
|
+
Dict[tuple, str]
|
|
539
|
+
Dictionary mapping (x, y) tuples to probeset names
|
|
540
|
+
"""
|
|
541
|
+
xy_map = {}
|
|
542
|
+
for unit in self.units:
|
|
543
|
+
for block in unit.blocks:
|
|
544
|
+
if block.name != "NONE":
|
|
545
|
+
for cell in block.cells:
|
|
546
|
+
xy_map[(cell.x, cell.y)] = block.name
|
|
547
|
+
return xy_map
|
|
548
|
+
|
|
549
|
+
def __repr__(self):
|
|
550
|
+
return (
|
|
551
|
+
f"CdfFile(version='{self.version}', "
|
|
552
|
+
f"chip='{self.chip_info.name if self.chip_info else 'N/A'}', "
|
|
553
|
+
f"units={len(self.units)}, qc_units={len(self.qc_units)})"
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
def get_annotated_array(self) -> np.ndarray:
|
|
557
|
+
"""Generate numpy array in size of the microarray plate containing annotations."""
|
|
558
|
+
array = np.full((self.chip_info.rows, self.chip_info.cols), fill_value=None, dtype=object)
|
|
559
|
+
probes = self.get_xy_to_probeset_map()
|
|
560
|
+
|
|
561
|
+
for (x, y), probeset in probes.items():
|
|
562
|
+
if array[y, x] is not None:
|
|
563
|
+
warnings.warn(
|
|
564
|
+
f"Warning: Overwriting existing probeset at ({x}, {y})",
|
|
565
|
+
category=UserWarning,
|
|
566
|
+
stacklevel=2,
|
|
567
|
+
)
|
|
568
|
+
if 0 <= x < self.chip_info.cols and 0 <= y < self.chip_info.rows:
|
|
569
|
+
array[y, x] = probeset
|
|
570
|
+
return array
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def parse_cdf(filepath: str) -> CdfFile:
|
|
574
|
+
"""Convenience function to parse a CDF file."""
|
|
575
|
+
return CdfFile.read(filepath)
|