-
Notifications
You must be signed in to change notification settings - Fork 13
/
setas.py
executable file
·859 lines (758 loc) · 32.2 KB
/
setas.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""setas module provides the setas class for DataFile and friends."""
__all__ = ["setas"]
import re
import copy
from collections.abc import MutableMapping, Mapping
import numpy as np
from ..compat import string_types, int_types, index_types, _pattern_type
from ..tools import AttributeStore, isiterable, typedList, isLikeList
from .utils import decode_string
class setas(MutableMapping):
"""A Class that provides a mechanism for managing the column assignments in a DataFile like object.
Implements a MutableMapping bsed on the column_headers as the keys (with a few tweaks!).
Note:
Iterating over setas will return the column assignments rather than the standard mapping behaviour of
iterating over the keys. Otherwise
the interface is essentially as a Mapping class.
Calling an existing setas instance and the constructor share the same signatgure:
setas("xyzuvw")
setas(["x"],["y"],["z"],["u"],["v"],["w"])
setas(x="column_1",y=3,column4="z")
Keyword Arguments:
_self (bool):
If True, make the call return a copy of the setas object, if False, return _object attribute, if None,
return None
reset (bool):
If False then preserve the existing set columns and simply add the new ones. Otherwise, clear all column
assignments before setting new ones (default).
"""
def __init__(self, row=False, bless=None):
"""Construct the setas instance and sets an initial value.
Args:
ref (DataFile): Contains a reference to the owning DataFile instance
Keyword Arguments:
initial_val (string or list or dict): Initial values to set
"""
self._row = row
self._cols = AttributeStore()
self._shape = tuple()
self._setas = list()
self._column_headers = typedList(string_types)
self._object = bless
self._col_defaults = {
2: {
"axes": 2,
"xcol": 0,
"ycol": [1],
"zcol": [],
"ucol": [],
"vcol": [],
"wcol": [],
"xerr": None,
"yerr": [],
"zerr": [],
}, # xy
3: {
"axes": 2,
"xcol": 0,
"ycol": [1],
"zcol": [],
"ucol": [],
"vcol": [],
"wcol": [],
"xerr": None,
"yerr": [2],
"zerr": [],
}, # xye
4: {
"axes": 2,
"xcol": 0,
"ycol": [2],
"zcol": [],
"ucol": [],
"vcol": [],
"wcol": [],
"xerr": 1,
"yerr": [3],
"zerr": [],
}, # xdye
5: {
"axes": 5,
"xcol": 0,
"ycol": [1],
"zcol": None,
"ucol": [2],
"vcol": [3],
"wcol": [4],
"xerr": None,
"yerr": [],
"zerr": [],
}, # xyuvw
6: {
"axes": 6,
"xcol": 0,
"ycol": [1],
"zcol": [2],
"ucol": [3],
"vcol": [4],
"wcol": [5],
"xerr": None,
"yerr": [],
"zerr": [],
},
} # xyzuvw
def _prepare_call(self, args, kargs):
"""Extract a value to be used to evaluate the setas attribute during a call."""
reset = kargs.pop("reset", True)
if not isinstance(reset, bool):
reset = True
if args:
value = args[0]
if isinstance(value, string_types): # expand the number-code combos in value
if reset:
self.setas = []
value = decode_string(value)
else:
value = kargs
if reset:
self.setas = []
return value
@property
def _size(self):
"""Calculate a size of the setas attribute."""
if len(self._shape) == 1 and self._row:
c = self._shape[0]
elif len(self._shape) == 1:
c = 1
elif len(self._shape) > 1:
c = self.shape[1]
else:
c = len(self._column_headers)
return c
@property
def _unique_headers(self):
"""Return either a column header or an index if the column_header is duplicated."""
ret = []
for i, ch in enumerate(self.column_headers):
if ch not in ret:
ret.append(ch)
else:
ret.append(i)
return ret
@property
def clone(self):
"""Create an exact copy of the current object."""
cls = type(self)
new = cls()
for attr in self.__dict__:
if not callable(self.__dict__[attr]):
new.__dict__[attr] = copy.deepcopy(self.__dict__[attr])
return new
@property
def cols(self):
"""Get the current column assignments."""
self._cols.update(self._get_cols())
return self._cols
@property
def x(self):
"""Quick access to the x column number.
Just a convenience read only property. If we want to change the setas.x
value we should use the setas(x=1,y=2) style call (so that reset can
be handled properly)
"""
return self.cols["xcol"]
@property
def y(self):
"""Quick access to the y column numbers list."""
return self.cols["ycol"]
@property
def z(self):
"""Quick access to the z column numbers list."""
return self.cols["zcol"]
@property
def column_headers(self):
"""Get the current column headers."""
cols = self._size
length = len(self._column_headers)
if length < cols: # Extend the column headers if necessary
self._column_headers.extend([f"Column {i + length}" for i in range(cols - length)])
return self._column_headers
@column_headers.setter
def column_headers(self, value):
"""Set the column headers."""
if isinstance(value, np.ndarray): # Convert ndarray to list of strings
value = value.astype(str).tolist()
elif isinstance(value, string_types): # Bare strings get turned into lists
value = [value]
self._column_headers = typedList(string_types, value)
@property
def empty(self):
"""Determine if any columns are set."""
return len(self._setas) == 0 or np.all(np.array(self._setas) == ".")
@property
def ndim(self):
"""Return the number of dimensions of the array."""
return len(self.shape)
@property
def not_set(self):
"""Return a boolean array if not set."""
return np.array([x == "." for x in self._setas])
@property
def set(self):
"""Return a boolean array if column is set."""
return np.array([x != "." for x in self._setas])
@property
def setas(self):
"""Guard the setas attribute."""
cols = self._size
length = len(self._setas)
if cols > length:
self._setas.extend(["."] * (cols - length))
self._setas = self._setas[:cols]
return self._setas
@setas.setter
def setas(self, value):
"""Minimal attribute setter."""
self._setas = value
@property
def shape(self):
"""Return the shape of the array that we think we are."""
return self._shape
@shape.setter
def shape(self, value):
"""Update the note of our shape."""
value = tuple(value)
if 0 <= len(value) <= 2:
self._shape = tuple(value)
else:
raise AttributeError(f"shape attribute should be a 2-tuple not a {value}-tuple")
def __call__(self, *args, **kargs):
"""Treat the current instance as a callable object and assign columns accordingly.
Variois forms of this method are accepted::
setas("xyzuvw")
setas(["x"],["y"],["z"],["u"],["v"],["w"])
setas(x="column_1",y=3,column4="z")
Keyword Arguments:
_self (bool):
If True, make the call return a copy of the setas object, if False, return _object attribute, if None,
return None. Default - **False**
reset (bool):
If False then preserve the existing set columns and simply add the new ones. Otherwise, clear
all column assignments before setting new ones (default).
"""
return_self = kargs.pop("_self", False)
if not (args or kargs): # New - bare call to setas will return the current value.
return self.setas
if len(args) == 1 and isinstance(args[0], setas):
args = list(args)
args[0] = args[0].to_list()
if len(args) == 1 and not (isinstance(args[0], string_types + (setas,)) or isiterable(args[0])):
raise SyntaxError(
f"setas should be called with eother a string, iterable object or setas object, not a {type(args[0])}"
)
# If reset is neither in kargs nor a False boolean, then clear the existing setas assignments
value = self._prepare_call(args, kargs)
_ = self.setas # Forxce setas to be the right length
if isinstance(value, dict):
for k, v in value.items():
if isinstance(k, string_types) and len(k) == 1 and k in "xyzuvwdef": # of the form x:column_name
for v_item in self.find_col(v, force_list=True):
try:
self._setas[v_item] = k
except (IndexError, KeyError):
pass
elif (
isinstance(k, index_types) and isinstance(v, string_types) and len(v) == 1 and v in "xyzuvwdef"
): # of the form column_name:x
k = self.find_col(k)
self._setas[k] = v
else:
raise IndexError(f"Unable to workout what do with {k}:{v} when setting the setas attribute.")
elif isiterable(value):
if len(value) > self._size:
value = value[: self._size]
elif len(value) < self._size:
value = [v for v in value] # Ensure value is now a list
value.extend(list("." * (self._size - len(value))))
value = value[: self._size]
for i, v in enumerate(value):
if v.lower() not in "xyzedfuvw.-":
raise ValueError(f"Set as column element is invalid: {v}")
if v != "-":
self.setas[i] = v.lower()
else:
raise ValueError("Set as column string ended with a number")
self.cols.update(self._get_cols())
if return_self is None:
return None
if return_self:
return self
return self._object
def __contains__(self, item):
"""Use getitem to test for membership. Either column assignments or column index types are tested."""
try:
_ = self[item]
except (IndexError, KeyError, ValueError):
return False
return True
def __delitem__(self, name):
"""Unset either by column index or column assignment.
Equivalent to unsetting the same object."""
self.unset(name)
def __eq__(self, other):
"""Check to see if this is the same object, or has the same headers and the same setas values."""
ret = False
if isinstance(other, string_types): # Expand strings and convert to list
other = [c for c in decode_string(other)]
if not isinstance(other, setas): # Ok, need to check whether items match
if isiterable(other) and len(other) <= self._size:
for m in self.setas[len(other) :]: # Check that if other is short we don't have assignments there
if m != ".":
return False
for o, m in zip(other, self.setas):
if o != m: # Look for mis-matched assignments
return False
return True
return False
if id(self) == id(other):
ret = True
else:
ret = self.column_headers == other.column_headers and self.setas == other.setas
return ret
def __getattr__(self, name):
"""Try to see if attribute name is a key in self.cols and return that instead."""
if name != "_cols" and name in self._cols:
return self._cols[name]
return getattr(super(), name)
def __getitem__(self, name):
"""Permit the setas attribute to be treated like either a list or a dictionary.
Args:
name (int, slice or string): if *name* is an integer or a slice, return the column type
of the corresponding column(s). If a string, should be a single letter
from the set x,y,z,u,v,w,d,e,f - if so returns the corresponding
column(s)
Returns:
Either a single letter x,y,z,u,v,w,d,e or f, or a list of letters if used in
list mode, or a single coliumn name or list of names if used in dictionary mode.
"""
if isinstance(name, string_types) and len(name) == 1 and name in "xyzuvwdef.-":
ret = self.to_dict()[name]
if len(ret) == 1:
ret = ret[0]
elif isinstance(name, string_types) and len(name) == 2 and name[0] == "#" and name[1] in "xyzuvwdef.-":
ret = list()
name = name[1]
s = 0
while name in self._setas[s:]:
s = self._setas.index(name) + 1
ret.append(s - 1)
if len(ret) == 1:
ret = ret[0]
elif isinstance(name, index_types):
ret = self.setas[self.find_col(name)]
elif isinstance(name, slice):
indices = name.indices(len(self.setas))
name = range(*indices)
ret = [self[x] for x in name]
elif isiterable(name):
ret = [self[x] for x in name]
else:
raise IndexError(f"{name} was not found in the setas attribute.")
return ret
def __iter__(self):
"""Iterate over thew column assignments.
.. warn::
This class does not follow standard Mapping semantics - iterating iterates over the values and not
the items.
"""
_ = self.setas # Force setas to fix size
for c in self._setas:
yield c
def __ne__(self, other):
"""!= is the same as no ==."""
return not self.__eq__(other)
def __setitem__(self, name, value):
"""Allow setting of the setas variable like a dictionary or a list.
Args:
name (string or int): If name is a string, it should be in the set x,y,z,u,v,w,d,e or f
and value should be a column index type. If name is an integer, then value should be
a single letter string in the set above.
value (integer or column index): See above.
"""
if isLikeList(name): # Sipport indexing with a list like object
if isLikeList(value) and len(value) == len(name):
for n, v in zip(name, value):
self._setas[n] = v
else:
for n in name:
self[n] = value
elif isinstance(name, string_types) and len(name) == 1 and name in "xyzuvwdef.-": # indexing by single letter
for c in self.find_col(value, force_list=True):
self._setas[c] = name
elif (
isinstance(name, index_types)
and isinstance(value, string_types)
and len(value) == 1
and value in "xyzuvwdef.-"
):
for c in self.find_col(name, force_list=True):
self.setas[c] = value
else:
raise IndexError(f"Failed to set setas as couldn't workout what todo with setas[{name}] = {value}")
def __len__(self):
"""Return our own length."""
return self._size
def __repr__(self):
"""Our representation is as a list of the values."""
return self.setas.__repr__()
def __str__(self):
"""Our string representation is just formed by joing the assignments together."""
# Quick string conversion routine
return "".join(self.setas)
#################################################################################################################
############################# Operator Methods ################################################################
def _add_core_(self, new, other):
"""Allow the user to add a dictionary to setas to add extra columns."""
if not isinstance(other, dict):
try:
tmp = self.clone
tmp(other)
other = tmp.to_dict()
except (TypeError, SyntaxError):
return NotImplemented
for k, v in other.items():
if isinstance(k, string_types) and len(k) == 1 and k in "xyzuvwdef": # of the form x:column_name
for v in new.find_col(v, force_list=True):
new._setas[v] = k
elif (
isinstance(k, index_types) and isinstance(v, string_types) and len(v) == 1 and v in "xyzuvwdef"
): # of the form column_name:x
k = new.find_col(k)
new._setas[k] = v
else:
raise IndexError(f"Unable to workout what do with {k}:{v} when setting the setas attribute.")
return new
def __add__(self, other):
"""Jump to the core."""
new = self.clone
return self._add_core_(new, other)
def __iadd__(self, other):
"""Jump to the core."""
new = self
return self._add_core_(new, other)
def _sub_core_(self, new, other):
"""Implement subtracting either column indices or x,y,z,d,e,f,u,v,w for the current setas."""
if isinstance(other, string_types) and len(other) == 1 and other in "xyzuvwdef":
while True:
try:
new._setas[new._setas.index(other)] = "."
except ValueError:
break
return new
if isinstance(other, index_types):
try:
new._setas[new.find_col(other)] = "."
return new
except KeyError:
other = new.clone(other, _self=True)
if isinstance(other, Mapping):
me = new.to_dict()
other = new.clone(other, _self=True).to_dict()
for k, v in other.items():
v = [v] if not isinstance(v, list) else v
if k in me:
for header in v:
if header in me[k]:
if isinstance(me[k], list):
me[k].remove(header)
else:
me[k] = ""
else:
raise ValueError(f"{header} is not set as {k}")
if len(me[k]) == 0:
del me[k]
else:
raise ValueError(f"No column is set as {k}")
new.clear()
new(me)
return new
if isiterable(other):
for o in other:
new = self._sub_core_(new, o)
if new is NotImplemented:
return NotImplemented
return new
return NotImplemented
def __sub__(self, other):
"""Jump to the core."""
new = self.clone
return self._sub_core_(new, other)
def __isub__(self, other):
"""Jump to the core."""
new = self
return self._sub_core_(new, other)
def find_col(self, col, force_list=False):
"""Indexes the column headers in order to locate a column of data.shape.
Indexing can be by supplying an integer, a string, a regular expression, a slice or a list of any of the above.
- Integer indices are simply checked to ensure that they are in range
- String indices are first checked for an exact match against a column header
if that fails they are then compiled to a regular expression and the first
match to a column header is taken.
- A regular expression index is simply matched against the column headers and the
first match found is taken. This allows additional regular expression options
such as case insensitivity.
- A slice index is converted to a list of integers and processed as below
- A list index returns the results of feading each item in the list at :py:meth:`find_col`
in turn.
Args:
col (int, a string, a re, a slice or a list): Which column(s) to retuirn indices for.
Keyword Arguments:
force_list (bool): Force the output always to be a list. Mainly for internal use only
Returns:
The matching column index as an integer or a KeyError
"""
if isinstance(col, int_types): # col is an int so pass on
if col >= len(self.column_headers):
raise IndexError(f"Attempting to index a non - existent column {col}")
if col < 0:
col = col % len(self.column_headers)
elif isinstance(col, string_types): # Ok we have a string
col = str(col)
if col in self.column_headers: # and it is an exact string match
col = self.column_headers.index(col)
else: # ok we'll try for a regular expression
test = re.compile(col)
possible = [x for x in self.column_headers if test.search(x)]
if not possible:
try:
col = int(col)
except ValueError as err:
raise KeyError(
f'Unable to find any possible column matches for "{col} in {self.column_headers}"'
) from err
if col < 0 or col >= self.data.shape[1]:
raise KeyError("Column index out of range")
else:
col = self.column_headers.index(possible[0])
elif isinstance(col, _pattern_type):
test = col
possible = [x for x in self.column_headers if test.search(x)]
if not possible:
raise KeyError(f"Unable to find any possible column matches for {col.pattern}")
col = self.find_col(possible)
elif isinstance(col, slice):
indices = col.indices(self.shape[1])
col = range(*indices)
col = self.find_col(col)
elif isiterable(col):
col = [self.find_col(x) for x in col]
else:
raise TypeError(f"Column index must be an integer, string, list or slice, not a {type(col)}")
if force_list and not isinstance(col, list):
col = [col]
return col
def clear(self):
"""Clear the current setas attribute.
Notes:
Equivalent to doing :py:meth:`setas.unset` with no argument.
"""
self.unset()
def get(self, key, default=None): # pylint: disable=arguments-differ
"""Implement a get method."""
try:
return self[key]
except (IndexError, KeyError) as err:
if default is not None:
return default
raise KeyError(f"{key} is not in setas and no default was given.") from err
def keys(self):
"""Access mapping keys.
Mapping keys are the same as iterating over the unique headers"""
for c in self._unique_headers:
yield c
def values(self):
"""Access mapping values.
Mapping values are the same as iterating over setas."""
for v in self.setas:
yield v
def items(self):
"""Access mapping items.
Mapping items iterates over keys and values."""
for k, v in zip(self._unique_headers, self.setas):
yield k, v
def pop(self, key, default=None): # pylint: disable=arguments-differ
"""Implement a get method."""
try:
ret = self[key]
self.unset(key)
return ret
except (IndexError, KeyError) as err:
if default is not None:
return default
raise KeyError(f"{key} is not in setas and no default was given.") from err
def popitem(self):
"""Return and clear a column assignment."""
for c in "xdyezfuvw":
if c in self:
v = self[c]
self.unset(c)
return (c, v)
raise KeyError("No columns set in setas!")
def setdefault(self, key, default=None): # pylint: disable=arguments-differ
"""Implement a setdefault method."""
try:
return self[key]
except (IndexError, KeyError):
self[key] = default
return default
def unset(self, what=None):
"""Remove column settings from the setas attribute in method call.
Parameters:
what (str,iterable,dict or None): What to unset.
Notes:
The *what* parameter determines what to unset, possible values are:
- A single lets from *xyzuvwdef* - all column assignments of the corresponding type are unset
- A column index type - all matching columns are unset
- A list or other iterable of the above - all matching entries are unset
- None - all setas assignments are cleared.
"""
if what is None:
self.setas = []
_ = self.setas
else:
self -= what
def update(self, other=(), **kwds): # pylint: disable=arguments-differ
"""Replace any assignments in self with assignments from other."""
if isinstance(other, setas):
other = other.to_dict()
elif isinstance(other, tuple) and len(other) == 0:
other = kwds
else:
try:
other = dict(other)
except (ValueError, TypeError) as err:
raise TypeError(f"setas.update requires a dictionary not a {type(other)}") from err
vals = list(other.values())
keys = list(other.keys())
for k in "xyzuvwdef":
if k in other:
try:
c = self[k]
self[c] = "."
except (KeyError, IndexError):
pass
self[k] = other[k]
elif k in vals:
try:
c = self[k]
self[c] = "."
except IndexError:
pass
self[k] = keys[vals.index(k)]
return self
def to_dict(self):
"""Return the setas attribute as a dictionary.
If multiple columns are assigned to the same type, then the column names are
returned as a list. If column headers are duplicated"""
ret = {}
for k, ch in zip(self._setas, self._unique_headers):
if k != ".":
if k in ret:
ret[k].append(ch)
else:
ret[k] = [ch]
for k in ret:
if len(ret[k]) == 1:
ret[k] = ret[k][0]
return ret
def to_list(self):
"""Return the setas attribute as a list of letter types."""
return list(self)
def to_string(self, encode=False):
"""Return the setas attribute encoded as a string.
Optionally replaces runs of 3 or more identical characters with a precediung digit."""
expanded = "".join(self)
if encode:
pat = re.compile(r"((.)\2{2,9})")
while True:
res = pat.search(expanded)
if not res:
break
start, stop = res.span()
let = str(stop - start) + res.group(2)
expanded = expanded[:start] + let + expanded[stop:]
return expanded
def _get_cols(self, what=None, startx=0, no_guess=False):
"""Use the setas attribute to work out which columns to use for x,y,z etc.
Keyword Arguments:
what (string): Returns either xcol, ycol, zcol, ycols,xcols rather than the full dictionary
startx (int): Start looking for x columns at this column.
Returns:
A single integer, a list of integers or a dictionary of all columns.
"""
# Do the xcolumn and xerror first. If only one x column then special case to reset startx to get any
# y columns
if self.setas.count("x") == 1:
xcol = self.setas.index("x")
maxcol = len(self.setas) + 1
startx = 0
xerr = self.setas.index("d") if "d" in self.setas else None
elif self.setas.count("x") > 1:
xcol = self.setas[startx:].index("x") + startx
startx = xcol
try:
maxcol = self.setas[xcol + 1 :].index("x") + xcol + 1
except ValueError:
maxcol = len(self.setas)
xerr = self.setas[startx:maxcol].index("d") if "d" in self.setas[startx:maxcol] else None
else:
xcol = None
maxcol = len(self.setas) + 1
startx = 0
xerr = None
# No longer enforce ordering of yezf - allow them to appear in any order.
columns = {"y": [], "e": [], "z": [], "f": [], "u": [], "v": [], "w": []}
for ix, lett in enumerate(self.setas[startx:maxcol]):
if lett in columns:
columns[lett].append(ix + startx)
if xcol is None:
axes = 0
elif not columns["y"]:
axes = 1
elif not columns["z"]:
axes = 2
else:
axes = 3
if axes == 2 and len(columns["u"]) * len(columns["v"]) > 0:
axes = 4
elif axes == 3:
if len(columns["u"]) * len(columns["v"]) * len(columns["w"]) > 0:
axes = 6
elif len(columns["u"]) * len(columns["v"]) > 0:
axes = 5
ret = AttributeStore()
ret.update({"axes": axes, "xcol": xcol, "xerr": xerr})
for ck, rk in {
"y": "ycol",
"z": "zcol",
"e": "yerr",
"f": "zerr",
"u": "ucol",
"v": "vcol",
"w": "wcol",
}.items():
ret[rk] = columns[ck]
if axes == 0 and self.ndim >= 2 and self.shape[1] in self._col_defaults and not no_guess:
ret = self._col_defaults[self.shape[1]]
for n in ["xcol", "xerr", "ycol", "yerr", "zcol", "zerr", "ucol", "vcol", "wcol", "axes"]:
ret[f"has_{n}"] = not (ret[n] is None or (isinstance(ret[n], list) and not ret[n]))
ret["has_uvw"] = ret["has_ucol"] & ret["has_vcol"] & ret["has_wcol"]
if what in ["xcol", "xerr"]:
ret = ret[what]
elif what in ("ycol", "zcol", "ucol", "vcol", "wcol", "yerr", "zerr"):
ret = ret[what][0]
elif what in ("ycols", "zcols", "ucols", "vcols", "wcols", "yerrs", "zerrs"):
ret = ret[what[0:-1]]
return ret