bed-reader 1.0.6__cp313-none-win_amd64.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 (57) hide show
  1. bed_reader/__init__.py +32 -0
  2. bed_reader/_open_bed.py +1842 -0
  3. bed_reader/_sample_data.py +159 -0
  4. bed_reader/_to_bed.py +605 -0
  5. bed_reader/bed_reader.cp313-win_amd64.pyd +0 -0
  6. bed_reader/tests/__init__.py +1 -0
  7. bed_reader/tests/benchmark/bench2.ipynb +251 -0
  8. bed_reader/tests/benchmark/benchmark.py +135 -0
  9. bed_reader/tests/data/500x400_o640_array.memmap +0 -0
  10. bed_reader/tests/data/badfile.bed +1 -0
  11. bed_reader/tests/data/empty.bed +0 -0
  12. bed_reader/tests/data/plink_sim_10s_100v_10pmiss.bed +0 -0
  13. bed_reader/tests/data/plink_sim_10s_100v_10pmiss.bim +100 -0
  14. bed_reader/tests/data/plink_sim_10s_100v_10pmiss.fam +10 -0
  15. bed_reader/tests/data/small.bad_bim +4 -0
  16. bed_reader/tests/data/small.bed +1 -0
  17. bed_reader/tests/data/small.bim +4 -0
  18. bed_reader/tests/data/small.bim_bad_positions.bim +4 -0
  19. bed_reader/tests/data/small.deb +1 -0
  20. bed_reader/tests/data/small.fam +3 -0
  21. bed_reader/tests/data/small.fam_bad +4 -0
  22. bed_reader/tests/data/small.maf +3 -0
  23. bed_reader/tests/data/small.mib +4 -0
  24. bed_reader/tests/data/small2_array.memmap +0 -0
  25. bed_reader/tests/data/small_array.memmap +0 -0
  26. bed_reader/tests/data/small_no_bim.bed +1 -0
  27. bed_reader/tests/data/small_no_bim.fam +3 -0
  28. bed_reader/tests/data/small_no_fam.bed +1 -0
  29. bed_reader/tests/data/small_no_fam.bim +4 -0
  30. bed_reader/tests/data/small_too_short.bed +1 -0
  31. bed_reader/tests/data/small_too_short.bim +4 -0
  32. bed_reader/tests/data/small_too_short.fam +3 -0
  33. bed_reader/tests/data/smallmode0.bed +0 -0
  34. bed_reader/tests/data/smallmode0.bim +3 -0
  35. bed_reader/tests/data/smallmode0.fam +4 -0
  36. bed_reader/tests/data/smallmodebad.bed +1 -0
  37. bed_reader/tests/data/some_missing.bed +0 -0
  38. bed_reader/tests/data/some_missing.bim +100 -0
  39. bed_reader/tests/data/some_missing.fam +100 -0
  40. bed_reader/tests/data/some_missing.properties.npz +0 -0
  41. bed_reader/tests/data/some_missing.val.npy +0 -0
  42. bed_reader/tests/data/sparse.bed +1 -0
  43. bed_reader/tests/data/sparse.bim +20 -0
  44. bed_reader/tests/data/sparse.fam +10 -0
  45. bed_reader/tests/data/toydata.5chrom.bed +0 -0
  46. bed_reader/tests/data/toydata.5chrom.bim +10000 -0
  47. bed_reader/tests/data/toydata.5chrom.fam +500 -0
  48. bed_reader/tests/registry.txt +41 -0
  49. bed_reader/tests/test_file_dot.py +359 -0
  50. bed_reader/tests/test_open_bed.py +1448 -0
  51. bed_reader/tests/test_open_bed_cloud.py +1165 -0
  52. bed_reader/tests/test_opt_dep.py +29 -0
  53. bed_reader-1.0.6.dist-info/METADATA +155 -0
  54. bed_reader-1.0.6.dist-info/RECORD +57 -0
  55. bed_reader-1.0.6.dist-info/WHEEL +4 -0
  56. bed_reader-1.0.6.dist-info/licenses/AUTHORS.txt +2 -0
  57. bed_reader-1.0.6.dist-info/licenses/LICENSE.md +203 -0
@@ -0,0 +1,1842 @@
1
+ import logging
2
+ import multiprocessing
3
+ import os
4
+ import re
5
+ from dataclasses import dataclass
6
+ from io import BytesIO
7
+ from itertools import repeat, takewhile
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any, List, Mapping, Optional, Union
10
+ from urllib.parse import ParseResult as UrlParseResult
11
+ from urllib.parse import urlparse
12
+
13
+ import numpy as np
14
+
15
+ if TYPE_CHECKING:
16
+ from scipy import sparse
17
+ else:
18
+ try:
19
+ from scipy import sparse
20
+ except ImportError:
21
+ sparse = None
22
+
23
+ from .bed_reader import (
24
+ check_file_cloud,
25
+ read_cloud_f32,
26
+ read_cloud_f64,
27
+ read_cloud_i8,
28
+ read_f32,
29
+ read_f64,
30
+ read_i8,
31
+ url_to_bytes,
32
+ )
33
+
34
+
35
+ # https://stackoverflow.com/questions/845058/how-to-get-line-count-of-a-large-file-cheaply-in-python
36
+ def _rawincount(f):
37
+ f.seek(0)
38
+ bufgen = takewhile(lambda x: x, (f.read(1024 * 1024) for _ in repeat(None)))
39
+ return sum(buf.count(b"\n") for buf in bufgen)
40
+
41
+
42
+ @dataclass
43
+ class _MetaMeta:
44
+ suffix: str
45
+ column: int
46
+ dtype: type
47
+ missing_value: object
48
+ fill_sequence: object
49
+
50
+
51
+ def _all_same(key, length, missing, dtype):
52
+ if np.issubdtype(dtype, np.str_):
53
+ dtype = f"<U{len(missing)}"
54
+ return np.full(length, missing, dtype=dtype)
55
+
56
+
57
+ def _sequence(key, length, missing, dtype):
58
+ if np.issubdtype(dtype, np.str_):
59
+ longest = len(f"{key}{length}")
60
+ dtype = f"<U{longest}"
61
+ return np.fromiter(
62
+ (f"{key}{i + 1}" for i in range(length)), dtype=dtype, count=length,
63
+ )
64
+
65
+
66
+ _delimiters = {"fam": r"\s+", "bim": "\t"}
67
+ _count_name = {"fam": "iid_count", "bim": "sid_count"}
68
+
69
+
70
+ _meta_meta = {
71
+ # https://stackoverflow.com/questions/41921255/staticmethod-object-is-not-callable
72
+ "fid": _MetaMeta("fam", 0, np.str_, "0", _all_same),
73
+ "iid": _MetaMeta("fam", 1, np.str_, None, _sequence),
74
+ "father": _MetaMeta("fam", 2, np.str_, "0", _all_same),
75
+ "mother": _MetaMeta("fam", 3, np.str_, "0", _all_same),
76
+ "sex": _MetaMeta("fam", 4, np.int32, 0, _all_same),
77
+ "pheno": _MetaMeta("fam", 5, np.str_, "0", _all_same),
78
+ "chromosome": _MetaMeta("bim", 0, np.str_, "0", _all_same),
79
+ "sid": _MetaMeta("bim", 1, np.str_, None, _sequence),
80
+ "cm_position": _MetaMeta("bim", 2, np.float32, 0, _all_same),
81
+ "bp_position": _MetaMeta("bim", 3, np.int32, 0, _all_same),
82
+ "allele_1": _MetaMeta("bim", 4, np.str_, "A1", _all_same),
83
+ "allele_2": _MetaMeta("bim", 5, np.str_, "A2", _all_same),
84
+ }
85
+
86
+
87
+ def get_num_threads(num_threads=None):
88
+ if num_threads is not None:
89
+ return num_threads
90
+ if "PST_NUM_THREADS" in os.environ:
91
+ return int(os.environ["PST_NUM_THREADS"])
92
+ if "NUM_THREADS" in os.environ:
93
+ return int(os.environ["NUM_THREADS"])
94
+ if "MKL_NUM_THREADS" in os.environ:
95
+ return int(os.environ["MKL_NUM_THREADS"])
96
+ return multiprocessing.cpu_count()
97
+
98
+
99
+ def get_max_concurrent_requests(max_concurrent_requests=None):
100
+ if max_concurrent_requests is not None:
101
+ return max_concurrent_requests
102
+ return 10
103
+
104
+
105
+ def get_max_chunk_bytes(max_chunk_bytes=None):
106
+ if max_chunk_bytes is not None:
107
+ return max_chunk_bytes
108
+ return 8_000_000
109
+
110
+
111
+ class open_bed:
112
+ """Open a PLINK .bed file, local or cloud, for reading.
113
+
114
+ Parameters
115
+ ----------
116
+ location: pathlib.Path or str
117
+ File path or URL to the .bed file. See :doc:`cloud_urls` for details on cloud URLs.
118
+ iid_count: None or int, optional
119
+ Number of individuals (samples) in the .bed file.
120
+ The default (``iid_count=None``) finds the number
121
+ automatically by quickly scanning the .fam file.
122
+ sid_count: None or int, optional
123
+ Number of SNPs (variants) in the .bed file.
124
+ The default (``sid_count=None``) finds the number
125
+ automatically by quickly scanning the .bim file.
126
+ properties: dict, optional
127
+ A dictionary of any replacement properties. The default is an empty dictionary.
128
+ The keys of the dictionary are the names of the properties to replace.
129
+ The possible keys are:
130
+
131
+ "fid" (family id), "iid" (individual or sample id), "father" (father id),
132
+ "mother" (mother id), "sex", "pheno" (phenotype), "chromosome", "sid"
133
+ (SNP or variant id), "cm_position" (centimorgan position), "bp_position"
134
+ (base-pair position), "allele_1", "allele_2".
135
+
136
+ The values are replacement lists or arrays. A value can also be `None`,
137
+ meaning do not read or offer this property. See examples, below.
138
+
139
+ The list or array will be converted to a :class:`numpy.ndarray`
140
+ of the appropriate dtype, if necessary. Any :data:`numpy.nan` values
141
+ will converted to the appropriate missing value. The PLINK `.fam specification
142
+ <https://www.cog-genomics.org/plink2/formats#fam>`_
143
+ and `.bim specification <https://www.cog-genomics.org/plink2/formats#bim>`_
144
+ lists the dtypes and missing values for each property.
145
+
146
+ count_A1: bool, optional
147
+ True (default) to count the number of A1 alleles (the PLINK standard).
148
+ False to count the number of A2 alleles.
149
+ num_threads: None or int, optional
150
+ The number of threads with which to read data. Defaults to all available
151
+ processors.
152
+ Can also be set with these environment variables (listed in priority order):
153
+ 'PST_NUM_THREADS', 'NUM_THREADS', 'MKL_NUM_THREADS'.
154
+ skip_format_check: bool, optional
155
+ False (default) to immediately check for expected starting bytes in
156
+ the .bed file. True to delay the check until (and if) data is read.
157
+ fam_location: pathlib.Path or str or URL, optional
158
+ Path to the file containing information about each individual (sample).
159
+ Defaults to replacing the .bed file’s suffix with .fam.
160
+ bim_location: pathlib.Path or str URL, optional
161
+ Path to the file containing information about each SNP (variant).
162
+ Defaults to replacing the .bed file’s suffix with .bim.
163
+ cloud_options: dict, optional
164
+ A dictionary of options for reading from cloud storage. The default is an empty.
165
+ max_concurrent_requests: None or int, optional
166
+ The maximum number of concurrent requests to make to the cloud storage service.
167
+ Defaults to 10.
168
+ max_chunk_bytes: None or int, optional
169
+ The maximum number of bytes to read in a single request to the cloud storage
170
+ service. Defaults to 8MB.
171
+ filepath: same as location
172
+ Deprecated. Use location instead.
173
+ fam_filepath: same as fam_location
174
+ Deprecated. Use fam_location instead.
175
+ bim_filepath: same as bim_location
176
+ Deprecated. Use bim_location instead.
177
+
178
+ Returns
179
+ -------
180
+ open_bed
181
+ an open_bed object
182
+
183
+ Examples
184
+ --------
185
+ Open a local file and list individual (sample) :attr:`iid` and SNP (variant) :attr:`sid`.
186
+ Then, :meth:`read` the whole file.
187
+
188
+ .. doctest::
189
+
190
+ >>> from bed_reader import open_bed, sample_file
191
+ >>>
192
+ >>> file_name = sample_file("small.bed")
193
+ >>> bed = open_bed(file_name)
194
+ >>> print(bed.iid)
195
+ ['iid1' 'iid2' 'iid3']
196
+ >>> print(bed.sid)
197
+ ['sid1' 'sid2' 'sid3' 'sid4']
198
+ >>> print(bed.read())
199
+ [[ 1. 0. nan 0.]
200
+ [ 2. 0. nan 2.]
201
+ [ 0. 1. 2. 0.]]
202
+ >>> del bed # optional: delete bed object
203
+
204
+ Open a cloud file with a non-default timeout.
205
+ Then, read the data for one SNP (variant)
206
+ at index position 2.
207
+
208
+ See :doc:`cloud_urls` for details on reading files from cloud storage.
209
+
210
+ .. doctest::
211
+
212
+ >>> import numpy as np
213
+ >>> with open_bed("https://raw.githubusercontent.com/fastlmm/bed-sample-files/main/small.bed",
214
+ ... cloud_options={"timeout": "10s"}) as bed:
215
+ ... print(bed.read(np.s_[:,2]))
216
+ [[nan]
217
+ [nan]
218
+ [ 2.]]
219
+
220
+ With the local file, replace :attr:`iid`.
221
+
222
+
223
+ >>> bed = open_bed(file_name, properties={"iid":["sample1","sample2","sample3"]})
224
+ >>> print(bed.iid) # replaced
225
+ ['sample1' 'sample2' 'sample3']
226
+ >>> print(bed.sid) # same as before
227
+ ['sid1' 'sid2' 'sid3' 'sid4']
228
+
229
+ Give the number of individuals (samples) and SNPs (variants) so that the .fam and
230
+ .bim files need never be opened.
231
+
232
+ >>> with open_bed(file_name, iid_count=3, sid_count=4) as bed:
233
+ ... print(bed.read())
234
+ [[ 1. 0. nan 0.]
235
+ [ 2. 0. nan 2.]
236
+ [ 0. 1. 2. 0.]]
237
+
238
+ Mark some properties as "don’t read or offer".
239
+
240
+ >>> bed = open_bed(file_name, properties={
241
+ ... "father" : None, "mother" : None, "sex" : None, "pheno" : None,
242
+ ... "allele_1" : None, "allele_2":None })
243
+ >>> print(bed.iid) # read from file
244
+ ['iid1' 'iid2' 'iid3']
245
+ >>> print(bed.allele_2) # not read and not offered
246
+ None
247
+
248
+ See the :meth:`read` for details of reading batches via slicing and fancy indexing.
249
+
250
+ """
251
+
252
+ def __init__(
253
+ self,
254
+ location: Union[str, Path, UrlParseResult],
255
+ iid_count: Optional[int] = None,
256
+ sid_count: Optional[int] = None,
257
+ properties: Mapping[str, List[Any]] = {},
258
+ count_A1: bool = True,
259
+ num_threads: Optional[int] = None,
260
+ skip_format_check: bool = False,
261
+ fam_location: Optional[Union[str, Path, UrlParseResult]] = None,
262
+ bim_location: Optional[Union[str, Path, UrlParseResult]] = None,
263
+ cloud_options: Mapping[str, str] = {},
264
+ max_concurrent_requests: Optional[int] = None,
265
+ max_chunk_bytes: Optional[int] = None,
266
+ # accept old keywords
267
+ filepath: Optional[Union[str, Path]] = None,
268
+ fam_filepath: Optional[Union[str, Path]] = None,
269
+ bim_filepath: Optional[Union[str, Path]] = None,
270
+ ) -> None:
271
+ location = self._combined(location, filepath, "location", "filepath")
272
+ fam_location = self._combined(
273
+ fam_location, fam_filepath, "fam_location", "fam_filepath",
274
+ )
275
+ bim_location = self._combined(
276
+ bim_location, bim_filepath, "bim_location", "bim_filepath",
277
+ )
278
+
279
+ self.location = self._path_or_url(location)
280
+ self.cloud_options = cloud_options
281
+ self.count_A1 = count_A1
282
+ self._num_threads = num_threads
283
+ self._max_concurrent_requests = max_concurrent_requests
284
+ self._max_chunk_bytes = max_chunk_bytes
285
+ self.skip_format_check = skip_format_check
286
+ self._fam_location = (
287
+ self._path_or_url(fam_location)
288
+ if fam_location is not None
289
+ else self._replace_extension(self.location, "fam")
290
+ )
291
+ self._bim_location = (
292
+ self._path_or_url(bim_location)
293
+ if bim_location is not None
294
+ else self._replace_extension(self.location, "bim")
295
+ )
296
+
297
+ self.properties_dict, self._counts = open_bed._fix_up_properties(
298
+ properties, iid_count, sid_count, use_fill_sequence=False,
299
+ )
300
+ self._iid_range = None
301
+ self._sid_range = None
302
+ if not self.skip_format_check:
303
+ if self._is_url(self.location):
304
+ check_file_cloud(self.location.geturl(), self.cloud_options)
305
+ else:
306
+ with open(self.location, "rb") as filepointer:
307
+ self._mode = self._check_file(filepointer)
308
+
309
+ # # its an error to set both location and filepath
310
+ # location = self._combined(location, filepath, "location", "filepath")
311
+ # fam_location = self._combined(fam_location, fam_filepath, "fam_location", "fam_filepath")
312
+ # bim_location = self._combined(bim_location, bim_filepath, "bim_location", "bim_filepath")
313
+ @staticmethod
314
+ def _combined(location, filepath, location_name, filepath_name):
315
+ if location is not None and filepath is not None:
316
+ msg = f"Cannot set both {location_name} and {filepath_name}"
317
+ raise ValueError(msg)
318
+ # None, None is ok for now
319
+ return location if location is not None else filepath
320
+
321
+ @staticmethod
322
+ def _replace_extension(location, extension):
323
+ if open_bed._is_url(location):
324
+ # Split the path and change the extension
325
+ path, _ = os.path.splitext(location.path)
326
+ new_path = f"{path}.{extension}"
327
+
328
+ # Create a new ParseResult with the updated path
329
+ return UrlParseResult(
330
+ scheme=location.scheme,
331
+ netloc=location.netloc,
332
+ path=new_path,
333
+ params=location.params,
334
+ query=location.query,
335
+ fragment=location.fragment,
336
+ )
337
+ assert isinstance(location, Path) # real assert
338
+ return location.parent / (location.stem + "." + extension)
339
+
340
+ @staticmethod
341
+ def _is_url(location):
342
+ return isinstance(location, UrlParseResult)
343
+
344
+ @staticmethod
345
+ def _path_or_url(input):
346
+ if isinstance(input, Path):
347
+ return input
348
+ if isinstance(input, UrlParseResult):
349
+ return input
350
+ assert isinstance(
351
+ input, str,
352
+ ), "Expected a string or Path object or UrlParseResult"
353
+ parsed = urlparse(input)
354
+ if parsed.scheme and "://" in input:
355
+ return parsed
356
+ return Path(input)
357
+
358
+ def read(
359
+ self,
360
+ index: Optional[Any] = None,
361
+ dtype: Optional[Union[type, str]] = "float32",
362
+ order: Optional[str] = "F",
363
+ force_python_only: Optional[bool] = False,
364
+ num_threads=None,
365
+ max_concurrent_requests=None,
366
+ max_chunk_bytes=None,
367
+ ) -> np.ndarray:
368
+ """Read genotype information.
369
+
370
+ Parameters
371
+ ----------
372
+ index:
373
+ An optional expression specifying the individuals (samples) and SNPs
374
+ (variants) to read. (See examples, below).
375
+ Defaults to ``None``, meaning read all.
376
+
377
+ (If index is a tuple, the first component indexes the individuals and the
378
+ second indexes
379
+ the SNPs. If it is not a tuple and not None, it indexes SNPs.)
380
+
381
+ dtype: {'float32' (default), 'float64', 'int8'}, optional
382
+ The desired data-type for the returned array.
383
+ order : {'F','C'}, optional
384
+ The desired memory layout for the returned array.
385
+ Defaults to ``F`` (Fortran order, which is SNP-major).
386
+ force_python_only: bool, optional
387
+ If False (default), uses the faster Rust code; otherwise it uses the slower
388
+ pure Python code.
389
+
390
+ num_threads: None or int, optional
391
+ The number of threads with which to read data. Defaults to all available
392
+ processors.
393
+ Can also be set with :class:`open_bed` or these
394
+ environment variables (listed in priority order):
395
+ 'PST_NUM_THREADS', 'NUM_THREADS', 'MKL_NUM_THREADS'.
396
+
397
+ max_concurrent_requests: None or int, optional
398
+ The maximum number of concurrent requests to make to the cloud storage
399
+ service. Defaults to 10.
400
+
401
+ max_chunk_bytes: None or int, optional
402
+ The maximum number of bytes to read in a single request to the cloud
403
+ storage service. Defaults to 8MB.
404
+
405
+ Returns
406
+ -------
407
+ numpy.ndarray
408
+ 2-D array containing values of 0, 1, 2, or missing
409
+
410
+
411
+ Rows represent individuals (samples). Columns represent SNPs (variants).
412
+
413
+ For ``dtype`` 'float32' and 'float64', NaN indicates missing values.
414
+ For 'int8', -127 indicates missing values.
415
+
416
+ Examples
417
+ --------
418
+ To read all data in a .bed file, set ``index`` to ``None``. This is the default.
419
+
420
+ .. doctest::
421
+
422
+ >>> from bed_reader import open_bed, sample_file
423
+ >>>
424
+ >>> file_name = sample_file("small.bed")
425
+ >>> with open_bed(file_name) as bed:
426
+ ... print(bed.read())
427
+ [[ 1. 0. nan 0.]
428
+ [ 2. 0. nan 2.]
429
+ [ 0. 1. 2. 0.]]
430
+
431
+ To read selected individuals (samples) and/or SNPs (variants), set each part of
432
+ a :data:`numpy.s_` to an `int`, a list of `int`, a slice expression, or
433
+ a list of `bool`.
434
+ Negative integers count from the end of the list.
435
+
436
+
437
+ .. doctest::
438
+
439
+ >>> import numpy as np
440
+ >>> bed = open_bed(file_name)
441
+ >>> print(bed.read(np.s_[:,2])) # read the SNPs indexed by 2.
442
+ [[nan]
443
+ [nan]
444
+ [ 2.]]
445
+ >>> print(bed.read(np.s_[:,[2,3,0]])) # read the SNPs indexed by 2, 3, and 0
446
+ [[nan 0. 1.]
447
+ [nan 2. 2.]
448
+ [ 2. 0. 0.]]
449
+ >>> # read SNPs from 1 (inclusive) to 4 (exclusive)
450
+ >>> print(bed.read(np.s_[:,1:4]))
451
+ [[ 0. nan 0.]
452
+ [ 0. nan 2.]
453
+ [ 1. 2. 0.]]
454
+ >>> print(np.unique(bed.chromosome)) # print unique chrom values
455
+ ['1' '5' 'Y']
456
+ >>> print(bed.read(np.s_[:,bed.chromosome=='5'])) # read all SNPs in chrom 5
457
+ [[nan]
458
+ [nan]
459
+ [ 2.]]
460
+ >>> print(bed.read(np.s_[0,:])) # Read 1st individual (across all SNPs)
461
+ [[ 1. 0. nan 0.]]
462
+ >>> print(bed.read(np.s_[::2,:])) # Read every 2nd individual
463
+ [[ 1. 0. nan 0.]
464
+ [ 0. 1. 2. 0.]]
465
+ >>> #read last and 2nd-to-last individuals and the last SNPs
466
+ >>> print(bed.read(np.s_[[-1,-2],-1]))
467
+ [[0.]
468
+ [2.]]
469
+
470
+
471
+ You can give a dtype for the output.
472
+
473
+ .. doctest::
474
+
475
+ >>> print(bed.read(dtype='int8'))
476
+ [[ 1 0 -127 0]
477
+ [ 2 0 -127 2]
478
+ [ 0 1 2 0]]
479
+ >>> del bed # optional: delete bed object
480
+
481
+ """
482
+ iid_index_or_slice_etc, sid_index_or_slice_etc = self._split_index(index)
483
+
484
+ dtype = np.dtype(dtype)
485
+ if order not in {"F", "C"}:
486
+ msg = f"order '{order}' not known, only 'F', 'C'"
487
+ raise ValueError(msg)
488
+
489
+ # Later happy with _iid_range and _sid_range or could it be done with
490
+ # allocation them?
491
+ if self._iid_range is None:
492
+ self._iid_range = np.arange(self.iid_count, dtype="intp")
493
+ if self._sid_range is None:
494
+ self._sid_range = np.arange(self.sid_count, dtype="intp")
495
+
496
+ iid_index = np.ascontiguousarray(
497
+ self._iid_range[iid_index_or_slice_etc],
498
+ dtype="intp",
499
+ )
500
+ sid_index = np.ascontiguousarray(
501
+ self._sid_range[sid_index_or_slice_etc], dtype="intp",
502
+ )
503
+
504
+ if not force_python_only or open_bed._is_url(self.location):
505
+ num_threads = get_num_threads(
506
+ self._num_threads if num_threads is None else num_threads,
507
+ )
508
+ max_concurrent_requests = get_max_concurrent_requests(
509
+ self._max_concurrent_requests
510
+ if max_concurrent_requests is None
511
+ else max_concurrent_requests,
512
+ )
513
+ max_chunk_bytes = get_max_chunk_bytes(
514
+ self._max_chunk_bytes if max_chunk_bytes is None else max_chunk_bytes,
515
+ )
516
+
517
+ val = np.zeros((len(iid_index), len(sid_index)), order=order, dtype=dtype)
518
+
519
+ if self.iid_count > 0 and self.sid_count > 0:
520
+ reader, location_str, is_cloud = self._pick_reader(dtype)
521
+
522
+ if not is_cloud:
523
+ reader(
524
+ location_str,
525
+ self.cloud_options,
526
+ iid_count=self.iid_count,
527
+ sid_count=self.sid_count,
528
+ is_a1_counted=self.count_A1,
529
+ iid_index=iid_index,
530
+ sid_index=sid_index,
531
+ val=val,
532
+ num_threads=num_threads,
533
+ )
534
+ else:
535
+ reader(
536
+ location_str,
537
+ self.cloud_options,
538
+ iid_count=self.iid_count,
539
+ sid_count=self.sid_count,
540
+ is_a1_counted=self.count_A1,
541
+ iid_index=iid_index,
542
+ sid_index=sid_index,
543
+ val=val,
544
+ num_threads=num_threads,
545
+ max_concurrent_requests=max_concurrent_requests,
546
+ max_chunk_bytes=max_chunk_bytes,
547
+ )
548
+
549
+ else:
550
+
551
+ if not self.count_A1:
552
+ byteZero = 0
553
+ byteThree = 2
554
+ else:
555
+ byteZero = 2
556
+ byteThree = 0
557
+ missing = -127 if dtype == np.int8 else np.nan
558
+ # An earlier version of this code had a way to read consecutive SNPs of code
559
+ # in one read. May want
560
+ # to add that ability back to the code.
561
+ # Also, note that reading with python will often result in
562
+ # non-contiguous memory
563
+ # logging.warn("using pure python plink parser (might be much slower!!)")
564
+
565
+ if self.major == "SNP":
566
+ minor_count = self.iid_count
567
+ minor_index = iid_index
568
+ major_index = sid_index
569
+ else:
570
+ minor_count = self.sid_count
571
+ minor_index = sid_index
572
+ major_index = iid_index
573
+
574
+ val = np.zeros(
575
+ ((int(np.ceil(0.25 * minor_count)) * 4), len(major_index)),
576
+ order=order,
577
+ dtype=dtype,
578
+ ) # allocate it a little big
579
+
580
+ nbyte = int(np.ceil(0.25 * minor_count))
581
+ with open(self.location, "rb") as filepointer:
582
+ for major_index_value, major_index_index in enumerate(major_index):
583
+ startbit = int(np.ceil(0.25 * minor_count) * major_index_index + 3)
584
+ filepointer.seek(startbit)
585
+ bytes = np.array(bytearray(filepointer.read(nbyte))).reshape(
586
+ (int(np.ceil(0.25 * minor_count)), 1), order="F",
587
+ )
588
+
589
+ val[3::4, major_index_value : major_index_value + 1] = byteZero
590
+ val[3::4, major_index_value : major_index_value + 1][
591
+ bytes >= 64
592
+ ] = missing
593
+ val[3::4, major_index_value : major_index_value + 1][
594
+ bytes >= 128
595
+ ] = 1
596
+ val[3::4, major_index_value : major_index_value + 1][
597
+ bytes >= 192
598
+ ] = byteThree
599
+ bytes = np.mod(bytes, 64)
600
+ val[2::4, major_index_value : major_index_value + 1] = byteZero
601
+ val[2::4, major_index_value : major_index_value + 1][
602
+ bytes >= 16
603
+ ] = missing
604
+ val[2::4, major_index_value : major_index_value + 1][
605
+ bytes >= 32
606
+ ] = 1
607
+ val[2::4, major_index_value : major_index_value + 1][
608
+ bytes >= 48
609
+ ] = byteThree
610
+ bytes = np.mod(bytes, 16)
611
+ val[1::4, major_index_value : major_index_value + 1] = byteZero
612
+ val[1::4, major_index_value : major_index_value + 1][
613
+ bytes >= 4
614
+ ] = missing
615
+ val[1::4, major_index_value : major_index_value + 1][bytes >= 8] = 1
616
+ val[1::4, major_index_value : major_index_value + 1][
617
+ bytes >= 12
618
+ ] = byteThree
619
+ bytes = np.mod(bytes, 4)
620
+ val[0::4, major_index_value : major_index_value + 1] = byteZero
621
+ val[0::4, major_index_value : major_index_value + 1][
622
+ bytes >= 1
623
+ ] = missing
624
+ val[0::4, major_index_value : major_index_value + 1][bytes >= 2] = 1
625
+ val[0::4, major_index_value : major_index_value + 1][
626
+ bytes >= 3
627
+ ] = byteThree
628
+ val = val[minor_index, :] # reorder or trim any extra allocation
629
+ assert val.dtype == np.dtype(dtype) # real assert
630
+ if not open_bed._array_properties_are_ok(val, order):
631
+ val = val.copy(order=order)
632
+ # if in force python mode, and individual-major mode, then we need to transpose
633
+ if self.major == "individual":
634
+ val = val.T
635
+ return val
636
+
637
+ def _pick_reader(self, dtype):
638
+ if dtype == np.int8:
639
+ file_reader = read_i8
640
+ cloud_reader = read_cloud_i8
641
+ elif dtype == np.float64:
642
+ file_reader = read_f64
643
+ cloud_reader = read_cloud_f64
644
+ elif dtype == np.float32:
645
+ file_reader = read_f32
646
+ cloud_reader = read_cloud_f32
647
+ else:
648
+ raise ValueError(
649
+ f"dtype '{dtype}' not known, only "
650
+ + "'int8', 'float32', and 'float64' are allowed.",
651
+ )
652
+
653
+ if open_bed._is_url(self.location):
654
+ reader = cloud_reader
655
+ location_str = self.location.geturl()
656
+ is_cloud = True
657
+ else:
658
+ reader = file_reader
659
+ location_str = str(self.location.as_posix())
660
+ is_cloud = False
661
+ return reader, location_str, is_cloud
662
+
663
+ def __str__(self) -> str:
664
+ return f"{self.__class__.__name__}('{self.location}',...)"
665
+
666
+ @property
667
+ def major(self) -> str:
668
+ """Major mode of a local .bed file.
669
+
670
+ Returns:
671
+ -------
672
+ str
673
+ 'SNP' or 'individual'
674
+
675
+
676
+ Almost all PLINK 1.9 .bed files are 'SNP' major. This makes
677
+ reading the data by SNP(s) fast.
678
+
679
+ Errors
680
+ ------
681
+ ValueError
682
+ If the file is a cloud file.
683
+
684
+ Example:
685
+ -------
686
+
687
+ .. doctest::
688
+
689
+ >>> from bed_reader import open_bed, sample_file
690
+ >>>
691
+ >>> file_name = sample_file("small.bed")
692
+ >>> with open_bed(file_name) as bed:
693
+ ... print(bed.major)
694
+ SNP
695
+
696
+ """
697
+ if self._is_url(self.location):
698
+ msg = "Cannot determine major mode for cloud files"
699
+ raise ValueError(msg)
700
+ # if self._mode is not set, set it
701
+ if not hasattr(self, "mode"):
702
+ with open(self.location, "rb") as filepointer:
703
+ self._mode = self._check_file(filepointer)
704
+
705
+ return "individual" if self._mode == b"\x00" else "SNP"
706
+
707
+ @property
708
+ def fid(self) -> np.ndarray:
709
+ """Family id of each individual (sample).
710
+
711
+ Returns:
712
+ -------
713
+ numpy.ndarray
714
+ array of str
715
+
716
+
717
+ '0' represents a missing value.
718
+
719
+ If needed, will cause a one-time read of the .fam file.
720
+
721
+ Example:
722
+ -------
723
+
724
+ .. doctest::
725
+
726
+ >>> from bed_reader import open_bed, sample_file
727
+ >>>
728
+ >>> file_name = sample_file("small.bed")
729
+ >>> with open_bed(file_name) as bed:
730
+ ... print(bed.fid)
731
+ ['fid1' 'fid1' 'fid2']
732
+
733
+ """
734
+ return self.property_item("fid")
735
+
736
+ @property
737
+ def iid(self) -> np.ndarray:
738
+ """Individual id of each individual (sample).
739
+
740
+ Returns:
741
+ -------
742
+ numpy.ndarray
743
+ array of str
744
+
745
+
746
+ If needed, will cause a one-time read of the .fam file.
747
+
748
+ Example:
749
+ -------
750
+
751
+ .. doctest::
752
+
753
+ >>> from bed_reader import open_bed, sample_file
754
+ >>>
755
+ >>> file_name = sample_file("small.bed")
756
+ >>> with open_bed(file_name) as bed:
757
+ ... print(bed.iid)
758
+ ['iid1' 'iid2' 'iid3']
759
+
760
+ """
761
+ return self.property_item("iid")
762
+
763
+ @property
764
+ def father(self) -> np.ndarray:
765
+ """Father id of each individual (sample).
766
+
767
+ Returns:
768
+ -------
769
+ numpy.ndarray
770
+ array of str
771
+
772
+
773
+ '0' represents a missing value.
774
+
775
+ If needed, will cause a one-time read of the .fam file.
776
+
777
+ Example:
778
+ -------
779
+
780
+ .. doctest::
781
+
782
+ >>> from bed_reader import open_bed, sample_file
783
+ >>>
784
+ >>> file_name = sample_file("small.bed")
785
+ >>> with open_bed(file_name) as bed:
786
+ ... print(bed.father)
787
+ ['iid23' 'iid23' 'iid22']
788
+
789
+ """
790
+ return self.property_item("father")
791
+
792
+ @property
793
+ def mother(self) -> np.ndarray:
794
+ """Mother id of each individual (sample).
795
+
796
+ Returns:
797
+ -------
798
+ numpy.ndarray
799
+ array of str
800
+
801
+
802
+ '0' represents a missing value.
803
+
804
+ If needed, will cause a one-time read of the .fam file.
805
+
806
+ Example:
807
+ -------
808
+
809
+ .. doctest::
810
+
811
+ >>> from bed_reader import open_bed, sample_file
812
+ >>>
813
+ >>> file_name = sample_file("small.bed")
814
+ >>> with open_bed(file_name) as bed:
815
+ ... print(bed.mother)
816
+ ['iid34' 'iid34' 'iid33']
817
+
818
+ """
819
+ return self.property_item("mother")
820
+
821
+ @property
822
+ def sex(self) -> np.ndarray:
823
+ """Sex of each individual (sample).
824
+
825
+ Returns:
826
+ -------
827
+ numpy.ndarray
828
+ array of 0, 1, or 2
829
+
830
+
831
+ 0 is unknown, 1 is male, 2 is female
832
+
833
+ If needed, will cause a one-time read of the .fam file.
834
+
835
+ Example:
836
+ -------
837
+ .. doctest::
838
+
839
+ >>> from bed_reader import open_bed, sample_file
840
+ >>>
841
+ >>> file_name = sample_file("small.bed")
842
+ >>> with open_bed(file_name) as bed:
843
+ ... print(bed.sex)
844
+ [1 2 0]
845
+
846
+ """
847
+ return self.property_item("sex")
848
+
849
+ @property
850
+ def pheno(self) -> np.ndarray:
851
+ """A phenotype for each individual (sample)
852
+ (seldom used).
853
+
854
+ Returns:
855
+ -------
856
+ numpy.ndarray
857
+ array of str
858
+
859
+
860
+ '0' may represent a missing value.
861
+
862
+ If needed, will cause a one-time read of the .fam file.
863
+
864
+ Example:
865
+ -------
866
+
867
+ .. doctest::
868
+
869
+ >>> from bed_reader import open_bed, sample_file
870
+ >>>
871
+ >>> file_name = sample_file("small.bed")
872
+ >>> with open_bed(file_name) as bed:
873
+ ... print(bed.pheno)
874
+ ['red' 'red' 'blue']
875
+
876
+ """
877
+ return self.property_item("pheno")
878
+
879
+ @property
880
+ def properties(self) -> Mapping[str, np.array]:
881
+ """All the properties returned as a dictionary.
882
+
883
+ Returns:
884
+ -------
885
+ dict
886
+ all the properties
887
+
888
+
889
+ The keys of the dictionary are the names of the properties, namely:
890
+
891
+ "fid" (family id), "iid" (individual or sample id), "father" (father id),
892
+ "mother" (mother id), "sex", "pheno" (phenotype), "chromosome", "sid"
893
+ (SNP or variant id), "cm_position" (centimorgan position), "bp_position"
894
+ (base-pair position), "allele_1", "allele_2".
895
+
896
+ The values are :class:`numpy.ndarray`.
897
+
898
+ If needed, will cause a one-time read of the .fam and .bim file.
899
+
900
+ Example:
901
+ -------
902
+
903
+ .. doctest::
904
+
905
+ >>> from bed_reader import open_bed, sample_file
906
+ >>>
907
+ >>> file_name = sample_file("small.bed")
908
+ >>> with open_bed(file_name) as bed:
909
+ ... print(len(bed.properties)) #length of dict
910
+ 12
911
+
912
+ """
913
+ for key in _meta_meta:
914
+ self.property_item(key)
915
+ return self.properties_dict
916
+
917
+ def property_item(self, name: str) -> np.ndarray:
918
+ """Retrieve one property by name.
919
+
920
+ Returns:
921
+ -------
922
+ numpy.ndarray
923
+ a property value
924
+
925
+
926
+ The name is one of these:
927
+
928
+ "fid" (family id), "iid" (individual or sample id), "father" (father id),
929
+ "mother" (mother id), "sex", "pheno" (phenotype), "chromosome", "sid"
930
+ (SNP or variant id), "cm_position" (centimorgan position), "bp_position"
931
+ (base-pair position), "allele_1", "allele_2".
932
+
933
+ If needed, will cause a one-time read of the .fam or .bim file.
934
+
935
+ Example:
936
+ -------
937
+
938
+ .. doctest::
939
+
940
+ >>> from bed_reader import open_bed, sample_file
941
+ >>>
942
+ >>> file_name = sample_file("small.bed")
943
+ >>> with open_bed(file_name) as bed:
944
+ ... print(bed.property_item('chromosome'))
945
+ ['1' '1' '5' 'Y']
946
+
947
+ """
948
+ if name not in self.properties_dict:
949
+ mm = _meta_meta[name]
950
+ self._read_fam_or_bim(suffix=mm.suffix)
951
+ return self.properties_dict[name]
952
+
953
+ @property
954
+ def chromosome(self) -> np.ndarray:
955
+ """Chromosome of each SNP (variant).
956
+
957
+ Returns:
958
+ -------
959
+ numpy.ndarray
960
+ array of str
961
+
962
+
963
+ '0' represents a missing value.
964
+
965
+ If needed, will cause a one-time read of the .bim file.
966
+
967
+ Example:
968
+ -------
969
+
970
+ .. doctest::
971
+
972
+ >>> from bed_reader import open_bed, sample_file
973
+ >>>
974
+ >>> file_name = sample_file("small.bed")
975
+ >>> with open_bed(file_name) as bed:
976
+ ... print(bed.chromosome)
977
+ ['1' '1' '5' 'Y']
978
+
979
+ """
980
+ return self.property_item("chromosome")
981
+
982
+ @property
983
+ def sid(self) -> np.ndarray:
984
+ """SNP id of each SNP (variant).
985
+
986
+ Returns:
987
+ -------
988
+ numpy.ndarray
989
+ array of str
990
+
991
+
992
+ If needed, will cause a one-time read of the .bim file.
993
+
994
+ Example:
995
+ -------
996
+
997
+ .. doctest::
998
+
999
+ >>> from bed_reader import open_bed, sample_file
1000
+ >>>
1001
+ >>> file_name = sample_file("small.bed")
1002
+ >>> with open_bed(file_name) as bed:
1003
+ ... print(bed.sid)
1004
+ ['sid1' 'sid2' 'sid3' 'sid4']
1005
+
1006
+ """
1007
+ return self.property_item("sid")
1008
+
1009
+ @property
1010
+ def cm_position(self) -> np.ndarray:
1011
+ """Centimorgan position of each SNP (variant).
1012
+
1013
+ Returns:
1014
+ -------
1015
+ numpy.ndarray
1016
+ array of float
1017
+
1018
+
1019
+ 0.0 represents a missing value.
1020
+
1021
+ If needed, will cause a one-time read of the .bim file.
1022
+
1023
+ Example:
1024
+ -------
1025
+
1026
+ .. doctest::
1027
+
1028
+ >>> from bed_reader import open_bed, sample_file
1029
+ >>>
1030
+ >>> file_name = sample_file("small.bed")
1031
+ >>> with open_bed(file_name) as bed:
1032
+ ... print(bed.cm_position)
1033
+ [ 100.4 2000.5 4000.7 7000.9]
1034
+
1035
+ """
1036
+ return self.property_item("cm_position")
1037
+
1038
+ @property
1039
+ def bp_position(self) -> np.ndarray:
1040
+ """Base-pair position of each SNP (variant).
1041
+
1042
+ Returns:
1043
+ -------
1044
+ numpy.ndarray
1045
+ array of int
1046
+
1047
+
1048
+ 0 represents a missing value.
1049
+
1050
+ If needed, will cause a one-time read of the .bim file.
1051
+
1052
+ Example:
1053
+ -------
1054
+
1055
+ .. doctest::
1056
+
1057
+ >>> from bed_reader import open_bed, sample_file
1058
+ >>>
1059
+ >>> file_name = sample_file("small.bed")
1060
+ >>> with open_bed(file_name) as bed:
1061
+ ... print(bed.bp_position)
1062
+ [ 1 100 1000 1004]
1063
+
1064
+ """
1065
+ return self.property_item("bp_position")
1066
+
1067
+ @property
1068
+ def allele_1(self) -> np.ndarray:
1069
+ """First allele of each SNP (variant).
1070
+
1071
+ Returns:
1072
+ -------
1073
+ numpy.ndarray
1074
+ array of str
1075
+
1076
+
1077
+ If needed, will cause a one-time read of the .bim file.
1078
+
1079
+ Example:
1080
+ -------
1081
+
1082
+ .. doctest::
1083
+
1084
+ >>> from bed_reader import open_bed, sample_file
1085
+ >>>
1086
+ >>> file_name = sample_file("small.bed")
1087
+ >>> with open_bed(file_name) as bed:
1088
+ ... print(bed.allele_1)
1089
+ ['A' 'T' 'A' 'T']
1090
+
1091
+ """
1092
+ return self.property_item("allele_1")
1093
+
1094
+ @property
1095
+ def allele_2(self) -> np.ndarray:
1096
+ """Second allele of each SNP (variant),.
1097
+
1098
+ Returns:
1099
+ -------
1100
+ numpy.ndarray
1101
+ array of str
1102
+
1103
+
1104
+ If needed, will cause a one-time read of the .bim file.
1105
+
1106
+ Example:
1107
+ -------
1108
+
1109
+ .. doctest::
1110
+
1111
+ >>> from bed_reader import open_bed, sample_file
1112
+ >>>
1113
+ >>> file_name = sample_file("small.bed")
1114
+ >>> with open_bed(file_name) as bed:
1115
+ ... print(bed.allele_2)
1116
+ ['A' 'C' 'C' 'G']
1117
+
1118
+ """
1119
+ return self.property_item("allele_2")
1120
+
1121
+ @property
1122
+ def iid_count(self) -> np.ndarray:
1123
+ """Number of individuals (samples).
1124
+
1125
+ Returns:
1126
+ -------
1127
+ int
1128
+ number of individuals
1129
+
1130
+
1131
+ If needed, will cause a fast line-count of the .fam file.
1132
+
1133
+ Example:
1134
+ -------
1135
+
1136
+ .. doctest::
1137
+
1138
+ >>> from bed_reader import open_bed, sample_file
1139
+ >>>
1140
+ >>> file_name = sample_file("small.bed")
1141
+ >>> with open_bed(file_name) as bed:
1142
+ ... print(bed.iid_count)
1143
+ 3
1144
+
1145
+ """
1146
+ return self._count("fam")
1147
+
1148
+ @property
1149
+ def sid_count(self) -> np.ndarray:
1150
+ """Number of SNPs (variants).
1151
+
1152
+ Returns:
1153
+ -------
1154
+ int
1155
+ number of SNPs
1156
+
1157
+
1158
+ If needed, will cause a fast line-count of the .bim file.
1159
+
1160
+ Example:
1161
+ -------
1162
+
1163
+ .. doctest::
1164
+
1165
+ >>> from bed_reader import open_bed, sample_file
1166
+ >>>
1167
+ >>> file_name = sample_file("small.bed")
1168
+ >>> with open_bed(file_name) as bed:
1169
+ ... print(bed.sid_count)
1170
+ 4
1171
+
1172
+ """
1173
+ return self._count("bim")
1174
+
1175
+ def _property_location(self, suffix):
1176
+ if suffix == "fam":
1177
+ return self._fam_location
1178
+ assert suffix == "bim" # real assert
1179
+ return self._bim_location
1180
+
1181
+ def _count(self, suffix):
1182
+ count = self._counts[suffix]
1183
+ if count is None:
1184
+ location = self._property_location(suffix)
1185
+ if open_bed._is_url(location):
1186
+ # should not download twice from cloud
1187
+ if suffix == "fam":
1188
+ if self.property_item("iid") is None:
1189
+ # ... unless user doesn't want iid
1190
+ file_bytes = bytes(
1191
+ url_to_bytes(location.geturl(), self.cloud_options),
1192
+ )
1193
+ count = _rawincount(BytesIO(file_bytes))
1194
+ else:
1195
+ count = len(self.iid)
1196
+ elif suffix == "bim":
1197
+ if self.property_item("sid") is None:
1198
+ # ... unless user doesn't want sid
1199
+ file_bytes = bytes(
1200
+ url_to_bytes(location.geturl(), self.cloud_options),
1201
+ )
1202
+ count = _rawincount(BytesIO(file_bytes))
1203
+ else:
1204
+ count = len(self.sid)
1205
+ else:
1206
+ msg = "real assert"
1207
+ raise ValueError(msg)
1208
+ else:
1209
+ with open(location, "rb") as f:
1210
+ count = _rawincount(f)
1211
+ self._counts[suffix] = count
1212
+ return count
1213
+
1214
+ @staticmethod
1215
+ def _check_file(filepointer):
1216
+ magic_number = filepointer.read(2)
1217
+ if magic_number != b"l\x1b":
1218
+ msg = "Not a valid .bed file"
1219
+ raise ValueError(msg)
1220
+ mode = filepointer.read(1)
1221
+ # Check if mode is either individual-major or SNP-major
1222
+ if mode not in (b"\x00", b"\x01"):
1223
+ msg = "Not a valid .bed file"
1224
+ raise ValueError(msg)
1225
+ return mode
1226
+
1227
+ def __del__(self) -> None:
1228
+ self.__exit__()
1229
+
1230
+ def __enter__(self):
1231
+ return self
1232
+
1233
+ def __exit__(self, *_):
1234
+ pass
1235
+
1236
+ @staticmethod
1237
+ def _array_properties_are_ok(val, order):
1238
+ if order == "F":
1239
+ return val.flags["F_CONTIGUOUS"]
1240
+ assert order == "C" # real assert
1241
+ return val.flags["C_CONTIGUOUS"]
1242
+
1243
+ @property
1244
+ def shape(self):
1245
+ """Number of individuals (samples) and SNPs (variants).
1246
+
1247
+ Returns:
1248
+ -------
1249
+ (int, int)
1250
+ number of individuals, number of SNPs
1251
+
1252
+
1253
+ If needed, will cause a fast line-count of the .fam and .bim files.
1254
+
1255
+ Example:
1256
+ -------
1257
+
1258
+ .. doctest::
1259
+
1260
+ >>> from bed_reader import open_bed, sample_file
1261
+ >>>
1262
+ >>> file_name = sample_file("small.bed")
1263
+ >>> with open_bed(file_name) as bed:
1264
+ ... print(bed.shape)
1265
+ (3, 4)
1266
+
1267
+ """
1268
+ return (self.iid_count, self.sid_count)
1269
+
1270
+ @staticmethod
1271
+ def _split_index(index):
1272
+ if not isinstance(index, tuple):
1273
+ index = (None, index)
1274
+ iid_index = open_bed._fix_up_index(index[0])
1275
+ sid_index = open_bed._fix_up_index(index[1])
1276
+ return iid_index, sid_index
1277
+
1278
+ @staticmethod
1279
+ def _fix_up_index(index):
1280
+ if index is None: # make a shortcut for None
1281
+ return slice(None)
1282
+ try: # If index is an int, return it in an array
1283
+ index = index.__index__() # (see
1284
+ # https://stackoverflow.com/questions/3501382/checking-whether-a-variable-is-an-integer-or-not)
1285
+ return [index]
1286
+ except Exception:
1287
+ pass
1288
+ return index
1289
+
1290
+ @staticmethod
1291
+ def _write_fam_or_bim(base_filepath, properties, suffix, property_filepath) -> None:
1292
+ assert suffix in {"fam", "bim"}, "real assert"
1293
+
1294
+ filepath = (
1295
+ Path(property_filepath)
1296
+ if property_filepath is not None
1297
+ else base_filepath.parent / (base_filepath.stem + "." + suffix)
1298
+ )
1299
+
1300
+ fam_bim_list = []
1301
+ for key, mm in _meta_meta.items():
1302
+ if mm.suffix == suffix:
1303
+ assert len(fam_bim_list) == mm.column, "real assert"
1304
+ fam_bim_list.append(properties[key])
1305
+
1306
+ sep = " " if suffix == "fam" else "\t"
1307
+
1308
+ with open(filepath, "w") as filepointer:
1309
+ for index in range(len(fam_bim_list[0])):
1310
+ filepointer.write(
1311
+ sep.join(str(seq[index]) for seq in fam_bim_list) + "\n",
1312
+ )
1313
+
1314
+ @staticmethod
1315
+ def _fix_up_properties_array(input, dtype, missing_value, key):
1316
+ if input is None:
1317
+ return None
1318
+ if len(input) == 0:
1319
+ return np.zeros([0], dtype=dtype)
1320
+
1321
+ if not isinstance(input, np.ndarray):
1322
+ return open_bed._fix_up_properties_array(
1323
+ np.array(input), dtype, missing_value, key,
1324
+ )
1325
+
1326
+ if len(input.shape) != 1:
1327
+ msg = f"{key} should be one dimensional"
1328
+ raise ValueError(msg)
1329
+
1330
+ do_missing_values = True
1331
+ if np.issubdtype(input.dtype, np.floating) and np.issubdtype(dtype, int):
1332
+ input[input != input] = missing_value
1333
+ old_settings = np.seterr(invalid="warn")
1334
+ try:
1335
+ output = np.array(input, dtype=dtype)
1336
+ finally:
1337
+ np.seterr(**old_settings)
1338
+ elif not np.issubdtype(input.dtype, dtype):
1339
+ # This will convert, for example, numerical sids to string sids or
1340
+ # floats that happen to be integers into ints,
1341
+ # but there will be a warning generated.
1342
+ old_settings = np.seterr(invalid="warn")
1343
+ try:
1344
+ output = np.array(input, dtype=dtype)
1345
+ finally:
1346
+ np.seterr(**old_settings)
1347
+ else:
1348
+ output = input
1349
+
1350
+ # Change NaN in input to correct missing value
1351
+ if do_missing_values and np.issubdtype(input.dtype, np.floating):
1352
+ output[input != input] = missing_value
1353
+
1354
+ return output
1355
+
1356
+ @staticmethod
1357
+ def _fix_up_properties(properties, iid_count, sid_count, use_fill_sequence):
1358
+ for key in properties:
1359
+ if key not in _meta_meta:
1360
+ msg = f"properties key '{key}' not known"
1361
+ raise KeyError(msg)
1362
+
1363
+ count_dict = {"fam": iid_count, "bim": sid_count}
1364
+ properties_dict = {}
1365
+ for key, mm in _meta_meta.items():
1366
+ count = count_dict[mm.suffix]
1367
+
1368
+ if key not in properties or (use_fill_sequence and properties[key] is None):
1369
+ if use_fill_sequence:
1370
+ output = mm.fill_sequence(key, count, mm.missing_value, mm.dtype)
1371
+ else:
1372
+ continue # Test coverage reaches this, but doesn't report it.
1373
+ else:
1374
+ output = open_bed._fix_up_properties_array(
1375
+ properties[key], mm.dtype, mm.missing_value, key,
1376
+ )
1377
+
1378
+ if output is not None:
1379
+ if count is None:
1380
+ count_dict[mm.suffix] = len(output)
1381
+ elif count != len(output):
1382
+ raise ValueError(
1383
+ f"The length of override {key}, {len(output)}, should not "
1384
+ + "be different from the current "
1385
+ + f"{_count_name[mm.suffix]}, {count}",
1386
+ )
1387
+ properties_dict[key] = output
1388
+ return properties_dict, count_dict
1389
+
1390
+ def _read_fam_or_bim(self, suffix) -> None:
1391
+ property_location = self._property_location(suffix)
1392
+
1393
+ logging.info(f"Loading {suffix} file {property_location}")
1394
+
1395
+ count = self._counts[suffix]
1396
+
1397
+ delimiter = _delimiters[suffix]
1398
+ if delimiter in {r"\s+"}:
1399
+ delimiter = None
1400
+
1401
+ usecolsdict = {}
1402
+ dtype_dict = {}
1403
+ for key, mm in _meta_meta.items():
1404
+ if mm.suffix is suffix and key not in self.properties_dict:
1405
+ usecolsdict[key] = mm.column
1406
+ dtype_dict[mm.column] = mm.dtype
1407
+ assert list(usecolsdict.values()) == sorted(usecolsdict.values()) # real assert
1408
+ assert len(usecolsdict) > 0 # real assert
1409
+
1410
+ if self._is_url(property_location):
1411
+ file_bytes = bytes(
1412
+ url_to_bytes(property_location.geturl(), self.cloud_options),
1413
+ )
1414
+ if len(file_bytes) == 0:
1415
+ columns, row_count = [], 0
1416
+ else: # note similar code below
1417
+ columns, row_count = _read_csv(
1418
+ BytesIO(file_bytes),
1419
+ delimiter=delimiter,
1420
+ dtype=dtype_dict,
1421
+ usecols=usecolsdict.values(),
1422
+ )
1423
+ elif os.path.getsize(property_location) == 0:
1424
+ columns, row_count = [], 0
1425
+ else:
1426
+ columns, row_count = _read_csv(
1427
+ property_location,
1428
+ delimiter=delimiter,
1429
+ dtype=dtype_dict,
1430
+ usecols=usecolsdict.values(),
1431
+ )
1432
+
1433
+ if count is None:
1434
+ self._counts[suffix] = row_count
1435
+ elif count != row_count:
1436
+ raise ValueError(
1437
+ f"The number of lines in the *.{suffix} file, {row_count}, "
1438
+ + "should not be different from the current "
1439
+ + "f{_count_name[suffix]}, {count}",
1440
+ )
1441
+ for i, key in enumerate(usecolsdict.keys()):
1442
+ mm = _meta_meta[key]
1443
+ if row_count == 0:
1444
+ output = np.array([], dtype=mm.dtype)
1445
+ else:
1446
+ output = columns[i]
1447
+ if not np.issubdtype(output.dtype, mm.dtype):
1448
+ output = np.array(output, dtype=mm.dtype)
1449
+ self.properties_dict[key] = output
1450
+
1451
+ def read_sparse(
1452
+ self,
1453
+ index: Optional[Any] = None,
1454
+ dtype: Optional[Union[type, str]] = "float32",
1455
+ batch_size: Optional[int] = None,
1456
+ format: Optional[str] = "csc",
1457
+ num_threads=None,
1458
+ max_concurrent_requests=None,
1459
+ max_chunk_bytes=None,
1460
+ ) -> (Union[sparse.csc_matrix, sparse.csr_matrix]) if sparse is not None else None: # type: ignore
1461
+ """Read genotype information into a :mod:`scipy.sparse` matrix. Sparse matrices
1462
+ may be useful when the data is mostly zeros.
1463
+
1464
+ .. note::
1465
+ This method requires :mod:`scipy`. Install `scipy` with:
1466
+
1467
+ .. code-block:: bash
1468
+
1469
+ pip install --upgrade bed-reader[sparse]
1470
+
1471
+ Parameters
1472
+ ----------
1473
+ index:
1474
+ An optional expression specifying the individuals (samples) and SNPs
1475
+ (variants) to read. (See examples, below).
1476
+ Defaults to ``None``, meaning read all.
1477
+
1478
+ (If index is a tuple, the first component indexes the individuals and the
1479
+ second indexes
1480
+ the SNPs. If it is not a tuple and not None, it indexes SNPs.)
1481
+
1482
+ dtype: {'float32' (default), 'float64', 'int8'}, optional
1483
+ The desired data-type for the returned array.
1484
+ batch_size: None or int, optional
1485
+ Number of dense columns or rows to read at a time, internally.
1486
+ Defaults to round(sqrt(total-number-of-columns-or-rows-to-read)).
1487
+ format : {'csc','csr'}, optional
1488
+ The desired format of the sparse matrix.
1489
+ Defaults to ``csc`` (Compressed Sparse Column, which is SNP-major).
1490
+ num_threads: None or int, optional
1491
+ The number of threads with which to read data. Defaults to all available
1492
+ processors.
1493
+ Can also be set with :class:`open_bed` or these
1494
+ environment variables (listed in priority order):
1495
+ 'PST_NUM_THREADS', 'NUM_THREADS', 'MKL_NUM_THREADS'.
1496
+ max_concurrent_requests: None or int, optional
1497
+ The maximum number of concurrent requests to make to the cloud storage
1498
+ service. Defaults to 10.
1499
+ max_chunk_bytes: None or int, optional
1500
+ The maximum number of bytes to read in a single request to the cloud
1501
+ storage service. Defaults to 8MB.
1502
+
1503
+
1504
+ Returns
1505
+ -------
1506
+ a :class:`scipy.sparse.csc_matrix` (default) or :class:`scipy.sparse.csr_matrix`
1507
+
1508
+ Rows represent individuals (samples). Columns represent SNPs (variants).
1509
+
1510
+ For ``dtype`` 'float32' and 'float64', NaN indicates missing values.
1511
+ For 'int8', -127 indicates missing values.
1512
+
1513
+
1514
+ The memory used by the final sparse matrix is approximately:
1515
+
1516
+ # of non-zero values * (4 bytes + 1 byte (for int8))
1517
+
1518
+ For example, consider reading 1000 individuals (samples) x 50,000 SNPs (variants)
1519
+ into csc format where the data is 97% sparse.
1520
+ The memory used will be about 7.5 MB (1000 x 50,000 x 3% x 5 bytes).
1521
+ This is 15% of the 50 MB needed by a dense matrix.
1522
+
1523
+ Internally, the function reads the data via small dense matrices.
1524
+ For this example, by default, the function will read 1000 individuals x 224 SNPs
1525
+ (because 224 * 224 is about 50,000).
1526
+ The memory used by the small dense matrix is 1000 x 244 x 1 byte (for int8) = 0.224 MB.
1527
+
1528
+ You can set `batch_size`. Larger values will be faster.
1529
+ Smaller values will use less memory.
1530
+
1531
+ For this example, we might want to set the `batch_size` to 5000. Then,
1532
+ the memory used by the small dense matrix
1533
+ would be 1000 x 5000 x 1 byte (for int8) = 5 MB,
1534
+ similar to the 7.5 MB needed for the final sparse matrix.
1535
+
1536
+ Examples
1537
+ --------
1538
+ Read all data in a .bed file into a :class:`scipy.sparse.csc_matrix`.
1539
+ The file has 10 individuals (samples) by 20 SNPs (variants).
1540
+ All but eight values are 0.
1541
+
1542
+ .. doctest::
1543
+
1544
+ >>> # pip install bed-reader[samples,sparse] # if needed
1545
+ >>> from bed_reader import open_bed, sample_file
1546
+ >>>
1547
+ >>> file_name = sample_file("sparse.bed")
1548
+ >>> with open_bed(file_name) as bed:
1549
+ ... print(bed.shape) # doctest:+NORMALIZE_WHITESPACE +ELLIPSIS
1550
+ ... val_sparse = bed.read_sparse(dtype="int8")
1551
+ (10, 20)
1552
+ >>> print("Nonzero Values", val_sparse.data) # doctest:+NORMALIZE_WHITESPACE +ELLIPSIS
1553
+ Nonzero Values [1 2 2 1 1 1 1 1]
1554
+
1555
+ To read selected individuals (samples) and/or SNPs (variants), set each part of
1556
+ a :data:`numpy.s_` to an `int`, a list of `int`, a slice expression, or
1557
+ a list of `bool`.
1558
+ Negative integers count from the end of the list.
1559
+
1560
+ .. doctest::
1561
+
1562
+ >>> import numpy as np
1563
+ >>> bed = open_bed(file_name)
1564
+ >>> print("Nonzero Values", bed.read_sparse(np.s_[:,5], dtype="int8").data) # read the SNPs indexed by 5.
1565
+ Nonzero Values [2]
1566
+ >>> # read the SNPs indexed by 5, 4, and 0
1567
+ >>> print("Nonzero Values", bed.read_sparse(np.s_[:,[5,4,0]], dtype="int8").data)
1568
+ Nonzero Values [2 1]
1569
+ >>> # read SNPs from 1 (inclusive) to 11 (exclusive)
1570
+ >>> print("Nonzero Values", bed.read_sparse(np.s_[:,1:11], dtype="int8").data)
1571
+ Nonzero Values [1 2 2 1 1]
1572
+ >>> print(np.unique(bed.chromosome)) # print unique chrom values
1573
+ ['1' '5' 'Y']
1574
+ >>> # read all SNPs in chrom 5
1575
+ >>> print("Nonzero Values", bed.read_sparse(np.s_[:,bed.chromosome=='5'], dtype="int8").data)
1576
+ Nonzero Values [1 2 2 1 1 1 1 1]
1577
+ >>> # Read 1st individual (across all SNPs)
1578
+ >>> print("Nonzero Values", bed.read_sparse(np.s_[0,:], dtype="int8").data)
1579
+ Nonzero Values [2]
1580
+ >>> print("Nonzero Values", bed.read_sparse(np.s_[::2,:], dtype="int8").data) # Read every 2nd individual
1581
+ Nonzero Values [1 2 2 1 1]
1582
+ >>> # read last and 2nd-to-last individuals and the 15th-from-the-last SNP
1583
+ >>> print("Nonzero Values", bed.read_sparse(np.s_[[-1,-2],-15], dtype="int8").data)
1584
+ Nonzero Values [2]
1585
+
1586
+ """
1587
+ if sparse is None:
1588
+ raise ImportError(
1589
+ "The function read_sparse() requires scipy. "
1590
+ + "Install it with 'pip install --upgrade bed-reader[sparse]'.",
1591
+ )
1592
+ iid_index_or_slice_etc, sid_index_or_slice_etc = self._split_index(index)
1593
+
1594
+ dtype = np.dtype(dtype)
1595
+
1596
+ # Similar code in read().
1597
+ # Later happy with _iid_range and _sid_range or could it be done with
1598
+ # allocation them?
1599
+ if self._iid_range is None:
1600
+ self._iid_range = np.arange(self.iid_count, dtype="intp")
1601
+ if self._sid_range is None:
1602
+ self._sid_range = np.arange(self.sid_count, dtype="intp")
1603
+
1604
+ iid_index = np.ascontiguousarray(
1605
+ self._iid_range[iid_index_or_slice_etc],
1606
+ dtype="intp",
1607
+ )
1608
+ sid_index = np.ascontiguousarray(
1609
+ self._sid_range[sid_index_or_slice_etc], dtype="intp",
1610
+ )
1611
+
1612
+ if (
1613
+ len(iid_index) > np.iinfo(np.int32).max
1614
+ or len(sid_index) > np.iinfo(np.int32).max
1615
+ ):
1616
+ msg = (
1617
+ "Too many Individuals or SNPs (variants) requested. "
1618
+ "Maximum is {np.iinfo(np.int32).max}."
1619
+ )
1620
+ raise ValueError(
1621
+ msg,
1622
+ )
1623
+
1624
+
1625
+ if batch_size is None:
1626
+ batch_size = round(np.sqrt(len(sid_index)))
1627
+
1628
+ num_threads = get_num_threads(
1629
+ self._num_threads if num_threads is None else num_threads,
1630
+ )
1631
+ max_concurrent_requests = get_max_concurrent_requests(
1632
+ self._max_concurrent_requests
1633
+ if max_concurrent_requests is None
1634
+ else max_concurrent_requests,
1635
+ )
1636
+ max_chunk_bytes = get_max_chunk_bytes(
1637
+ self._max_chunk_bytes if max_chunk_bytes is None else max_chunk_bytes,
1638
+ )
1639
+
1640
+ if format == "csc":
1641
+ order = "F"
1642
+ indptr = np.zeros(len(sid_index) + 1, dtype=np.int32)
1643
+ elif format == "csr":
1644
+ order = "C"
1645
+ indptr = np.zeros(len(iid_index) + 1, dtype=np.int32)
1646
+ else:
1647
+ msg = f"format '{format}' not known. Expected 'csc' or 'csr'."
1648
+ raise ValueError(msg)
1649
+
1650
+ # We init data and indices with zero element arrays to set their dtype.
1651
+ data = [np.empty(0, dtype=dtype)]
1652
+ indices = [np.empty(0, dtype=np.int32)]
1653
+
1654
+ if self.iid_count > 0 and self.sid_count > 0:
1655
+ reader, location_str, is_cloud = self._pick_reader(dtype)
1656
+
1657
+ if format == "csc":
1658
+ val = np.zeros((len(iid_index), batch_size), order=order, dtype=dtype)
1659
+ for batch_start in range(0, len(sid_index), batch_size):
1660
+ batch_end = batch_start + batch_size
1661
+ if batch_end > len(sid_index):
1662
+ batch_end = len(sid_index)
1663
+ del val
1664
+ val = np.zeros(
1665
+ (len(iid_index), batch_end - batch_start),
1666
+ order=order,
1667
+ dtype=dtype,
1668
+ )
1669
+ batch_slice = np.s_[batch_start:batch_end]
1670
+ batch_index = sid_index[batch_slice]
1671
+
1672
+ if not is_cloud:
1673
+ reader(
1674
+ location_str,
1675
+ self.cloud_options,
1676
+ iid_count=self.iid_count,
1677
+ sid_count=self.sid_count,
1678
+ is_a1_counted=self.count_A1,
1679
+ iid_index=iid_index,
1680
+ sid_index=batch_index,
1681
+ val=val,
1682
+ num_threads=num_threads,
1683
+ )
1684
+ else:
1685
+ reader(
1686
+ location_str,
1687
+ self.cloud_options,
1688
+ iid_count=self.iid_count,
1689
+ sid_count=self.sid_count,
1690
+ is_a1_counted=self.count_A1,
1691
+ iid_index=iid_index,
1692
+ sid_index=batch_index,
1693
+ val=val,
1694
+ num_threads=num_threads,
1695
+ max_concurrent_requests=max_concurrent_requests,
1696
+ max_chunk_bytes=max_chunk_bytes,
1697
+ )
1698
+
1699
+ self.sparsify(
1700
+ val, order, iid_index, batch_slice, data, indices, indptr,
1701
+ )
1702
+ else:
1703
+ assert format == "csr" # real assert
1704
+ val = np.zeros((batch_size, len(sid_index)), order=order, dtype=dtype)
1705
+ for batch_start in range(0, len(iid_index), batch_size):
1706
+ batch_end = batch_start + batch_size
1707
+ if batch_end > len(iid_index):
1708
+ batch_end = len(iid_index)
1709
+ del val
1710
+ val = np.zeros(
1711
+ (batch_end - batch_start, len(sid_index)),
1712
+ order=order,
1713
+ dtype=dtype,
1714
+ )
1715
+
1716
+ batch_slice = np.s_[batch_start:batch_end]
1717
+ batch_index = iid_index[batch_slice]
1718
+
1719
+ if not is_cloud:
1720
+ reader(
1721
+ location_str,
1722
+ self.cloud_options,
1723
+ iid_count=self.iid_count,
1724
+ sid_count=self.sid_count,
1725
+ is_a1_counted=self.count_A1,
1726
+ iid_index=batch_index,
1727
+ sid_index=sid_index,
1728
+ val=val,
1729
+ num_threads=num_threads,
1730
+ )
1731
+ else:
1732
+ reader(
1733
+ location_str,
1734
+ self.cloud_options,
1735
+ iid_count=self.iid_count,
1736
+ sid_count=self.sid_count,
1737
+ is_a1_counted=self.count_A1,
1738
+ iid_index=batch_index,
1739
+ sid_index=sid_index,
1740
+ val=val,
1741
+ num_threads=num_threads,
1742
+ max_concurrent_requests=max_concurrent_requests,
1743
+ max_chunk_bytes=max_chunk_bytes,
1744
+ )
1745
+
1746
+ self.sparsify(
1747
+ val, order, sid_index, batch_slice, data, indices, indptr,
1748
+ )
1749
+
1750
+ data = np.concatenate(data)
1751
+ indices = np.concatenate(indices)
1752
+
1753
+ if format == "csc":
1754
+ return sparse.csc_matrix(
1755
+ (data, indices, indptr), (len(iid_index), len(sid_index)),
1756
+ )
1757
+ assert format == "csr" # real assert
1758
+ return sparse.csr_matrix(
1759
+ (data, indices, indptr), (len(iid_index), len(sid_index)),
1760
+ )
1761
+
1762
+ def sparsify(self, val, order, minor_index, batch_slice, data, indices, indptr) -> None:
1763
+ flatten = np.ravel(val, order=order)
1764
+ nz_indices = np.flatnonzero(flatten).astype(np.int32)
1765
+ column_indexes = nz_indices // len(minor_index)
1766
+ counts = np.bincount(
1767
+ column_indexes, minlength=batch_slice.stop - batch_slice.start,
1768
+ ).astype(np.int32)
1769
+ counts_with_initial = np.r_[
1770
+ indptr[batch_slice.start : batch_slice.start + 1], counts,
1771
+ ]
1772
+
1773
+ data.append(flatten[nz_indices])
1774
+ indices.append(np.mod(nz_indices, len(minor_index)))
1775
+ indptr[1:][batch_slice] = np.cumsum(counts_with_initial)[1:]
1776
+
1777
+
1778
+ def _read_csv(filepath, delimiter=None, dtype=None, usecols=None):
1779
+ pattern = re.compile(r"^np\.\w+\((.+?)\)$")
1780
+
1781
+ # Prepare the usecols by ensuring it is a list of indices
1782
+ usecols_indices = list(usecols)
1783
+ transposed = np.loadtxt(
1784
+ filepath,
1785
+ dtype=np.str_,
1786
+ delimiter=delimiter,
1787
+ usecols=usecols_indices,
1788
+ unpack=True,
1789
+ )
1790
+ if transposed.ndim == 1:
1791
+ transposed = transposed.reshape(-1, 1)
1792
+ row_count = transposed.shape[1] # because unpack=True
1793
+
1794
+ # Convert column lists to numpy arrays with the specified dtype
1795
+ columns = []
1796
+ for output_index, input_index in enumerate(usecols_indices):
1797
+ col = transposed[output_index]
1798
+
1799
+ # work around numpy/python bug
1800
+ if len(col) > 0 and pattern.fullmatch(col[0]):
1801
+ col = np.array([pattern.fullmatch(x).group(1) for x in col])
1802
+
1803
+ # Find the dtype for this column
1804
+ col_dtype = dtype.get(input_index, np.str_)
1805
+ # Convert the column list to a numpy array with the specified dtype
1806
+ columns.append(_convert_to_dtype(col, col_dtype))
1807
+
1808
+ return columns, row_count
1809
+
1810
+
1811
+ def _convert_to_dtype(str_arr, dtype):
1812
+ assert dtype in [np.str_, np.float32, np.int32] # real assert
1813
+
1814
+ if dtype == np.str_:
1815
+ return str_arr
1816
+
1817
+ try:
1818
+ new_arr = str_arr.astype(dtype)
1819
+ except ValueError as e:
1820
+ if dtype == np.float32:
1821
+ raise
1822
+ # for backwards compatibility, see if intermediate float helps int conversion
1823
+ try:
1824
+ assert dtype == np.int32 # real assert
1825
+ float_arr = str_arr.astype(np.float32)
1826
+ except ValueError:
1827
+ raise e
1828
+ new_arr = float_arr.astype(np.int32)
1829
+ if not np.array_equal(new_arr, float_arr):
1830
+ msg = f"invalid literal for int: '{str_arr[np.where(new_arr != float_arr)][:1]}')"
1831
+ raise ValueError(
1832
+ msg,
1833
+ )
1834
+ return new_arr
1835
+
1836
+
1837
+ if __name__ == "__main__":
1838
+ import pytest
1839
+
1840
+ logging.basicConfig(level=logging.INFO)
1841
+
1842
+ pytest.main(["--doctest-modules", __file__])