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.
Files changed (44) hide show
  1. microarray/__init__.py +15 -0
  2. microarray/_version.py +3 -0
  3. microarray/datasets/__init__.py +3 -0
  4. microarray/datasets/_arrayexpress.py +1 -0
  5. microarray/datasets/_cdf_files.py +35 -0
  6. microarray/datasets/_geo.py +1 -0
  7. microarray/datasets/_utils.py +143 -0
  8. microarray/io/__init__.py +17 -0
  9. microarray/io/_anndata_converter.py +198 -0
  10. microarray/io/_cdf.py +575 -0
  11. microarray/io/_cel.py +591 -0
  12. microarray/io/_read.py +127 -0
  13. microarray/plotting/__init__.py +28 -0
  14. microarray/plotting/_base.py +253 -0
  15. microarray/plotting/_cel.py +75 -0
  16. microarray/plotting/_de_plots.py +239 -0
  17. microarray/plotting/_diagnostic_plots.py +268 -0
  18. microarray/plotting/_heatmap.py +279 -0
  19. microarray/plotting/_ma_plots.py +136 -0
  20. microarray/plotting/_pca.py +320 -0
  21. microarray/plotting/_qc_plots.py +335 -0
  22. microarray/plotting/_score.py +38 -0
  23. microarray/plotting/_top_table_heatmap.py +98 -0
  24. microarray/plotting/_utils.py +280 -0
  25. microarray/preprocessing/__init__.py +39 -0
  26. microarray/preprocessing/_background.py +862 -0
  27. microarray/preprocessing/_log2.py +77 -0
  28. microarray/preprocessing/_normalize.py +1292 -0
  29. microarray/preprocessing/_rma.py +243 -0
  30. microarray/preprocessing/_robust.py +170 -0
  31. microarray/preprocessing/_summarize.py +318 -0
  32. microarray/py.typed +0 -0
  33. microarray/tools/__init__.py +26 -0
  34. microarray/tools/_biomart.py +416 -0
  35. microarray/tools/_empirical_bayes.py +401 -0
  36. microarray/tools/_fdist.py +171 -0
  37. microarray/tools/_linear_models.py +387 -0
  38. microarray/tools/_mds.py +101 -0
  39. microarray/tools/_pca.py +88 -0
  40. microarray/tools/_score.py +86 -0
  41. microarray/tools/_toptable.py +360 -0
  42. microarray-0.1.0.dist-info/METADATA +75 -0
  43. microarray-0.1.0.dist-info/RECORD +44 -0
  44. 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)