|
7 | 7 | created on June 22, 2017 |
8 | 8 | """ |
9 | 9 |
|
| 10 | +from collections.abc import Mapping |
10 | 11 | from copy import deepcopy |
11 | 12 | from numpy import ndarray, array, hstack, iterable |
12 | | -from astropy.table import QTable, Column |
| 13 | +from astropy.table import QTable, Table, Column, Row, vstack |
13 | 14 | from astropy.time import Time |
14 | 15 | from astropy.coordinates import Angle |
15 | 16 | import astropy.units as u |
@@ -661,32 +662,35 @@ def __contains__(self, value): |
661 | 662 | else: |
662 | 663 | return False |
663 | 664 |
|
664 | | - def _translate_columns(self, target_colnames): |
| 665 | + def _translate_columns(self, target_colnames, ignore_missing=False): |
665 | 666 | """Translate target_colnames to the corresponding column names |
666 | 667 | present in this object's table. Returns a list of actual column |
667 | 668 | names present in this object that corresponds to target_colnames |
668 | | - (order is preserved). Raises KeyError if not all columns are |
669 | | - present or one or more columns could not be translated. |
| 669 | + (order is preserved). If `ignore_missing == False` (default), |
| 670 | + raises a `KeyError` if a match cannot be found for an input column |
| 671 | + name (neither in this object nor defined in `Conf.fieldnames`). |
| 672 | + If `ignore_missing == True`, then the problemtic column name will |
| 673 | + be silently carried over and returned. |
670 | 674 | """ |
671 | 675 |
|
672 | 676 | if not isinstance(target_colnames, (list, ndarray, tuple)): |
673 | 677 | target_colnames = [target_colnames] |
674 | 678 |
|
675 | 679 | translated_colnames = deepcopy(target_colnames) |
676 | 680 | for idx, colname in enumerate(target_colnames): |
677 | | - # colname is already a column name in self.table |
678 | | - if colname in self.field_names: |
679 | | - continue |
680 | | - # colname is an alternative column name |
681 | | - else: |
| 681 | + if colname not in self.field_names: |
| 682 | + # colname not already in self.table |
682 | 683 | for alt in Conf.fieldnames[ |
683 | 684 | Conf.fieldname_idx.get(colname, slice(0))]: |
| 685 | + # defined in `Conf.fieldnames` |
684 | 686 | if alt in self.field_names: |
685 | 687 | translated_colnames[idx] = alt |
686 | 688 | break |
687 | 689 | else: |
688 | | - raise KeyError('field "{:s}" not available.'.format( |
689 | | - colname)) |
| 690 | + # undefined colname |
| 691 | + if not ignore_missing: |
| 692 | + raise KeyError('field "{:s}" not available.'.format( |
| 693 | + colname)) |
690 | 694 |
|
691 | 695 | return translated_colnames |
692 | 696 |
|
@@ -934,3 +938,116 @@ def verify_fields(self, field=None): |
934 | 938 | ): |
935 | 939 | raise FieldError('Field {} does not have units of {}' |
936 | 940 | .format(test_field, str(dim.unit))) |
| 941 | + |
| 942 | + def add_row(self, vals, names=None, units=None): |
| 943 | + """Add a new row to the end of DataClass. |
| 944 | +
|
| 945 | + This is similar to `astropy.table.Table.add_row`, but allows for |
| 946 | + a set of different columns in the new row from the original DataClass |
| 947 | + object. It also allows for aliases of column names. |
| 948 | +
|
| 949 | + Parameters |
| 950 | + ---------- |
| 951 | + vals : `~astropy.table.Row`, tuple, list, dict |
| 952 | + Row to be added |
| 953 | + names : iterable of strings, optional |
| 954 | + The names of columns if not implicitly specified in ``vals``. |
| 955 | + Takes precedence over the column names in ``vals`` if any. |
| 956 | + units : str or list-like, optional |
| 957 | + Unit labels (as provided by `~astropy.units.Unit`) in which |
| 958 | + the data provided in ``rows`` will be stored in the underlying |
| 959 | + table. If None, the units as provided by ``rows`` |
| 960 | + are used. If the units provided in ``units`` differ from those |
| 961 | + used in ``rows``, ``rows`` will be transformed to the units |
| 962 | + provided in ``units``. Must have the same length as ``names`` |
| 963 | + and the individual data rows in ``rows``. Default: None |
| 964 | +
|
| 965 | + Notes |
| 966 | + ----- |
| 967 | + If a time is included in ``vals``, it can either be an explicit |
| 968 | + `~astropy.time.Time` object, or a number, `~astropy.units.Quantity` |
| 969 | + object, or string that can be inferred to be a time by the existing |
| 970 | + column of the same name or by its position in the sequence. In |
| 971 | + this case, the type of time values must be valid to initialize |
| 972 | + an `~astropy.time.Time` object with format='jd' or 'isot', and |
| 973 | + the scale of time is default to the scale of the corresponding |
| 974 | + existing column of time. |
| 975 | +
|
| 976 | + Examples |
| 977 | + -------- |
| 978 | + >>> import astropy.units as u |
| 979 | + >>> from sbpy.data import DataClass |
| 980 | + >>> |
| 981 | + >>> data = DataClass.from_dict( |
| 982 | + ... {'rh': [1, 2, 3] * u.au, 'delta': [1, 2, 3] * u.au}) |
| 983 | + >>> row = {'rh': 4 * u.au, 'delta': 4 * u.au, 'phase': 15 * u.deg} |
| 984 | + >>> data.add_row(row) |
| 985 | + """ |
| 986 | + if isinstance(vals, Row): |
| 987 | + vals = DataClass.from_table(vals) |
| 988 | + else: |
| 989 | + if isinstance(vals, Mapping): |
| 990 | + keys_list = list(vals.keys()) |
| 991 | + vals_list = [vals[k] for k in keys_list] |
| 992 | + vals = vals_list |
| 993 | + if names is None: |
| 994 | + names = keys_list |
| 995 | + else: |
| 996 | + # assume it's an iterable that can be taken as columns |
| 997 | + if names is None: |
| 998 | + # if names of columns are not specified, default to the |
| 999 | + # existing names and orders |
| 1000 | + names = self.field_names |
| 1001 | + # check if any astropy Time columns |
| 1002 | + for i, k in enumerate(names): |
| 1003 | + if k in self and isinstance(self[k], Time): |
| 1004 | + vals[i] = Time(vals[i], scale=self[k].scale, |
| 1005 | + format='isot' if isinstance(vals[i], str) |
| 1006 | + else 'jd') |
| 1007 | + vals = DataClass.from_rows(vals, names, units=units) |
| 1008 | + self.vstack(vals) |
| 1009 | + |
| 1010 | + def vstack(self, data, **kwargs): |
| 1011 | + """Stack another DataClass object to the end of DataClass |
| 1012 | +
|
| 1013 | + Similar to `~astropy.table.Table.vstack`, the DataClass object |
| 1014 | + to be stacked doesn't have to have the same set of columns as |
| 1015 | + the existing object. The `join_type` keyword parameter will be |
| 1016 | + used to decide how to process the different sets of columns. |
| 1017 | +
|
| 1018 | + Joining will be in-place. |
| 1019 | +
|
| 1020 | + Parameters |
| 1021 | + ---------- |
| 1022 | + data : `~sbpy.data.DataClass`, dict, `~astropy.table.Table` |
| 1023 | + Object to be joined with the current object |
| 1024 | + kwargs : dict |
| 1025 | + Keyword parameters accepted by `~astropy.table.Table.vstack`. |
| 1026 | +
|
| 1027 | + Examples |
| 1028 | + -------- |
| 1029 | + >>> import astropy.units as u |
| 1030 | + >>> from sbpy.data import DataClass |
| 1031 | + >>> |
| 1032 | + >>> data1 = DataClass.from_dict( |
| 1033 | + ... {'rh': [1, 2, 3] * u.au, 'delta': [1, 2, 3] * u.au}) |
| 1034 | + >>> data2 = DataClass.from_dict( |
| 1035 | + ... {'rh': [4, 5] * u.au, 'phase': [15, 15] * u.deg}) |
| 1036 | + >>> data1.vstack(data2) |
| 1037 | + """ |
| 1038 | + # check and process input data |
| 1039 | + if isinstance(data, dict): |
| 1040 | + data = DataClass.from_dict(data) |
| 1041 | + elif isinstance(data, Table): |
| 1042 | + data = DataClass.from_table(data) |
| 1043 | + if not isinstance(data, DataClass): |
| 1044 | + raise ValueError('DataClass, dict, or astorpy.table.Table are ' |
| 1045 | + 'expected, but {} is received.'. |
| 1046 | + format(type(data))) |
| 1047 | + |
| 1048 | + # adjust input column names for alises |
| 1049 | + alt = self._translate_columns(data.field_names, ignore_missing=True) |
| 1050 | + data.table.rename_columns(data.field_names, alt) |
| 1051 | + |
| 1052 | + # join with the input table |
| 1053 | + self.table = vstack([self.table, data.table], **kwargs) |
0 commit comments