Repository: StingraySoftware/stingray

Issue:
Need to refresh I/O to ascii objects (and maybe the other formats too)
A lot has changed in our code and dependencies since @haroonrashid235 's work, and we need to rethink our I/O infrastructure. In particular, the roundtrip-to-ascii test of an event list is failing in recent version of Astropy.
Instead of just patching it and wait for the next way to make it fail, I suggest to mark the test with xfail for the time being (see commit ee01b89) and take the time to rethink our I/O infrastructure from scratch.

Discussion:
pep8speaks: Hello @matteobachetti! Thanks for opening this PR. We checked the lines you've touched for [PEP 8](https://www.python.org/dev/peps/pep-0008) issues, and found:





* In the file [`stingray/tests/test_events.py`](https://github.com/StingraySoftware/stingray/blob/f9f4dac4fdbd935f1591e087dc70f724bcfb3bc1/stingray/tests/test_events.py):

> [Line 321:1](https://github.com/StingraySoftware/stingray/blob/f9f4dac4fdbd935f1591e087dc70f724bcfb3bc1/stingray/tests/test_events.py#L321): [W391](https://duckduckgo.com/?q=pep8%20W391) blank line at end of file

* In the file [`stingray/tests/test_lightcurve.py`](https://github.com/StingraySoftware/stingray/blob/f9f4dac4fdbd935f1591e087dc70f724bcfb3bc1/stingray/tests/test_lightcurve.py):

> [Line 1009:24](https://github.com/StingraySoftware/stingray/blob/f9f4dac4fdbd935f1591e087dc70f724bcfb3bc1/stingray/tests/test_lightcurve.py#L1009): [E128](https://duckduckgo.com/?q=pep8%20E128) continuation line under-indented for visual indent


codecov[bot]: # [Codecov](https://codecov.io/gh/StingraySoftware/stingray/pull/518?src=pr&el=h1) Report
> Merging [#518](https://codecov.io/gh/StingraySoftware/stingray/pull/518?src=pr&el=desc) (7beab3f) into [master](https://codecov.io/gh/StingraySoftware/stingray/commit/b64758fb28fff308bce2fd283b1fc5c7baf08dac?el=desc) (b64758f) will **decrease** coverage by `3.10%`.
> The diff coverage is `95.23%`.

[![Impacted file tree graph](https://codecov.io/gh/StingraySoftware/stingray/pull/518/graphs/tree.svg?width=650&height=150&src=pr&token=FjWeFfhU9F)](https://codecov.io/gh/StingraySoftware/stingray/pull/518?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #518      +/-   ##
==========================================
- Coverage   95.46%   92.35%   -3.11%     
==========================================
  Files          36       36              
  Lines        5531     5602      +71     
==========================================
- Hits         5280     5174     -106     
- Misses        251      428     +177     
```


| [Impacted Files](https://codecov.io/gh/StingraySoftware/stingray/pull/518?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [stingray/lightcurve.py](https://codecov.io/gh/StingraySoftware/stingray/pull/518/diff?src=pr&el=tree#diff-c3RpbmdyYXkvbGlnaHRjdXJ2ZS5weQ==) | `95.84% <90.00%> (-0.49%)` | :arrow_down: |
| [stingray/events.py](https://codecov.io/gh/StingraySoftware/stingray/pull/518/diff?src=pr&el=tree#diff-c3RpbmdyYXkvZXZlbnRzLnB5) | `100.00% <100.00%> (+1.21%)` | :arrow_up: |
| [stingray/deadtime/model.py](https://codecov.io/gh/StingraySoftware/stingray/pull/518/diff?src=pr&el=tree#diff-c3RpbmdyYXkvZGVhZHRpbWUvbW9kZWwucHk=) | `64.64% <0.00%> (-33.34%)` | :arrow_down: |
| [stingray/stats.py](https://codecov.io/gh/StingraySoftware/stingray/pull/518/diff?src=pr&el=tree#diff-c3RpbmdyYXkvc3RhdHMucHk=) | `73.97% <0.00%> (-23.29%)` | :arrow_down: |
| [stingray/utils.py](https://codecov.io/gh/StingraySoftware/stingray/pull/518/diff?src=pr&el=tree#diff-c3RpbmdyYXkvdXRpbHMucHk=) | `88.31% <0.00%> (-10.72%)` | :arrow_down: |
| [stingray/pulse/pulsar.py](https://codecov.io/gh/StingraySoftware/stingray/pull/518/diff?src=pr&el=tree#diff-c3RpbmdyYXkvcHVsc2UvcHVsc2FyLnB5) | `91.28% <0.00%> (-7.32%)` | :arrow_down: |
| [stingray/pulse/search.py](https://codecov.io/gh/StingraySoftware/stingray/pull/518/diff?src=pr&el=tree#diff-c3RpbmdyYXkvcHVsc2Uvc2VhcmNoLnB5) | `89.93% <0.00%> (-6.72%)` | :arrow_down: |
| [stingray/filters.py](https://codecov.io/gh/StingraySoftware/stingray/pull/518/diff?src=pr&el=tree#diff-c3RpbmdyYXkvZmlsdGVycy5weQ==) | `94.44% <0.00%> (-4.63%)` | :arrow_down: |
| [stingray/gti.py](https://codecov.io/gh/StingraySoftware/stingray/pull/518/diff?src=pr&el=tree#diff-c3RpbmdyYXkvZ3RpLnB5) | `93.76% <0.00%> (-4.54%)` | :arrow_down: |
| [stingray/pulse/accelsearch.py](https://codecov.io/gh/StingraySoftware/stingray/pull/518/diff?src=pr&el=tree#diff-c3RpbmdyYXkvcHVsc2UvYWNjZWxzZWFyY2gucHk=) | `91.25% <0.00%> (-2.19%)` | :arrow_down: |
| ... and [4 more](https://codecov.io/gh/StingraySoftware/stingray/pull/518/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/StingraySoftware/stingray/pull/518?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/StingraySoftware/stingray/pull/518?src=pr&el=footer). Last update [b64758f...d2117b3](https://codecov.io/gh/StingraySoftware/stingray/pull/518?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/setup.cfg b/setup.cfg
index 242f3651..0bab447c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -31,6 +31,7 @@ console_scripts =
 all =
     scipy
     matplotlib
+    tbb
     numba
     jplephem
     emcee
@@ -42,6 +43,8 @@ all =
     pint-pulsar
     zarr
     numcodecs
+    pyyaml
+
 test =
     pytest<6
     pytest-astropy
diff --git a/stingray/events.py b/stingray/events.py
index d8ebf954..5c22e991 100644
--- a/stingray/events.py
+++ b/stingray/events.py
@@ -86,7 +86,8 @@ class EventList(object):
 
     """
     def __init__(self, time=None, energy=None, ncounts=None, mjdref=0, dt=0,
-                 notes="", gti=None, pi=None, high_precision=False):
+                 notes="", gti=None, pi=None, high_precision=False,
+                 mission=None, instr=None):
 
         self.energy = None if energy is None else np.asarray(energy)
         self.notes = notes
@@ -95,6 +96,8 @@ class EventList(object):
         self.gti = np.asarray(gti) if gti is not None else None
         self.pi = pi
         self.ncounts = ncounts
+        self.mission = mission
+        self.instr = instr
 
         if time is not None:
             if not high_precision:
@@ -369,14 +372,16 @@ class EventList(object):
 
         * ``time``:  the time stamps of the photon arrivals
         * ``energy``: the photon energy corresponding to each time stamp
-        * ``ncounts``: the total number of photon counts recorded
         * ``mjdref``: a reference time in Modified Julian Date
-        * ``dt``: the time resolution of the data
         * ``notes``: other possible meta-data
         * ``gti``: Good Time Intervals
         * ``pi``: some instruments record energies as "Pulse Invariant", an integer number recorded from
           the Pulse Height Amplitude
 
+        Ascii files need to be ECSV files with an
+        :class:`astropy.timeseries.TimeSeries` containing time, energy and pi
+        in the data cols and the remaining metadata in the ``meta`` attribute
+
         Parameters
         ----------
         filename: str
@@ -391,15 +396,17 @@ class EventList(object):
             The :class:`EventList` object reconstructed from file
         """
 
+        if format_ == 'ascii':
+            from astropy.timeseries import TimeSeries
+            # time = np.asarray(data.columns[0])
+            ts = TimeSeries.read(filename, format='ascii.ecsv')
+            return EventList.from_astropy_timeseries(ts)
+
         attributes = ['time', 'energy', 'ncounts', 'mjdref', 'dt',
                       'notes', 'gti', 'pi']
         data = read(filename, format_, cols=attributes)
 
-        if format_ == 'ascii':
-            time = np.asarray(data.columns[0])
-            return EventList(time=time)
-
-        elif format_ == 'hdf5' or format_ == 'fits':
+        if format_ == 'hdf5' or format_ == 'fits':
             keys = data.keys()
             values = []
 
@@ -439,7 +446,9 @@ class EventList(object):
         """
 
         if format_ == 'ascii':
-            write(np.asarray([self.time]).T, filename, format_, fmt=["%s"])
+            # write(np.asarray([self.time]).T, filename, format_, fmt=["%s"])
+            ts = self.to_astropy_timeseries()
+            ts.write(filename, format='ascii.ecsv', overwrite=True)
 
         elif format_ == 'pickle':
             write(self, filename, format_)
@@ -564,3 +573,43 @@ class EventList(object):
         new_ev.gti = new_ev.gti + time_shift
 
         return new_ev
+
+    def to_astropy_timeseries(self):
+        from astropy.timeseries import TimeSeries
+        from astropy.time import TimeDelta
+        from astropy import units as u
+        data = {}
+        for attr in ['energy', 'pi']:
+            if hasattr(self, attr) and getattr(self, attr) is not None:
+                data[attr] = np.asarray(getattr(self, attr))
+        if data == {}:
+            data = None
+        ts = TimeSeries(data=data, time=TimeDelta(self.time * u.s))
+        ts.meta['gti'] = self.gti
+        ts.meta['mjdref'] = self.mjdref
+        ts.meta['instr'] = self.instr
+        ts.meta['mission'] = self.mission
+        return ts
+
+    @staticmethod
+    def from_astropy_timeseries(ts):
+        from astropy.timeseries import TimeSeries
+        from astropy import units as u
+        energy = pi = gti = instr = mission = mjdref = None
+        if 'energy' in ts.colnames:
+            energy = ts['energy']
+        if 'pi' in ts.colnames:
+            pi = ts['pi']
+        if 'gti' in ts.meta:
+            gti = ts.meta['gti']
+        if 'instr' in ts.meta:
+            instr = ts.meta['instr']
+        if 'mission' in ts.meta:
+            mission = ts.meta['mission']
+        if 'mjdref' in ts.meta:
+            mjdref = ts.meta['mjdref']
+
+        ev = EventList(time=ts.time.to(u.s).value, energy=energy, pi=pi,
+                       gti=gti, mission=mission, instr=instr, mjdref=mjdref)
+
+        return ev
diff --git a/stingray/lightcurve.py b/stingray/lightcurve.py
index 196e7e2b..b5a9bdf2 100644
--- a/stingray/lightcurve.py
+++ b/stingray/lightcurve.py
@@ -136,7 +136,8 @@ class Lightcurve(object):
     """
     def __init__(self, time, counts, err=None, input_counts=True,
                  gti=None, err_dist='poisson', mjdref=0, dt=None,
-                 skip_checks=False, low_memory=False):
+                 skip_checks=False, low_memory=False, mission=None,
+                 instr=None):
 
         if hasattr(time, 'value'):
             time = time.value
@@ -202,6 +203,8 @@ class Lightcurve(object):
         self._bin_lo = None
         self._bin_hi = None
         self._n = None
+        self.mission = mission
+        self.instr = instr
 
         self.input_counts = input_counts
         self.low_memory = low_memory
@@ -1422,6 +1425,54 @@ class Lightcurve(object):
                           err=lk.flux_err, input_counts=False,
                           skip_checks=skip_checks)
 
+    def to_astropy_timeseries(self):
+        from astropy.timeseries import TimeSeries
+        from astropy.time import TimeDelta
+        from astropy import units as u
+        data = {}
+        for attr in ['_counts', '_counts_err', '_countrate', '_countrate_err',
+                     '_bin_lo', '_bin_hi']:
+            if hasattr(self, attr) and getattr(self, attr) is not None:
+                data[attr.lstrip('_')] = np.asarray(getattr(self, attr))
+
+        ts = TimeSeries(data=data, time=TimeDelta(self.time * u.s))
+        for attr in ['_gti', 'mjdref', '_meancounts', '_meancountrate',
+                     'instr', 'mission', 'dt']:
+            if hasattr(self, attr) and getattr(self, attr) is not None:
+                ts.meta[attr.lstrip('_')] = getattr(self, attr)
+
+        return ts
+
+    @staticmethod
+    def from_astropy_timeseries(ts):
+        from astropy.timeseries import TimeSeries
+        from astropy import units as u
+
+        dt = None
+        if 'dt' in ts.meta:
+            dt = ts.meta['dt']
+        if 'counts' in ts.colnames:
+            lc = Lightcurve(ts.time.to(u.s).value, ts['counts'],
+                            skip_checks=True, dt=dt)
+        elif 'countrate' in ts.colnames:
+            lc = Lightcurve(ts.time.to(u.s).value, ts['countrate'],
+                            input_counts=False, skip_checks=True, dt=dt)
+        else:
+            raise ValueError('Input timeseries must contain at least a '
+                             '`counts` or a `countrate` column')
+
+        for attr in ['counts', 'counts_err', 'countrate', 'countrate_err',
+                     'bin_hi', 'bin_low']:
+            if attr in ts.colnames and getattr(lc, '_' + attr) is None:
+                setattr(lc, attr, ts[attr])
+
+        for attr in ['gti', 'mjdref', 'meancounts', 'meancountrate',
+                     'instr', 'mission']:
+            if attr in ts.meta and ts.meta[attr] is not None:
+                setattr(lc, attr, ts.meta[attr])
+
+        return lc
+
     def plot(self, witherrors=False, labels=None, axis=None, title=None,
              marker='-', save=False, filename=None):
         """
@@ -1518,8 +1569,11 @@ class Lightcurve(object):
             self.countrate, self.countrate_err, \
             self.gti
         if format_ == 'ascii':
-            io.write(np.array([self.time, self.counts, self.counts_err]).T,
-                     filename, format_, fmt=["%s", "%s", "%s"])
+            # io.write(np.array([self.time, self.counts, self.counts_err]).T,
+            #          filename, format_, fmt=["%s", "%s", "%s"])
+            ts = self.to_astropy_timeseries()
+            return ts.write(filename, format='ascii.ecsv', overwrite=True)
+
         elif format_ == 'pickle':
             io.write(self, filename, format_)
         elif format_ == 'hdf5':
@@ -1535,6 +1589,11 @@ class Lightcurve(object):
         * HDF5
         * ASCII
 
+        Ascii files need to be ECSV files with an
+        :class:`astropy.timeseries.TimeSeries` containing time and counts
+        (+possibly counts_err, countrate, etc.) in the data cols and the
+        remaining metadata in the ``meta`` attribute
+
         Parameters
         ----------
         filename: str
@@ -1554,28 +1613,18 @@ class Lightcurve(object):
 
         Returns
         --------
-        lc : ``astropy.table`` or ``dict`` or :class:`Lightcurve` object
-            * If ``format\_`` is ``ascii``: ``astropy.table`` is returned.
+        lc : ``dict`` or :class:`Lightcurve` object
+            * If ``format\_`` is ``ascii``: :class:`Lightcurve` is returned.
             * If ``format\_`` is ``hdf5``: dictionary with key-value pairs is returned.
             * If ``format\_`` is ``pickle``: :class:`Lightcurve` object is returned.
         """
-
         if format_ == 'ascii':
-            data_raw = io.read(filename, format_,
-                               names=['time', 'counts', 'counts_err'])
-
-            data = {'time': np.array(data_raw['time']),
-                    'counts': np.array(data_raw['counts']),
-                    'counts_err': np.array(data_raw['counts_err'])}
-            data['dt'] = np.median(np.diff(data['time']))
-            data['gti'] = np.array([[data['time'][0] - data['dt'] / 0,
-                                     data['time'][-1] + data['dt'] / 0]])
-            # We use default_err_dist == 'gauss' just because people using
-            # ASCII files will generally use Gaussian errors. This can be
-            # changed from the command line.
-            data['err_dist'] = default_err_dist
-            data['mjdref'] = 0
-        elif format_ == 'hdf5':
+            from astropy.timeseries import TimeSeries
+
+            ts = TimeSeries.read(filename, format='ascii.ecsv')
+            return Lightcurve.from_astropy_timeseries(ts)
+
+        if format_ == 'hdf5':
             data_raw = io.read(filename, format_)
 
             data = {'time': np.array(data_raw['_time']),


Test patch:
diff --git a/stingray/tests/test_events.py b/stingray/tests/test_events.py
index b1c54944..fb8bea5a 100644
--- a/stingray/tests/test_events.py
+++ b/stingray/tests/test_events.py
@@ -10,12 +10,24 @@ curdir = os.path.abspath(os.path.dirname(__file__))
 datadir = os.path.join(curdir, 'data')
 
 _H5PY_INSTALLED = True
+_HAS_YAML = True
+_HAS_TIMESERIES = True
 
 try:
     import h5py
 except ImportError:
     _H5PY_INSTALLED = False
 
+try:
+    import astropy.timeseries
+    from astropy.timeseries import TimeSeries
+except ImportError:
+    _HAS_TIMESERIES = False
+
+try:
+    import yaml
+except ImportError:
+    _HAS_YAML = False
 
 class TestEvents(object):
 
@@ -265,13 +277,13 @@ class TestEvents(object):
                 np.array([10, 6, 2, 2, 11, 8, 1, 3, 3, 2])).all()
         assert np.allclose(ev_new.gti, np.array([[5, 6]]))
 
-    @pytest.mark.xfail
+    @pytest.mark.skipif('not (_HAS_YAML and _HAS_TIMESERIES)')
     def test_io_with_ascii(self):
         ev = EventList(self.time)
-        ev.write('ascii_ev.txt', format_='ascii')
-        ev = ev.read('ascii_ev.txt', format_='ascii')
+        ev.write('ascii_ev.ecsv', format_='ascii')
+        ev = ev.read('ascii_ev.ecsv', format_='ascii')
         assert np.all(ev.time == self.time)
-        os.remove('ascii_ev.txt')
+        os.remove('ascii_ev.ecsv')
 
     def test_io_with_pickle(self):
         ev = EventList(self.time)
@@ -318,5 +330,22 @@ class TestEvents(object):
             with pytest.raises(KeyError):
                 ev.write('ev.pickle', format_="unsupported")
 
-            with pytest.raises(KeyError):
-                ev.read('ev.pickle', format_="unsupported")
+        with pytest.raises(KeyError):
+            ev.read('ev.pickle', format_="unsupported")
+
+    @pytest.mark.skipif('not _HAS_TIMESERIES')
+    def test_timeseries_roundtrip(self):
+        """Test that io methods raise Key Error when
+        wrong format is provided.
+        """
+        N = len(self.time)
+        ev = EventList(time=self.time, gti=self.gti, energy=np.zeros(N),
+                       pi=np.ones(N), mission="BUBU", instr="BABA",
+                       mjdref=53467.)
+        ts = ev.to_astropy_timeseries()
+        new_ev = ev.from_astropy_timeseries(ts)
+        for attr in ['time', 'energy', 'pi', 'gti']:
+            assert np.all(getattr(ev, attr) == getattr(new_ev, attr))
+        for attr in ['mission', 'instr', 'mjdref']:
+            assert getattr(ev, attr) == getattr(new_ev, attr)
+
diff --git a/stingray/tests/test_lightcurve.py b/stingray/tests/test_lightcurve.py
index 24a7be3d..f3e7eb5f 100644
--- a/stingray/tests/test_lightcurve.py
+++ b/stingray/tests/test_lightcurve.py
@@ -16,6 +16,8 @@ np.random.seed(20150907)
 
 _H5PY_INSTALLED = True
 _HAS_LIGHTKURVE = True
+_HAS_TIMESERIES = True
+_HAS_YAML = True
 
 try:
     import h5py
@@ -27,6 +29,17 @@ try:
 except ImportError:
     _HAS_LIGHTKURVE = False
 
+try:
+    import astropy.timeseries
+    from astropy.timeseries import TimeSeries
+except ImportError:
+    _HAS_TIMESERIES = False
+
+try:
+    import yaml
+except ImportError:
+    _HAS_YAML = False
+
 def fvar_fun(lc):
     from stingray.utils import excess_variance
     return excess_variance(lc, normalization='fvar')
@@ -944,11 +957,20 @@ class TestLightcurve(object):
             lc.plot(title="Test Lightcurve")
         assert plt.fignum_exists(1)
 
+    # def test_io_with_ascii(self):
+    #     lc = Lightcurve(self.times, self.counts)
+    #     lc.write('ascii_lc.txt', format_='ascii')
+    #     lc.read('ascii_lc.txt', format_='ascii')
+    #     os.remove('ascii_lc.txt')
+
+    @pytest.mark.skipif('not (_HAS_YAML and _HAS_TIMESERIES)')
     def test_io_with_ascii(self):
         lc = Lightcurve(self.times, self.counts)
-        lc.write('ascii_lc.txt', format_='ascii')
-        lc.read('ascii_lc.txt', format_='ascii')
-        os.remove('ascii_lc.txt')
+        lc.write('ascii_lc.ecsv', format_='ascii')
+        lc = lc.read('ascii_lc.ecsv', format_='ascii')
+        assert np.all(lc.time == self.times)
+        assert np.all(lc.counts == self.counts)
+        os.remove('ascii_lc.ecsv')
 
     def test_io_with_pickle(self):
         lc = Lightcurve(self.times, self.counts)
@@ -1023,6 +1045,53 @@ class TestLightcurve(object):
         assert np.all(lc2.counts == lc.counts)
         assert np.all(lc2.countrate == lc.countrate)
 
+    @pytest.mark.skipif('not _HAS_TIMESERIES')
+    def test_timeseries_roundtrip(self):
+        """Test that io methods raise Key Error when
+        wrong format is provided.
+        """
+        N = len(self.times)
+        lc = Lightcurve(self.times, self.counts, mission="BUBU", instr="BABA",
+                        mjdref=53467.)
+
+        ts = lc.to_astropy_timeseries()
+        new_lc = lc.from_astropy_timeseries(ts)
+        for attr in ['time', 'gti', 'counts']:
+            assert np.all(getattr(lc, attr) == getattr(new_lc, attr))
+        for attr in ['mission', 'instr', 'mjdref']:
+            assert getattr(lc, attr) == getattr(new_lc, attr)
+
+    @pytest.mark.skipif('not _HAS_TIMESERIES')
+    def test_timeseries_roundtrip_ctrate(self):
+        """Test that io methods raise Key Error when
+        wrong format is provided.
+        """
+        N = len(self.times)
+        dt = 0.5
+        mean_counts = 2.0
+        times = np.arange(0 + dt / 2, 5 - dt / 2, dt)
+        countrate = np.zeros_like(times) + mean_counts
+
+        lc = Lightcurve(times, countrate, mission="BUBU", instr="BABA",
+                        mjdref=53467., input_counts=False)
+
+        ts = lc.to_astropy_timeseries()
+        new_lc = lc.from_astropy_timeseries(ts)
+        for attr in ['time', 'gti', 'countrate']:
+            assert np.allclose(getattr(lc, attr), getattr(new_lc, attr))
+        assert np.allclose(new_lc.counts, lc.countrate * lc.dt)
+        for attr in ['mission', 'instr', 'mjdref']:
+            assert getattr(lc, attr) == getattr(new_lc, attr)
+
+    @pytest.mark.skipif('not _HAS_TIMESERIES')
+    def test_from_timeseries_bad(self):
+        from astropy.time import TimeDelta
+        times = TimeDelta(np.arange(10) * u.s)
+        ts = TimeSeries(time=times)
+        with pytest.raises(ValueError) as excinfo:
+            Lightcurve.from_astropy_timeseries(ts)
+        assert "Input timeseries must contain at least" in str(excinfo.value)
+
 
 class TestLightcurveRebin(object):