From f8c420b2f52baf99bd77cd741f6d764072949bc5 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Fri, 25 Aug 2023 15:38:45 -0700 Subject: [PATCH 01/35] A minimal Dask Dataframe subclass for the Ensemble --- src/tape/__init__.py | 1 + src/tape/ensemble_frame.py | 219 ++++++++++++++++++++++++ tests/tape_tests/test_ensemble_frame.py | 89 ++++++++++ 3 files changed, 309 insertions(+) create mode 100644 src/tape/ensemble_frame.py create mode 100644 tests/tape_tests/test_ensemble_frame.py diff --git a/src/tape/__init__.py b/src/tape/__init__.py index 770ee9a4..e2dbb691 100644 --- a/src/tape/__init__.py +++ b/src/tape/__init__.py @@ -1,3 +1,4 @@ from .analysis import * # noqa from .ensemble import * # noqa +from .ensemble_frame import * # noqa from .timeseries import * # noqa diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py new file mode 100644 index 00000000..c84ec1df --- /dev/null +++ b/src/tape/ensemble_frame.py @@ -0,0 +1,219 @@ +import dask.dataframe as dd + +from packaging.version import Version +import dask +DASK_2021_06_0 = Version(dask.__version__) >= Version("2021.06.0") +DASK_2022_06_0 = Version(dask.__version__) >= Version("2022.06.0") +if DASK_2021_06_0: + from dask.dataframe.dispatch import make_meta_dispatch + from dask.dataframe.backends import _nonempty_index, meta_nonempty, meta_nonempty_dataframe +else: + from dask.dataframe.core import make_meta as make_meta_dispatch + from dask.dataframe.utils import _nonempty_index, meta_nonempty, meta_nonempty_dataframe + +from dask.dataframe.core import get_parallel_type +from dask.dataframe.extensions import make_array_nonempty + +import pandas as pd + +class _Frame(dd.core._Frame): + """Base class for extensions of Dask Dataframes that track additional Ensemble-related metadata.""" + + def __init__(self, dsk, name, meta, divisions, label=None, ensemble=None): + super().__init__(dsk, name, meta, divisions) + self.label = label # A label used by the Ensemble to identify this frame. + self.ensemble = ensemble # The Ensemble object containing this frame. + + @property + def _args(self): + # Ensure our Dask extension can correctly be used by pickle. + # See https://github.com/geopandas/dask-geopandas/issues/237 + return super()._args + (self.label, self.ensemble) + + def _propagate_metadata(self, new_frame): + """Propagatees any relevant metadata to a new frame. + + Parameters + ---------- + new_frame: `_Frame` + | A frame to propage metadata to + + Returns + ---------- + new_frame: `_Frame` + The modifed frame + """ + new_frame.label = self.label + new_frame.ensemble = self.ensemble + return new_frame + + def copy(self): + self_copy = super().copy() + return self._propagate_metadata(self_copy) + +class TapeSeries(pd.Series): + """A barebones extension of a Pandas series to be used for underlying Ensmeble data. + + See https://pandas.pydata.org/docs/development/extending.html#subclassing-pandas-data-structures + """ + @property + def _constructor(self): + return TapeSeries + + @property + def _constructor_sliced(self): + return TapeSeries + +class TapeFrame(pd.DataFrame): + """A barebones extension of a Pandas frame to be used for underlying Ensmeble data. + + See https://pandas.pydata.org/docs/development/extending.html#subclassing-pandas-data-structures + """ + @property + def _constructor(self): + return TapeFrame + + @property + def _constructor_expanddim(self): + return TapeFrame + + +class EnsembleSeries(_Frame, dd.core.Series): + """A barebones extension of a Dask Series for Ensemble data. + """ + _partition_type = TapeSeries # Tracks the underlying data type + +class EnsembleFrame(_Frame, dd.core.DataFrame): + """An extension for a Dask Dataframe for data used by a lightcurve Ensemble. + + The underlying non-parallel dataframes are TapeFrames and TapeSeries which extend Pandas frames. + + Example + ---------- + import tape + ens = tape.Ensemble() + data = {...} # Some data you want tracked by the Ensemble + ensemble_frame = tape.EnsembleFrame.from_dict(data, label="my_frame", ensemble=ens) + """ + _partition_type = TapeFrame # Tracks the underlying data type + + def __getitem__(self, key): + result = super().__getitem__(key) + if isinstance(result, _Frame): + # Ensures that we have any + result = self._propagate_metadata(result) + return result + + @classmethod + def from_tapeframe( + cls, data, npartitions=None, chunksize=None, sort=True, label=None, ensemble=None + ): + """ Returns an EnsembleFrame constructed from a TapeFrame. + Parameters + ---------- + data: `TapeFrame` + Frame containing the underlying data fro the EnsembleFram + npartitions: `int`, optional + The number of partitions of the index to create. Note that depending on + the size and index of the dataframe, the output may have fewer + partitions than requested. + chunksize: `int`, optional + Size of the individual chunks of data in non-parallel objects that make up Dask frames. + sort: `bool`, optional + Whether to sort the frame by a default index. + label: `str`, optional + | The label used to by the Ensemble to identify the frame. + ensemble: `tape.Ensemble`, optional + | A linnk to the Ensmeble object that owns this frame. + Returns + result: `tape.EnsembleFrame` + The constructed EnsembleFrame object. + """ + result = dd.from_pandas(data, npartitions=npartitions, chunksize=chunksize, sort=sort, name="fdsafdfasd") + result.label = label + result.ensemble = ensemble + return result + + @classmethod + def from_dict( + cls, data, npartitions=None, orient="columns", dtype=None, columns=None, label=None, + ensemble=None, + ): + """Returns an EnsembleFrame constructed from a Python Dictionary. + Parameters + ---------- + data: `TapeFrame` + Frame containing the underlying data fro the EnsembleFram + npartitions: `int`, optional + The number of partitions of the index to create. Note that depending on + the size and index of the dataframe, the output may have fewer + partitions than requested. + orient: `str`, optional + The "orientation" of the data. If the keys of the passed dict + should be the columns of the resulting DataFrame, pass 'columns' + (default). Otherwise if the keys should be rows, pass 'index'. + If 'tight', assume a dict with keys + ['index', 'columns', 'data', 'index_names', 'column_names']. + dtype: `bool`, optional + Data type to force, otherwise infer. + columns: `str`, optional + Column labels to use when ``orient='index'``. Raises a ValueError + if used with ``orient='columns'`` or ``orient='tight'``. + label: `str`, optional + | The label used to by the Ensemble to identify the frame. + ensemble: `tape.Ensemble`, optional + | A linnk to the Ensmeble object that owns this frame. + Returns + result: `tape.EnsembleFrame` + The constructed EnsembleFrame object. + """ + frame = TapeFrame.from_dict(data, orient, dtype, columns) + return EnsembleFrame.from_tapeframe(frame, + label=label, ensemble=ensemble, npartitions=npartitions + ) + +""" +Dask Dataframes are constructed indirectly using method dispatching and inference on the +underlying data. So to ensure our subclasses behave correctly, we register the methods +below. + +For more information, see https://docs.dask.org/en/latest/dataframe-extend.html + +The following should ensure that any Dask Dataframes which use TapeSeries or TapeFrames as their +underlying data will be resolved as EnsembleFrames or EnsembleSeries as their parrallel +counterparts. The underlying Dask Dataframe _meta will be a TapeSeries or TapeFrame. +""" +get_parallel_type.register(TapeSeries, lambda _: EnsembleSeries) +get_parallel_type.register(TapeFrame, lambda _: EnsembleFrame) + +@make_meta_dispatch.register(TapeSeries) +def make_meta_series(x, index=None): + # Create an empty TapeSeries to use as Dask's underlying object meta. + result = x.head(0) + # Re-index if requested + if index is not None: + result = result.reindex(index[:0]) + return result + +@make_meta_dispatch.register(TapeFrame) +def make_meta_frame(x, index=None): + # Create an empty TapeFrame to use as Dask's underlying object meta. + result = x.head(0) + # Re-index if requested + if index is not None: + result = result.reindex(index[:0]) + return result + +@meta_nonempty.register(TapeSeries) +def _nonempty_tapeseries(x, index=None): + # Construct a new TapeSeries with the same underlying data. + if index is None: + index = _nonempty_index(x.index) + data = make_array_nonempty(x.dtype) + return TapeSeries(data, name=x.name, crs=x.crs) + +@meta_nonempty.register(TapeFrame) +def _nonempty_tapeseries(x, index=None): + # Construct a new TapeFrame with the same underlying data. + df = meta_nonempty_dataframe(x) + return TapeFrame(df) \ No newline at end of file diff --git a/tests/tape_tests/test_ensemble_frame.py b/tests/tape_tests/test_ensemble_frame.py new file mode 100644 index 00000000..b964c620 --- /dev/null +++ b/tests/tape_tests/test_ensemble_frame.py @@ -0,0 +1,89 @@ +""" Test EnsembleFrame (inherited from Dask.DataFrame) creation and manipulations. """ +import pandas as pd +from tape import Ensemble, EnsembleFrame, TapeFrame + +import pytest + +# Create some fake lightcurve data with two IDs (8001, 8002), two bands ["g", "b"] +# and a few time steps. +SAMPLE_LC_DATA = { + "id": [8001, 8001, 8001, 8001, 8002, 8002, 8002, 8002, 8002], + "time": [10.1, 10.2, 10.2, 11.1, 11.2, 11.3, 11.4, 15.0, 15.1], + "band": ["g", "g", "b", "g", "b", "g", "g", "g", "g"], + "err": [1.0, 2.0, 1.0, 3.0, 2.0, 3.0, 4.0, 5.0, 6.0], + "flux": [1.0, 2.0, 5.0, 3.0, 1.0, 2.0, 3.0, 4.0, 5.0], + } +TEST_LABEL = "test_frame" +TEST_ENSEMBLE = Ensemble() + +def test_from_dict(): + """ + Test creating an EnsembleFrame from a dictionary and verify that dask lazy evaluation was appropriately inherited. + """ + ens_frame = EnsembleFrame.from_dict(SAMPLE_LC_DATA, + label=TEST_LABEL, + ensemble=TEST_ENSEMBLE, + npartitions=1) + + assert isinstance(ens_frame, EnsembleFrame) + assert isinstance(ens_frame._meta, TapeFrame) + assert ens_frame.label == TEST_LABEL + assert ens_frame.ensemble is TEST_ENSEMBLE + + # The calculation for finding the max flux from the data. Note that the + # inherited dask compute method must be called to obtain the result. + assert ens_frame.flux.max().compute() == 5.0 + +def test_from_pandas(): + """ + Test creating an EnsembleFrame from a Pandas dataframe and verify that dask lazy evaluation was appropriately inherited. + """ + frame = TapeFrame(SAMPLE_LC_DATA) + ens_frame = EnsembleFrame.from_tapeframe(frame, + label=TEST_LABEL, + ensemble=TEST_ENSEMBLE, + npartitions=1) + + assert isinstance(ens_frame, EnsembleFrame) + assert isinstance(ens_frame._meta, TapeFrame) + assert ens_frame.label == TEST_LABEL + assert ens_frame.ensemble is TEST_ENSEMBLE + + # The calculation for finding the max flux from the data. Note that the + # inherited dask compute method must be called to obtain the result. + assert ens_frame.flux.max().compute() == 5.0 + + +def test_frame_propagation(): + """ + Test ensuring that slices and copies of an EnsembleFrame or still the same class. + """ + ens_frame = EnsembleFrame.from_dict(SAMPLE_LC_DATA, + label=TEST_LABEL, + ensemble=TEST_ENSEMBLE, + npartitions=1) + + # Create a copy of an EnsembleFrame and verify that it's still a proper + # EnsembleFrame with appropriate metadata propagated. + copied_frame = ens_frame.copy() + assert isinstance(copied_frame, EnsembleFrame) + assert isinstance(copied_frame._meta, TapeFrame) + assert copied_frame.label == TEST_LABEL + assert copied_frame.ensemble == TEST_ENSEMBLE + + # Test that a filtered EnsembleFrame is still an EnsembleFrame. + filtered_frame = ens_frame[["id", "time"]] + assert isinstance(filtered_frame, EnsembleFrame) + assert isinstance(filtered_frame._meta, TapeFrame) + assert filtered_frame.label == TEST_LABEL + assert filtered_frame.ensemble == TEST_ENSEMBLE + + # Test that head returns a subset of the underlying TapeFrame. + h = ens_frame.head(5) + assert isinstance(h, TapeFrame) + assert len(h) == 5 + + # Test that the inherited dask.DataFrame.compute method returns + # the underlying TapeFrame. + assert isinstance(ens_frame.compute(), TapeFrame) + assert len(ens_frame) == len(ens_frame.compute()) \ No newline at end of file From 740d2d788d3cf8510afc3a20eb202489be45a4db Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Mon, 28 Aug 2023 14:45:24 -0700 Subject: [PATCH 02/35] Addressed comments, added test fixture. --- src/tape/ensemble_frame.py | 52 ++----------------- tests/tape_tests/conftest.py | 20 ++++++++ tests/tape_tests/test_ensemble_frame.py | 68 +++++++++++++++---------- 3 files changed, 65 insertions(+), 75 deletions(-) diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index c84ec1df..1894fe2a 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -1,15 +1,8 @@ import dask.dataframe as dd -from packaging.version import Version import dask -DASK_2021_06_0 = Version(dask.__version__) >= Version("2021.06.0") -DASK_2022_06_0 = Version(dask.__version__) >= Version("2022.06.0") -if DASK_2021_06_0: - from dask.dataframe.dispatch import make_meta_dispatch - from dask.dataframe.backends import _nonempty_index, meta_nonempty, meta_nonempty_dataframe -else: - from dask.dataframe.core import make_meta as make_meta_dispatch - from dask.dataframe.utils import _nonempty_index, meta_nonempty, meta_nonempty_dataframe +from dask.dataframe.dispatch import make_meta_dispatch +from dask.dataframe.backends import _nonempty_index, meta_nonempty, meta_nonempty_dataframe from dask.dataframe.core import get_parallel_type from dask.dataframe.extensions import make_array_nonempty @@ -129,49 +122,10 @@ def from_tapeframe( result: `tape.EnsembleFrame` The constructed EnsembleFrame object. """ - result = dd.from_pandas(data, npartitions=npartitions, chunksize=chunksize, sort=sort, name="fdsafdfasd") + result = dd.from_pandas(data, npartitions=npartitions, chunksize=chunksize, sort=sort) result.label = label result.ensemble = ensemble return result - - @classmethod - def from_dict( - cls, data, npartitions=None, orient="columns", dtype=None, columns=None, label=None, - ensemble=None, - ): - """Returns an EnsembleFrame constructed from a Python Dictionary. - Parameters - ---------- - data: `TapeFrame` - Frame containing the underlying data fro the EnsembleFram - npartitions: `int`, optional - The number of partitions of the index to create. Note that depending on - the size and index of the dataframe, the output may have fewer - partitions than requested. - orient: `str`, optional - The "orientation" of the data. If the keys of the passed dict - should be the columns of the resulting DataFrame, pass 'columns' - (default). Otherwise if the keys should be rows, pass 'index'. - If 'tight', assume a dict with keys - ['index', 'columns', 'data', 'index_names', 'column_names']. - dtype: `bool`, optional - Data type to force, otherwise infer. - columns: `str`, optional - Column labels to use when ``orient='index'``. Raises a ValueError - if used with ``orient='columns'`` or ``orient='tight'``. - label: `str`, optional - | The label used to by the Ensemble to identify the frame. - ensemble: `tape.Ensemble`, optional - | A linnk to the Ensmeble object that owns this frame. - Returns - result: `tape.EnsembleFrame` - The constructed EnsembleFrame object. - """ - frame = TapeFrame.from_dict(data, orient, dtype, columns) - return EnsembleFrame.from_tapeframe(frame, - label=label, ensemble=ensemble, npartitions=npartitions - ) - """ Dask Dataframes are constructed indirectly using method dispatching and inference on the underlying data. So to ensure our subclasses behave correctly, we register the methods diff --git a/tests/tape_tests/conftest.py b/tests/tape_tests/conftest.py index 5ceb081c..51f02018 100644 --- a/tests/tape_tests/conftest.py +++ b/tests/tape_tests/conftest.py @@ -100,3 +100,23 @@ def parquet_ensemble_from_hipscat(dask_client): ) return ens + +# pylint: disable=redefined-outer-name +@pytest.fixture +def ensemble_from_source_dict(dask_client): + """Create an Ensemble from a source dict, returning the ensemble and the source dict.""" + ens = Ensemble(client=dask_client) + + # Create some fake data with two IDs (8001, 8002), two bands ["g", "b"] + # a few time steps, and flux. + source_dict = { + "id": [8001, 8001, 8001, 8001, 8002, 8002, 8002, 8002, 8002], + "time": [10.1, 10.2, 10.2, 11.1, 11.2, 11.3, 11.4, 15.0, 15.1], + "band": ["g", "g", "b", "g", "b", "g", "g", "g", "g"], + "err": [1.0, 2.0, 1.0, 3.0, 2.0, 3.0, 4.0, 5.0, 6.0], + "flux": [1.0, 2.0, 5.0, 3.0, 1.0, 2.0, 3.0, 4.0, 5.0], + } + cmap = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="err", band_col="band") + ens.from_source_dict(source_dict, column_mapper=cmap) + + return ens, source_dict \ No newline at end of file diff --git a/tests/tape_tests/test_ensemble_frame.py b/tests/tape_tests/test_ensemble_frame.py index b964c620..ce82712e 100644 --- a/tests/tape_tests/test_ensemble_frame.py +++ b/tests/tape_tests/test_ensemble_frame.py @@ -4,64 +4,73 @@ import pytest -# Create some fake lightcurve data with two IDs (8001, 8002), two bands ["g", "b"] -# and a few time steps. -SAMPLE_LC_DATA = { - "id": [8001, 8001, 8001, 8001, 8002, 8002, 8002, 8002, 8002], - "time": [10.1, 10.2, 10.2, 11.1, 11.2, 11.3, 11.4, 15.0, 15.1], - "band": ["g", "g", "b", "g", "b", "g", "g", "g", "g"], - "err": [1.0, 2.0, 1.0, 3.0, 2.0, 3.0, 4.0, 5.0, 6.0], - "flux": [1.0, 2.0, 5.0, 3.0, 1.0, 2.0, 3.0, 4.0, 5.0], - } TEST_LABEL = "test_frame" -TEST_ENSEMBLE = Ensemble() -def test_from_dict(): +# pylint: disable=protected-access +@pytest.mark.parametrize( + "data_fixture", + [ + "ensemble_from_source_dict", + ], +) +def test_from_dict(data_fixture, request): """ Test creating an EnsembleFrame from a dictionary and verify that dask lazy evaluation was appropriately inherited. """ - ens_frame = EnsembleFrame.from_dict(SAMPLE_LC_DATA, - label=TEST_LABEL, - ensemble=TEST_ENSEMBLE, + _, data = request.getfixturevalue(data_fixture) + ens_frame = EnsembleFrame.from_dict(data, npartitions=1) assert isinstance(ens_frame, EnsembleFrame) assert isinstance(ens_frame._meta, TapeFrame) - assert ens_frame.label == TEST_LABEL - assert ens_frame.ensemble is TEST_ENSEMBLE # The calculation for finding the max flux from the data. Note that the # inherited dask compute method must be called to obtain the result. assert ens_frame.flux.max().compute() == 5.0 -def test_from_pandas(): +@pytest.mark.parametrize( + "data_fixture", + [ + "ensemble_from_source_dict", + ], +) +def test_from_pandas(data_fixture, request): """ Test creating an EnsembleFrame from a Pandas dataframe and verify that dask lazy evaluation was appropriately inherited. """ - frame = TapeFrame(SAMPLE_LC_DATA) + ens, data = request.getfixturevalue(data_fixture) + frame = TapeFrame(data) ens_frame = EnsembleFrame.from_tapeframe(frame, label=TEST_LABEL, - ensemble=TEST_ENSEMBLE, + ensemble=ens, npartitions=1) assert isinstance(ens_frame, EnsembleFrame) assert isinstance(ens_frame._meta, TapeFrame) assert ens_frame.label == TEST_LABEL - assert ens_frame.ensemble is TEST_ENSEMBLE + assert ens_frame.ensemble is ens # The calculation for finding the max flux from the data. Note that the # inherited dask compute method must be called to obtain the result. assert ens_frame.flux.max().compute() == 5.0 -def test_frame_propagation(): +@pytest.mark.parametrize( + "data_fixture", + [ + "ensemble_from_source_dict", + ], +) +def test_frame_propagation(data_fixture, request): """ Test ensuring that slices and copies of an EnsembleFrame or still the same class. """ - ens_frame = EnsembleFrame.from_dict(SAMPLE_LC_DATA, - label=TEST_LABEL, - ensemble=TEST_ENSEMBLE, + ens, data = request.getfixturevalue(data_fixture) + ens_frame = EnsembleFrame.from_dict(data, npartitions=1) + # Set a label and ensemble for the frame and copies/transformations retain them. + ens_frame.label = TEST_LABEL + ens_frame.ensemble=ens # Create a copy of an EnsembleFrame and verify that it's still a proper # EnsembleFrame with appropriate metadata propagated. @@ -69,14 +78,21 @@ def test_frame_propagation(): assert isinstance(copied_frame, EnsembleFrame) assert isinstance(copied_frame._meta, TapeFrame) assert copied_frame.label == TEST_LABEL - assert copied_frame.ensemble == TEST_ENSEMBLE + assert copied_frame.ensemble == ens # Test that a filtered EnsembleFrame is still an EnsembleFrame. filtered_frame = ens_frame[["id", "time"]] assert isinstance(filtered_frame, EnsembleFrame) assert isinstance(filtered_frame._meta, TapeFrame) assert filtered_frame.label == TEST_LABEL - assert filtered_frame.ensemble == TEST_ENSEMBLE + assert filtered_frame.ensemble == ens + + # Test that the output of an EnsembleFrame query is still an EnsembleFrame + queried_rows = ens_frame.query("flux > 3.0") + assert isinstance(queried_rows, EnsembleFrame) + assert isinstance(filtered_frame._meta, TapeFrame) + assert filtered_frame.label == TEST_LABEL + assert filtered_frame.ensemble == ens # Test that head returns a subset of the underlying TapeFrame. h = ens_frame.head(5) From 9a613923af007a2c12f1dbed9b1ed40300382c90 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Tue, 29 Aug 2023 10:38:43 -0700 Subject: [PATCH 03/35] Make convert_flux_to_mag part of the EnsembleFrame --- src/tape/ensemble.py | 58 +---------------------- src/tape/ensemble_frame.py | 63 +++++++++++++++++++++++++ tests/tape_tests/conftest.py | 17 ++++--- tests/tape_tests/test_ensemble.py | 55 --------------------- tests/tape_tests/test_ensemble_frame.py | 60 +++++++++++++++++++++-- 5 files changed, 130 insertions(+), 123 deletions(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index f1693918..839d39a7 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -1094,63 +1094,7 @@ def from_source_dict(self, source_dict, column_mapper=None, npartitions=1, **kwa self._source_dirty = False self._object_dirty = False return self - - def convert_flux_to_mag(self, flux_col, zero_point, err_col=None, zp_form="mag", out_col_name=None): - """Converts a flux column into a magnitude column. - - Parameters - ---------- - flux_col: 'str' - The name of the ensemble flux column to convert into magnitudes. - zero_point: 'str' - The name of the ensemble column containing the zero point - information for column transformation. - err_col: 'str', optional - The name of the ensemble column containing the errors to propagate. - Errors are propagated using the following approximation: - Err= (2.5/log(10))*(flux_error/flux), which holds mainly when the - error in flux is much smaller than the flux. - zp_form: `str`, optional - The form of the zero point column, either "flux" or - "magnitude"/"mag". Determines how the zero point (zp) is applied in - the conversion. If "flux", then the function is applied as - mag=-2.5*log10(flux/zp), or if "magnitude", then - mag=-2.5*log10(flux)+zp. - out_col_name: 'str', optional - The name of the output magnitude column, if None then the output - is just the flux column name + "_mag". The error column is also - generated as the out_col_name + "_err". - - Returns - ---------- - ensemble: `tape.ensemble.Ensemble` - The ensemble object with a new magnitude (and error) column. - - """ - if out_col_name is None: - out_col_name = flux_col + "_mag" - - if zp_form == "flux": # mag = -2.5*np.log10(flux/zp) - self._source = self._source.assign( - **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / x[zero_point])} - ) - - elif zp_form == "magnitude" or zp_form == "mag": # mag = -2.5*np.log10(flux) + zp - self._source = self._source.assign( - **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + x[zero_point]} - ) - - else: - raise ValueError(f"{zp_form} is not a valid zero_point format.") - - # Calculate Errors - if err_col is not None: - self._source = self._source.assign( - **{out_col_name + "_err": lambda x: (2.5 / np.log(10)) * (x[err_col] / x[flux_col])} - ) - - return self - + def _generate_object_table(self): """Generate the object table from the source table.""" counts = self._source.groupby([self._id_col, self._band_col])[self._time_col].aggregate("count") diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index 1894fe2a..70098c13 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -7,6 +7,7 @@ from dask.dataframe.core import get_parallel_type from dask.dataframe.extensions import make_array_nonempty +import numpy as np import pandas as pd class _Frame(dd.core._Frame): @@ -126,6 +127,68 @@ def from_tapeframe( result.label = label result.ensemble = ensemble return result + + def convert_flux_to_mag(self, + flux_col, + zero_point, + err_col=None, + zp_form="mag", + out_col_name=None, + ): + """Converts this EnsembleFrame's flux column into a magnitude column, returning a new + EnsembleFrame. + + Parameters + ---------- + flux_col: 'str' + The name of the EnsembleFrame flux column to convert into magnitudes. + zero_point: 'str' + The name of the EnsembleFrame column containing the zero point + information for column transformation. + err_col: 'str', optional + The name of the EnsembleFrame column containing the errors to propagate. + Errors are propagated using the following approximation: + Err= (2.5/log(10))*(flux_error/flux), which holds mainly when the + error in flux is much smaller than the flux. + zp_form: `str`, optional + The form of the zero point column, either "flux" or + "magnitude"/"mag". Determines how the zero point (zp) is applied in + the conversion. If "flux", then the function is applied as + mag=-2.5*log10(flux/zp), or if "magnitude", then + mag=-2.5*log10(flux)+zp. + out_col_name: 'str', optional + The name of the output magnitude column, if None then the output + is just the flux column name + "_mag". The error column is also + generated as the out_col_name + "_err". + Returns + ---------- + result: `tape.EnsembleFrame` + A new EnsembleFrame object with a new magnitude (and error) column. + """ + if out_col_name is None: + out_col_name = flux_col + "_mag" + + result = None + if zp_form == "flux": # mag = -2.5*np.log10(flux/zp) + result = self.assign( + **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / x[zero_point])} + ) + + elif zp_form == "magnitude" or zp_form == "mag": # mag = -2.5*np.log10(flux) + zp + result = self.assign( + **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + x[zero_point]} + ) + else: + raise ValueError(f"{zp_form} is not a valid zero_point format.") + + # Calculate Errors + if err_col is not None: + result = result.assign( + **{out_col_name + "_err": lambda x: (2.5 / np.log(10)) * (x[err_col] / x[flux_col])} + ) + + return result + """ Dask Dataframes are constructed indirectly using method dispatching and inference on the underlying data. So to ensure our subclasses behave correctly, we register the methods diff --git a/tests/tape_tests/conftest.py b/tests/tape_tests/conftest.py index 51f02018..15174293 100644 --- a/tests/tape_tests/conftest.py +++ b/tests/tape_tests/conftest.py @@ -108,15 +108,18 @@ def ensemble_from_source_dict(dask_client): ens = Ensemble(client=dask_client) # Create some fake data with two IDs (8001, 8002), two bands ["g", "b"] - # a few time steps, and flux. + # a few time steps, flux, and data for zero point calculations. source_dict = { - "id": [8001, 8001, 8001, 8001, 8002, 8002, 8002, 8002, 8002], - "time": [10.1, 10.2, 10.2, 11.1, 11.2, 11.3, 11.4, 15.0, 15.1], - "band": ["g", "g", "b", "g", "b", "g", "g", "g", "g"], - "err": [1.0, 2.0, 1.0, 3.0, 2.0, 3.0, 4.0, 5.0, 6.0], - "flux": [1.0, 2.0, 5.0, 3.0, 1.0, 2.0, 3.0, 4.0, 5.0], + "id": [8001, 8001, 8002, 8002, 8002], + "time": [1, 2, 3, 4, 5], + "flux": [30.5, 70, 80.6, 30.2, 60.3], + "zp_mag": [25.0, 25.0, 25.0, 25.0, 25.0], + "zp_flux": [10**10, 10**10, 10**10, 10**10, 10**10], + "error": [10, 10, 10, 10, 10], + "band": ["g", "g", "b", "b", "b"], } - cmap = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="err", band_col="band") + # map flux_col to one of the flux columns at the start + cmap = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="error", band_col="band") ens.from_source_dict(source_dict, column_mapper=cmap) return ens, source_dict \ No newline at end of file diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index 41567e2f..49f92238 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -706,61 +706,6 @@ def test_coalesce(dask_client, drop_inputs): for col in ["flux1", "flux2", "flux3"]: assert col in ens._source.columns - -@pytest.mark.parametrize("zp_form", ["flux", "mag", "magnitude", "lincc"]) -@pytest.mark.parametrize("err_col", [None, "error"]) -@pytest.mark.parametrize("out_col_name", [None, "mag"]) -def test_convert_flux_to_mag(dask_client, zp_form, err_col, out_col_name): - ens = Ensemble(client=dask_client) - - source_dict = { - "id": [0, 0, 0, 0, 0], - "time": [1, 2, 3, 4, 5], - "flux": [30.5, 70, 80.6, 30.2, 60.3], - "zp_mag": [25.0, 25.0, 25.0, 25.0, 25.0], - "zp_flux": [10**10, 10**10, 10**10, 10**10, 10**10], - "error": [10, 10, 10, 10, 10], - "band": ["g", "g", "g", "g", "g"], - } - - if out_col_name is None: - output_column = "flux_mag" - else: - output_column = out_col_name - - # map flux_col to one of the flux columns at the start - col_map = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="error", band_col="band") - ens.from_source_dict(source_dict, column_mapper=col_map) - - if zp_form == "flux": - ens.convert_flux_to_mag("flux", "zp_flux", err_col, zp_form, out_col_name) - - res_mag = ens._source.compute()[output_column].to_list()[0] - assert pytest.approx(res_mag, 0.001) == 21.28925 - - if err_col is not None: - res_err = ens._source.compute()[output_column + "_err"].to_list()[0] - assert pytest.approx(res_err, 0.001) == 0.355979 - else: - assert output_column + "_err" not in ens._source.columns - - elif zp_form == "mag" or zp_form == "magnitude": - ens.convert_flux_to_mag("flux", "zp_mag", err_col, zp_form, out_col_name) - - res_mag = ens._source.compute()[output_column].to_list()[0] - assert pytest.approx(res_mag, 0.001) == 21.28925 - - if err_col is not None: - res_err = ens._source.compute()[output_column + "_err"].to_list()[0] - assert pytest.approx(res_err, 0.001) == 0.355979 - else: - assert output_column + "_err" not in ens._source.columns - - else: - with pytest.raises(ValueError): - ens.convert_flux_to_mag("flux", "zp_mag", err_col, zp_form, "mag") - - def test_find_day_gap_offset(dask_client): ens = Ensemble(client=dask_client) diff --git a/tests/tape_tests/test_ensemble_frame.py b/tests/tape_tests/test_ensemble_frame.py index ce82712e..a75d96bc 100644 --- a/tests/tape_tests/test_ensemble_frame.py +++ b/tests/tape_tests/test_ensemble_frame.py @@ -1,6 +1,6 @@ """ Test EnsembleFrame (inherited from Dask.DataFrame) creation and manipulations. """ import pandas as pd -from tape import Ensemble, EnsembleFrame, TapeFrame +from tape import ColumnMapper, EnsembleFrame, TapeFrame import pytest @@ -26,7 +26,7 @@ def test_from_dict(data_fixture, request): # The calculation for finding the max flux from the data. Note that the # inherited dask compute method must be called to obtain the result. - assert ens_frame.flux.max().compute() == 5.0 + assert ens_frame.flux.max().compute() == 80.6 @pytest.mark.parametrize( "data_fixture", @@ -52,7 +52,7 @@ def test_from_pandas(data_fixture, request): # The calculation for finding the max flux from the data. Note that the # inherited dask compute method must be called to obtain the result. - assert ens_frame.flux.max().compute() == 5.0 + assert ens_frame.flux.max().compute() == 80.6 @pytest.mark.parametrize( @@ -102,4 +102,56 @@ def test_frame_propagation(data_fixture, request): # Test that the inherited dask.DataFrame.compute method returns # the underlying TapeFrame. assert isinstance(ens_frame.compute(), TapeFrame) - assert len(ens_frame) == len(ens_frame.compute()) \ No newline at end of file + assert len(ens_frame) == len(ens_frame.compute()) + +@pytest.mark.parametrize( + "data_fixture", + [ + "ensemble_from_source_dict", + ], +) +@pytest.mark.parametrize("err_col", [None, "error"]) +@pytest.mark.parametrize("zp_form", ["flux", "mag", "magnitude", "lincc"]) +@pytest.mark.parametrize("out_col_name", [None, "mag"]) +def test_convert_flux_to_mag(data_fixture, request, err_col, zp_form, out_col_name): + ens, data = request.getfixturevalue(data_fixture) + + if out_col_name is None: + output_column = "flux_mag" + else: + output_column = out_col_name + + ens_frame = EnsembleFrame.from_dict(data, npartitions=1) + ens_frame.label = TEST_LABEL + ens_frame.ensemble = ens + + if zp_form == "flux": + ens_frame = ens_frame.convert_flux_to_mag("flux", "zp_flux", err_col, zp_form, out_col_name) + + res_mag = ens_frame.compute()[output_column].to_list()[0] + assert pytest.approx(res_mag, 0.001) == 21.28925 + + if err_col is not None: + res_err = ens_frame.compute()[output_column + "_err"].to_list()[0] + assert pytest.approx(res_err, 0.001) == 0.355979 + else: + assert output_column + "_err" not in ens_frame.columns + + elif zp_form == "mag" or zp_form == "magnitude": + ens_frame = ens_frame.convert_flux_to_mag("flux", "zp_mag", err_col, zp_form, out_col_name) + + res_mag = ens_frame.compute()[output_column].to_list()[0] + assert pytest.approx(res_mag, 0.001) == 21.28925 + + if err_col is not None: + res_err = ens_frame.compute()[output_column + "_err"].to_list()[0] + assert pytest.approx(res_err, 0.001) == 0.355979 + else: + assert output_column + "_err" not in ens_frame.columns + + else: + with pytest.raises(ValueError): + ens_frame.convert_flux_to_mag("flux", "zp_mag", err_col, zp_form, "mag") + + # Verify that if we converted to a new frame, it's still an EnsembleFrame. + assert isinstance(ens_frame, EnsembleFrame) \ No newline at end of file From 72b862958ef4b7ff568ff94c034da17673ae8538 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Wed, 30 Aug 2023 14:44:41 -0700 Subject: [PATCH 04/35] Ensembles can now track a group of labeled frames --- src/tape/ensemble.py | 160 +++++++++++++++++++++++++++++- tests/tape_tests/test_ensemble.py | 96 +++++++++++++++++- 2 files changed, 252 insertions(+), 4 deletions(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index 839d39a7..1aa6cd7b 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -13,9 +13,12 @@ from .analysis.structure_function import SF_METHODS from .analysis.structurefunction2 import calc_sf2 +from .ensemble_frame import EnsembleFrame, TapeFrame from .timeseries import TimeSeries from .utils import ColumnMapper +SOURCE_FRAME_LABEL = "source" +OBJECT_FRAME_LABEL = "object" class Ensemble: """Ensemble object is a collection of light curve ids""" @@ -26,6 +29,12 @@ def __init__(self, client=None, **kwargs): self._source = None # Source Table self._object = None # Object Table + self.frames = {} # Frames managed by this Ensemble, keyed by label + + # TODO(wbeebe@uw.edu) Replace self._source and self._object with these + self.source = None # Source Table EnsembleFrame + self.object = None # Object Table EnsembleFrame + self._source_dirty = False # Source Dirty Flag self._object_dirty = False # Object Dirty Flag @@ -67,6 +76,152 @@ def __del__(self): self.client.close() return self + def add_frame(self, frame, label): + """Adds a new frame for the Ensemble to track. + + Parameters + ---------- + frame: `tape.ensemble.EnsembleFrame` + The frame object for the Ensemble to track. + label: `str` + | The label for the Ensemble to use to track the frame. + + Returns + ------- + self: `Ensemble` + + Raises + ------ + ValueError if the label is "source", "object", or already tracked by the Ensemble. + """ + if label == SOURCE_FRAME_LABEL or label == OBJECT_FRAME_LABEL: + raise ValueError( + f"Unable to add frame with reserved label " f"'{label}'" + ) + if label in self.frames: + raise ValueError( + f"Unable to add frame: a frame with label " f"'{label}'" f"is in the Ensemble." + ) + # Assign the frame to the requested tracking label. + frame.label = label + # Update the ensemble to track this labeled frame. + self.update_frame(frame) + return self + + def update_frame(self, frame): + """Updates a frame tracked by the Ensemble or otherwise adds it to the Ensemble. + The frame is tracked by its `EnsembleFrame.label` field. + + Parameters + ---------- + frame: `tape.ensemble.EnsembleFrame` + The frame for the Ensemble to update. If not already tracked, it is added. + + Returns + ------- + self: `Ensemble` + + Raises + ------ + ValueError if the `frame.label` is unpopulated, "source", or "object". + """ + if frame.label is None: + raise ValueError( + f"Unable to update frame with no populated `EnsembleFrame.label`." + ) + if frame.label == SOURCE_FRAME_LABEL or frame.label == OBJECT_FRAME_LABEL: + raise ValueError( + f"Unable to update frame with reserved label " f"'{frame.label}'" + ) + # Ensure this frame is assigned to this Ensemble. + frame.ensemble = self + self.frames[frame.label] = frame + return self + + def drop_frame(self, label): + """Drops a frame tracked by the Ensemble. + + Parameters + ---------- + label: `str` + | The label of the frame to be dropped by the Ensemble. + + Returns + ------- + self: `Ensemble` + + Raises + ------ + ValueError if the label is "source", or "object". + KeyError if the label is not tracked by the Ensemble. + """ + if label == SOURCE_FRAME_LABEL or label == OBJECT_FRAME_LABEL: + raise ValueError( + f"Unable to drop frame with reserved label " f"'{label}'" + ) + if label not in self.frames: + raise KeyError( + f"Unable to drop frame: no frame with label " f"'{label}'" f"is in the Ensemble." + ) + del self.frames[label] + return self + + def select_frame(self, label): + """Selects and returns frame tracked by the Ensemble. + + Parameters + ---------- + label: `str` + | The label of a frame tracked by the Ensemble to be selected. + + Returns + ------- + result: `tape.ensemeble.EnsembleFrame` + + Raises + ------ + KeyError if the label is not tracked by the Ensemble. + """ + if label not in self.frames: + raise KeyError( + f"Unable to select frame: no frame with label" f"'{label}'" f" is in the Ensemble." + ) + return self.frames[label] + + def frame_info(self, labels=None, verbose=True, memory_usage=True, **kwargs): + """Wrapper for calling dask.dataframe.DataFrame.info() on frames tracked by the Ensemble. + + Parameters + ---------- + labels: `list`, optional + A list of labels for Ensemble frames to summarize. + If None, info is printed for all tracked frames. + verbose: `bool`, optional + Whether to print the whole summary + memory_usage: `bool`, optional + Specifies whether total memory usage of the DataFrame elements + (including the index) should be displayed. + **kwargs: + keyword arguments passed along to + `dask.dataframe.DataFrame.info()` + Returns + ------- + None + + Raises + ------ + KeyError if a label in labels is not tracked by the Ensemble. + """ + if labels is None: + labels = self.frames.keys() + for label in labels: + if label not in self.frames: + raise KeyError( + f"Unable to get frame info: no frame with label " f"'{label}'" f" is in the Ensemble." + ) + print(label, "Frame") + print(self.frames[label].info(verbose=verbose, memory_usage=memory_usage, **kwargs)) + def insert_sources( self, obj_ids, @@ -174,7 +329,7 @@ def client_info(self): return self.client # Prints Dask dashboard to screen def info(self, verbose=True, memory_usage=True, **kwargs): - """Wrapper for dask.dataframe.DataFrame.info() + """Wrapper for dask.dataframe.DataFrame.info() for the Source and Object tables Parameters ---------- @@ -185,8 +340,7 @@ def info(self, verbose=True, memory_usage=True, **kwargs): (including the index) should be displayed. Returns ---------- - counts: `pandas.series` - A series of counts by object + None """ # Sync tables if user wants to retrieve their information self._lazy_sync_tables(table="all") diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index 49f92238..e29c89b9 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -6,7 +6,7 @@ import pandas as pd import pytest -from tape import Ensemble +from tape import Ensemble, EnsembleFrame, TapeFrame from tape.analysis.stetsonj import calc_stetson_J from tape.analysis.structure_function.base_argument_container import StructureFunctionArgumentContainer from tape.analysis.structurefunction2 import calc_sf2 @@ -78,6 +78,97 @@ def test_available_datasets(dask_client): assert isinstance(datasets, dict) assert len(datasets) > 0 # Find at least one +@pytest.mark.parametrize( + "data_fixture", + [ + "ensemble_from_source_dict", + ], +) +def test_frame_tracking(data_fixture, request): + """ + Tests a workflow of adding and removing the frames tracked by the Ensemble. + """ + ens, data = request.getfixturevalue(data_fixture) + + # Construct frames for the Ensemble to track. For this test, the underlying data is irrelevant. + ens_frame1 = EnsembleFrame.from_dict(data, npartitions=1) + ens_frame2 = EnsembleFrame.from_dict(data, npartitions=1) + ens_frame3 = EnsembleFrame.from_dict(data, npartitions=1) + ens_frame4 = EnsembleFrame.from_dict(data, npartitions=1) + + # Labels to give the EnsembleFrames + label1, label2, label3, label4 = "frame1", "frame2", "frame3", "frame4" + + assert not ens.frames + + # TODO(wbeebe@uw.edu) Remove once Ensemble.source and Ensemble.object are populated by loaders + ens.source = EnsembleFrame.from_tapeframe( + TapeFrame(ens._source), label="source", npartitions=1) + ens.object = EnsembleFrame.from_tapeframe( + TapeFrame(ens._source), label="object", npartitions=1) + ens.frames["source"] = ens.source + ens.frames["object"] = ens.object + + # Check that we can select source and object frames + assert len(ens.frames) == 2 + assert ens.select_frame("source") is ens.source + assert ens.select_frame("object") is ens.object + + # Validate that new source and object frames can't be added or updated. + with pytest.raises(ValueError): + ens.add_frame(ens_frame1, "source") + with pytest.raises(ValueError): + ens.add_frame(ens_frame1, "object") + with pytest.raises(ValueError): + ens.update_frame(ens.source) + with pytest.raises(ValueError): + ens.update_frame(ens.object) + + # Test that we can add and select a new ensemble frame + assert ens.add_frame(ens_frame1, label1).select_frame(label1) is ens_frame1 + assert len(ens.frames) == 3 + + # Validate that we can't add a new frame that uses an exisiting label + with pytest.raises(ValueError): + ens.add_frame(ens_frame2, label1) + + # We add two more frames to track + ens.add_frame(ens_frame2, label2).add_frame(ens_frame3, label3) + assert ens.select_frame(label2) is ens_frame2 + assert ens.select_frame(label3) is ens_frame3 + assert len(ens.frames) == 5 + + # Now we begin dropping frames. First verifyt that we can't drop object or source. + with pytest.raises(ValueError): + ens.drop_frame("source") + with pytest.raises(ValueError): + ens.drop_frame("object") + + # And verify that we can't call drop with an unknown label. + with pytest.raises(KeyError): + ens.drop_frame("nonsense") + + # Drop an existing frame and that it can no longer be selected. + ens.drop_frame(label3) + assert len(ens.frames) == 4 + with pytest.raises(KeyError): + ens.select_frame(label3) + + # Update the ensemble with the dropped frame, and then select the frame + assert ens.update_frame(ens_frame3).select_frame(label3) is ens_frame3 + assert len(ens.frames) == 5 + + # Update the ensemble with a new frame, verifying a missing label generates an error. + with pytest.raises(ValueError): + ens.update_frame(ens_frame4) + ens_frame4.label = label4 + assert ens.update_frame(ens_frame4).select_frame(label4) is ens_frame4 + assert len(ens.frames) == 6 + + # Change the label of the 4th ensemble frame to verify update overrides an existing frame + ens_frame4.label = label3 + assert ens.update_frame(ens_frame4).select_frame(label3) is ens_frame4 + assert len(ens.frames) == 6 def test_from_rrl_dataset(dask_client): """ @@ -291,6 +382,9 @@ def test_core_wrappers(parquet_ensemble): # Just test if these execute successfully parquet_ensemble.client_info() parquet_ensemble.info() + parquet_ensemble.frame_info() + with pytest.raises(KeyError): + parquet_ensemble.frame_info(labels=["source", "invalid_label"]) parquet_ensemble.columns() parquet_ensemble.head(n=5) parquet_ensemble.tail(n=5) From 31281412bfb3f62c66aa5028a500c2a3213f012b Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Thu, 31 Aug 2023 11:18:52 -0700 Subject: [PATCH 05/35] Preserve EnsembleFrame metadata after assign() --- src/tape/ensemble_frame.py | 26 +++++++++++++++++++++++++ tests/tape_tests/test_ensemble_frame.py | 6 ++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index 70098c13..8ba7b4cc 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -44,6 +44,32 @@ def _propagate_metadata(self, new_frame): def copy(self): self_copy = super().copy() return self._propagate_metadata(self_copy) + + def assign(self, **kwargs): + """Assign new columns to a DataFrame. + + This docstring was copied from dask.dataframe.DataFrame.assign. + + Some inconsistencies with the Dask version may exist. + + Returns a new object with all original columns in addition to new ones. Existing columns + that are re-assigned will be overwritten. + + Parameters + ---------- + **kwargs: `dict` + The column names are keywords. If the values are callable, they are computed on the + DataFrame and assigned to the new columns. The callable must not change input DataFrame + (though pandas doesn’t check it). If the values are not callable, (e.g. a Series, + scalar, or array), they are simply assigned. + + Returns + ---------- + result: `tape._Frame` + The modifed frame + """ + result = super().assign(**kwargs) + return self._propagate_metadata(result) class TapeSeries(pd.Series): """A barebones extension of a Pandas series to be used for underlying Ensmeble data. diff --git a/tests/tape_tests/test_ensemble_frame.py b/tests/tape_tests/test_ensemble_frame.py index a75d96bc..559a85c6 100644 --- a/tests/tape_tests/test_ensemble_frame.py +++ b/tests/tape_tests/test_ensemble_frame.py @@ -151,7 +151,9 @@ def test_convert_flux_to_mag(data_fixture, request, err_col, zp_form, out_col_na else: with pytest.raises(ValueError): - ens_frame.convert_flux_to_mag("flux", "zp_mag", err_col, zp_form, "mag") + ens_frame = ens_frame.convert_flux_to_mag("flux", "zp_mag", err_col, zp_form, "mag") # Verify that if we converted to a new frame, it's still an EnsembleFrame. - assert isinstance(ens_frame, EnsembleFrame) \ No newline at end of file + assert isinstance(ens_frame, EnsembleFrame) + assert ens_frame.label == TEST_LABEL + assert ens_frame.ensemble is ens \ No newline at end of file From 8db79e03eebff792bfe797123bdeab23e89de928 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Fri, 1 Sep 2023 17:23:13 -0700 Subject: [PATCH 06/35] Parquet support for frame subclasses checkpoint --- src/tape/ensemble.py | 115 ++++++++++- src/tape/ensemble_frame.py | 253 +++++++++++++++++++++++- tests/tape_tests/conftest.py | 20 ++ tests/tape_tests/test_ensemble.py | 42 ++-- tests/tape_tests/test_ensemble_frame.py | 75 ++++++- 5 files changed, 471 insertions(+), 34 deletions(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index 295b469a..b9d51c8f 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -12,10 +12,11 @@ from .analysis.feature_extractor import BaseLightCurveFeature, FeatureExtractor from .analysis.structure_function import SF_METHODS from .analysis.structurefunction2 import calc_sf2 -from .ensemble_frame import EnsembleFrame, TapeFrame +from .ensemble_frame import ObjectFrame, SourceFrame from .timeseries import TimeSeries from .utils import ColumnMapper +# TODO import from EnsembleFrame...? SOURCE_FRAME_LABEL = "source" OBJECT_FRAME_LABEL = "object" @@ -1108,7 +1109,7 @@ def from_parquet( source_file: 'str' Path to a parquet file, or multiple parquet files that contain source information to be read into the ensemble - object_file: 'str' + object_file: 'str', optional Path to a parquet file, or multiple parquet files that contain object information. If not specified, it is generated from the source table @@ -1199,6 +1200,114 @@ def from_parquet( self._source = self._source.repartition(partition_size=partition_size) return self + + def objsor_from_parquet( + self, + source_file, + object_file, + column_mapper=None, + provenance_label="survey_1", + sync_tables=True, + additional_cols=True, + npartitions=None, + partition_size=None, + **kwargs, + ): + """Read in parquet file(s) into an ensemble object + + Parameters + ---------- + source_file: 'str' + Path to a parquet file, or multiple parquet files that contain + source information to be read into the ensemble + object_file: 'str' + Path to a parquet file, or multiple parquet files that contain + object information. + column_mapper: 'ColumnMapper' object + If provided, the ColumnMapper is used to populate relevant column + information mapped from the input dataset. + provenance_label: 'str', optional + Determines the label to use if a provenance column is generated + sync_tables: 'bool', optional + In the case where object files are loaded in, determines whether an + initial sync is performed between the object and source tables. If + not performed, dynamic information like the number of observations + may be out of date until a sync is performed internally. + additional_cols: 'bool', optional + Boolean to indicate whether to carry in columns beyond the + critical columns, true will, while false will only load the columns + containing the critical quantities (id,time,flux,err,band) + npartitions: `int`, optional + If specified, attempts to repartition the ensemble to the specified + number of partitions + partition_size: `int`, optional + If specified, attempts to repartition the ensemble to partitions + of size `partition_size`. + + Returns + ---------- + ensemble: `tape.ensemble.Ensemble` + The ensemble object with parquet data loaded + """ + + # load column mappings + self._load_column_mapper(column_mapper, **kwargs) + + # Handle additional columnss + if additional_cols: + columns = None # None will prompt read_parquet to read in all cols + else: + columns = [self._time_col, self._flux_col, self._err_col, self._band_col] + if self._provenance_col is not None: + columns.append(self._provenance_col) + if self._nobs_tot_col is not None: + columns.append(self._nobs_tot_col) + if self._nobs_band_cols is not None: + for col in self._nobs_band_cols: + columns.append(col) + + # Read in the source parquet file(s) + self.source = SourceFrame.from_parquet(source_file, index=self._id_col, columns=columns, + ensemble=self) + + # Read in the object file(s) + self.object = ObjectFrame.from_parquet( + object_file, index=self._id_col, ensemble=self) + + if self._nobs_band_cols is None: + # sets empty nobs cols in object + unq_filters = np.unique(self.source[self._band_col]) + self._nobs_band_cols = [f"nobs_{filt}" for filt in unq_filters] + for col in self._nobs_band_cols: + self.object[col] = np.nan + + # Handle nobs_total column + if self._nobs_tot_col is None: + self.object["nobs_total"] = np.nan + self._nobs_tot_col = "nobs_total" + + # Optionally sync the tables, recalculates nobs columns + if sync_tables: + # TODO(wbeebe@uw.edu) Make this meaningful as part of milestone 4 + self._source_dirty = True + self._object_dirty = True + self._sync_tables() + + # Generate a provenance column if not provided + if self._provenance_col is None: + self.source["provenance"] = self.source.apply( + lambda x: provenance_label, axis=1, meta=pd.Series(name="provenance", dtype=str) + ) + self._provenance_col = "provenance" + + if npartitions and npartitions > 1: + self.source = self.source.repartition(npartitions=npartitions) + elif partition_size: + self.source = self.source.repartition(partition_size=partition_size) + + self.frames[self.source.label] = self.source + self.frames[self.object.label] = self.object + return self def from_dataset(self, dataset, **kwargs): """Load the ensemble from a TAPE dataset. @@ -1318,7 +1427,7 @@ def _generate_object_table(self): zero_pdf = pd.DataFrame(rows, dtype=int).set_index(self._id_col) zero_ddf = dd.from_pandas(zero_pdf, sort=True, npartitions=1) - # Concatonate the zero dataframe onto the results. + # Concatenate the zero dataframe onto the results. res = dd.concat([res, zero_ddf], interleave_partitions=True).astype(int) res = res.repartition(npartitions=prev_partitions) diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index 8ba7b4cc..ee5096eb 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -10,6 +10,69 @@ import numpy as np import pandas as pd +from functools import partial +from dask.dataframe.io.parquet.arrow import ( + ArrowDatasetEngine as DaskArrowDatasetEngine, + ) + +SOURCE_FRAME_LABEL = "source" +OBJECT_FRAME_LABEL = "object" + +class TapeArrowEngine(DaskArrowDatasetEngine): + """ + Engine for reading parquet files into Tape and assigning the appropriate Dask meta. + + Based off of the approach used in dask_geopandas.io + """ + + @classmethod + def _update_meta(cls, meta, schema): + """ + Convert meta to a TapeFrame + """ + return TapeFrame(meta) + + @classmethod + def _create_dd_meta(cls, dataset_info, use_nullable_dtypes=False): + """Overriding private method for dask >= 2021.10.0""" + meta = super()._create_dd_meta(dataset_info) + + schema = dataset_info["schema"] + if not schema.names and not schema.metadata: + if len(list(dataset_info["ds"].get_fragments())) == 0: + raise ValueError( + "No dataset parts discovered. Use dask.dataframe.read_parquet " + "to read it as an empty DataFrame" + ) + meta = cls._update_meta(meta, schema) + return meta + +class TapeSourceArrowEngine(TapeArrowEngine): + """ + Barebones subclass of TapeArrowEngine for assigning the meta when loading from a parquet file + of source data. + """ + + @classmethod + def _update_meta(cls, meta, schema): + """ + Convert meta to a TapeSourceFrame + """ + return TapeSourceFrame(meta) + +class TapeObjectArrowEngine(TapeArrowEngine): + """ + Barebones subclass of TapeArrowEngine for assigning the meta when loading from a parquet file + of object data. + """ + + @classmethod + def _update_meta(cls, meta, schema): + """ + Convert meta to a TapeObjectFrame + """ + return TapeObjectFrame(meta) + class _Frame(dd.core._Frame): """Base class for extensions of Dask Dataframes that track additional Ensemble-related metadata.""" @@ -72,7 +135,7 @@ def assign(self, **kwargs): return self._propagate_metadata(result) class TapeSeries(pd.Series): - """A barebones extension of a Pandas series to be used for underlying Ensmeble data. + """A barebones extension of a Pandas series to be used for underlying Ensemble data. See https://pandas.pydata.org/docs/development/extending.html#subclassing-pandas-data-structures """ @@ -85,7 +148,7 @@ def _constructor_sliced(self): return TapeSeries class TapeFrame(pd.DataFrame): - """A barebones extension of a Pandas frame to be used for underlying Ensmeble data. + """A barebones extension of a Pandas frame to be used for underlying Ensemble data. See https://pandas.pydata.org/docs/development/extending.html#subclassing-pandas-data-structures """ @@ -120,7 +183,7 @@ class EnsembleFrame(_Frame, dd.core.DataFrame): def __getitem__(self, key): result = super().__getitem__(key) if isinstance(result, _Frame): - # Ensures that we have any + # Ensures that any _Frame metadata is propagated. result = self._propagate_metadata(result) return result @@ -215,6 +278,156 @@ def convert_flux_to_mag(self, return result + @classmethod + def from_parquet( + cl, + path, + index=None, + columns=None, + ensemble=None, + ): + """ Returns an EnsembleFrame constructed from loading a parquet file. + Parameters + ---------- + path: `str` or `list` + Source directory for data, or path(s) to individual parquet files. Prefix with a + protocol like s3:// to read from alternative filesystems. To read from multiple + files you can pass a globstring or a list of paths, with the caveat that they must all + have the same protocol. + columns: `str` or `list`, optional + Field name(s) to read in as columns in the output. By default all non-index fields will + be read (as determined by the pandas parquet metadata, if present). Provide a single + field name instead of a list to read in the data as a Series. + index: `str`, `list`, `False`, optional + Field name(s) to use as the output frame index. Default is None and index will be + inferred from the pandas parquet file metadata, if present. Use False to read all + fields as columns. + ensemble: `tape.ensemble.Ensemble`, optional + | A link to the Ensmeble object that owns this frame. + Returns + result: `tape.EnsembleFrame` + The constructed EnsembleFrame object. + """ + # Read the parquet file with an engine that will assume the meta is a TapeFrame which Dask will + # instantiate as EnsembleFrame via its dispatcher. + result = dd.read_parquet( + path, index=index, columns=columns, split_row_groups=True, engine=TapeArrowEngine, + ) + result.ensemble=ensemble + + return result + +class TapeSourceFrame(TapeFrame): + """A barebones extension of a Pandas frame to be used for underlying Ensemble source data + + See https://pandas.pydata.org/docs/development/extending.html#subclassing-pandas-data-structures + """ + @property + def _constructor(self): + return TapeSourceFrame + + @property + def _constructor_expanddim(self): + return TapeSourceFrame + +class TapeObjectFrame(TapeFrame): + """A barebones extension of a Pandas frame to be used for underlying Ensemble object data. + + See https://pandas.pydata.org/docs/development/extending.html#subclassing-pandas-data-structures + """ + @property + def _constructor(self): + return TapeObjectFrame + + @property + def _constructor_expanddim(self): + return TapeObjectFrame + +class SourceFrame(EnsembleFrame): + """ A subclass of EnsembleFrame for Source data. """ + + _partition_type = TapeSourceFrame # Tracks the underlying data type + + def __init__(self, dsk, name, meta, divisions, ensemble=None): + super().__init__(dsk, name, meta, divisions) + self.label = SOURCE_FRAME_LABEL # A label used by the Ensemble to identify this frame. + self.ensemble = ensemble # The Ensemble object containing this frame. + + def __getitem__(self, key): + result = super().__getitem__(key) + if isinstance(result, _Frame): + # Ensures that we have any metadata + result = self._propagate_metadata(result) + return result + + @classmethod + def from_parquet( + cl, + path, + index=None, + columns=None, + ensemble=None, + ): + """ Returns a SourceFrame constructed from loading a parquet file. + Parameters + ---------- + path: `str` or `list` + Source directory for data, or path(s) to individual parquet files. Prefix with a + protocol like s3:// to read from alternative filesystems. To read from multiple + files you can pass a globstring or a list of paths, with the caveat that they must all + have the same protocol. + columns: `str` or `list`, optional + Field name(s) to read in as columns in the output. By default all non-index fields will + be read (as determined by the pandas parquet metadata, if present). Provide a single + field name instead of a list to read in the data as a Series. + index: `str`, `list`, `False`, optional + Field name(s) to use as the output frame index. Default is None and index will be + inferred from the pandas parquet file metadata, if present. Use False to read all + fields as columns. + ensemble: `tape.ensemble.Ensemble`, optional + | A link to the Ensmeble object that owns this frame. + Returns + result: `tape.EnsembleFrame` + The constructed EnsembleFrame object. + """ + # Read the source parquet file with an engine that will assume the meta is a + # TapeSourceFrame which tells Dask to instantiate a SourceFrame via its + # dispatcher. + result = dd.read_parquet( + path, index=index, columns=columns, split_row_groups=True, engine=TapeSourceArrowEngine, + ) + result.ensemble=ensemble + result.label = SOURCE_FRAME_LABEL + + return result + +class ObjectFrame(EnsembleFrame): + """ A subclass of EnsembleFrame for Object data. """ + + _partition_type = TapeObjectFrame # Tracks the underlying data type + + def __init__(self, dsk, name, meta, divisions, ensemble=None): + super().__init__(dsk, name, meta, divisions) + self.label = OBJECT_FRAME_LABEL # A label used by the Ensemble to identify this frame. + self.ensemble = ensemble # The Ensemble object containing this frame. + + @classmethod + def from_parquet( + cl, + path, + index=None, + columns=None, + ensemble=None, + ): + # Read in the object Parquet file + result = dd.read_parquet( + path, index=index, columns=columns, split_row_groups=True, engine=TapeObjectArrowEngine, + ) + result.ensemble=ensemble + result.label= OBJECT_FRAME_LABEL + + return result + """ Dask Dataframes are constructed indirectly using method dispatching and inference on the underlying data. So to ensure our subclasses behave correctly, we register the methods @@ -228,6 +441,8 @@ def convert_flux_to_mag(self, """ get_parallel_type.register(TapeSeries, lambda _: EnsembleSeries) get_parallel_type.register(TapeFrame, lambda _: EnsembleFrame) +get_parallel_type.register(TapeObjectFrame, lambda _: ObjectFrame) +get_parallel_type.register(TapeSourceFrame, lambda _: SourceFrame) @make_meta_dispatch.register(TapeSeries) def make_meta_series(x, index=None): @@ -259,4 +474,34 @@ def _nonempty_tapeseries(x, index=None): def _nonempty_tapeseries(x, index=None): # Construct a new TapeFrame with the same underlying data. df = meta_nonempty_dataframe(x) - return TapeFrame(df) \ No newline at end of file + return TapeFrame(df) + +@make_meta_dispatch.register(TapeObjectFrame) +def make_meta_frame(x, index=None): + # Create an empty TapeObjectFrame to use as Dask's underlying object meta. + result = x.head(0) + # Re-index if requested + if index is not None: + result = result.reindex(index[:0]) + return result + +@meta_nonempty.register(TapeObjectFrame) +def _nonempty_tapesourceframe(x, index=None): + # Construct a new TapeObjectFrame with the same underlying data. + df = meta_nonempty_dataframe(x) + return TapeObjectFrame(df) + +@make_meta_dispatch.register(TapeSourceFrame) +def make_meta_frame(x, index=None): + # Create an empty TapeSourceFrame to use as Dask's underlying object meta. + result = x.head(0) + # Re-index if requested + if index is not None: + result = result.reindex(index[:0]) + return result + +@meta_nonempty.register(TapeSourceFrame) +def _nonempty_tapesourceframe(x, index=None): + # Construct a new TapeSourceFrame with the same underlying data. + df = meta_nonempty_dataframe(x) + return TapeSourceFrame(df) \ No newline at end of file diff --git a/tests/tape_tests/conftest.py b/tests/tape_tests/conftest.py index f334a24b..15fe6f92 100644 --- a/tests/tape_tests/conftest.py +++ b/tests/tape_tests/conftest.py @@ -31,6 +31,26 @@ def parquet_ensemble_without_client(): return ens +@pytest.fixture +def parquet_files_and_ensemble_without_client(): + """Create an Ensemble from parquet data without a dask client.""" + ens = Ensemble(client=False) + source_file = "tests/tape_tests/data/source/test_source.parquet" + object_file = "tests/tape_tests/data/object/test_object.parquet" + colmap = ColumnMapper().assign( + id_col="ps1_objid", + time_col="midPointTai", + flux_col="psFlux", + err_col="psFluxErr", + band_col="filterName", + ) + ens.from_parquet( + source_file, + object_file, + column_mapper=colmap + ) + + return ens, source_file, object_file, colmap # pylint: disable=redefined-outer-name @pytest.fixture diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index 69406bb8..a883ae33 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -6,7 +6,7 @@ import pandas as pd import pytest -from tape import Ensemble, EnsembleFrame, TapeFrame +from tape import Ensemble, ObjectFrame, SourceFrame from tape.analysis.stetsonj import calc_stetson_J from tape.analysis.structure_function.base_argument_container import StructureFunctionArgumentContainer from tape.analysis.structurefunction2 import calc_sf2 @@ -82,38 +82,38 @@ def test_available_datasets(dask_client): @pytest.mark.parametrize( "data_fixture", [ - "ensemble_from_source_dict", + "parquet_files_and_ensemble_without_client", ], ) def test_frame_tracking(data_fixture, request): """ Tests a workflow of adding and removing the frames tracked by the Ensemble. """ - ens, data = request.getfixturevalue(data_fixture) + ens, source_file, object_file, colmap = request.getfixturevalue(data_fixture) - # Construct frames for the Ensemble to track. For this test, the underlying data is irrelevant. - ens_frame1 = EnsembleFrame.from_dict(data, npartitions=1) - ens_frame2 = EnsembleFrame.from_dict(data, npartitions=1) - ens_frame3 = EnsembleFrame.from_dict(data, npartitions=1) - ens_frame4 = EnsembleFrame.from_dict(data, npartitions=1) + ens = ens.objsor_from_parquet(source_file, object_file, column_mapper=colmap) - # Labels to give the EnsembleFrames - label1, label2, label3, label4 = "frame1", "frame2", "frame3", "frame4" - - assert not ens.frames - - # TODO(wbeebe@uw.edu) Remove once Ensemble.source and Ensemble.object are populated by loaders - ens.source = EnsembleFrame.from_tapeframe( - TapeFrame(ens._source), label="source", npartitions=1) - ens.object = EnsembleFrame.from_tapeframe( - TapeFrame(ens._source), label="object", npartitions=1) - ens.frames["source"] = ens.source - ens.frames["object"] = ens.object + # Since we load the ensemble from a parquet, we expect the Source and Object frames to be populated. + assert len(ens.frames) == 2 + assert isinstance(ens.select_frame("source"), SourceFrame) + assert isinstance(ens.select_frame("object"), ObjectFrame) # Check that we can select source and object frames assert len(ens.frames) == 2 assert ens.select_frame("source") is ens.source + assert isinstance(ens.select_frame("source"), SourceFrame) assert ens.select_frame("object") is ens.object + assert isinstance(ens.select_frame("object"), ObjectFrame) + + # Construct some result frames for the Ensemble to track. Underlying data is irrelevant for + # this test. + ens_frame1 = ens.select_frame("source").copy() + ens_frame2 = ens.select_frame("source").copy() + ens_frame3 = ens.select_frame("source").copy() + ens_frame4 = ens.select_frame("source").copy() + + # Labels to give the EnsembleFrames + label1, label2, label3, label4 = "frame1", "frame2", "frame3", "frame4" # Validate that new source and object frames can't be added or updated. with pytest.raises(ValueError): @@ -139,7 +139,7 @@ def test_frame_tracking(data_fixture, request): assert ens.select_frame(label3) is ens_frame3 assert len(ens.frames) == 5 - # Now we begin dropping frames. First verifyt that we can't drop object or source. + # Now we begin dropping frames. First verify that we can't drop object or source. with pytest.raises(ValueError): ens.drop_frame("source") with pytest.raises(ValueError): diff --git a/tests/tape_tests/test_ensemble_frame.py b/tests/tape_tests/test_ensemble_frame.py index 559a85c6..d85d4bbe 100644 --- a/tests/tape_tests/test_ensemble_frame.py +++ b/tests/tape_tests/test_ensemble_frame.py @@ -1,10 +1,14 @@ """ Test EnsembleFrame (inherited from Dask.DataFrame) creation and manipulations. """ +import numpy as np import pandas as pd -from tape import ColumnMapper, EnsembleFrame, TapeFrame +from tape import ColumnMapper, EnsembleFrame, ObjectFrame, SourceFrame, TapeObjectFrame, TapeSourceFrame, TapeFrame import pytest TEST_LABEL = "test_frame" +SOURCE_LABEL = "source" +OBJECT_LABEL = "object" + # pylint: disable=protected-access @pytest.mark.parametrize( @@ -61,7 +65,7 @@ def test_from_pandas(data_fixture, request): "ensemble_from_source_dict", ], ) -def test_frame_propagation(data_fixture, request): +def test_ensemble_frame_propagation(data_fixture, request): """ Test ensuring that slices and copies of an EnsembleFrame or still the same class. """ @@ -90,9 +94,9 @@ def test_frame_propagation(data_fixture, request): # Test that the output of an EnsembleFrame query is still an EnsembleFrame queried_rows = ens_frame.query("flux > 3.0") assert isinstance(queried_rows, EnsembleFrame) - assert isinstance(filtered_frame._meta, TapeFrame) - assert filtered_frame.label == TEST_LABEL - assert filtered_frame.ensemble == ens + assert isinstance(queried_rows._meta, TapeFrame) + assert queried_rows.label == TEST_LABEL + assert queried_rows.ensemble == ens # Test that head returns a subset of the underlying TapeFrame. h = ens_frame.head(5) @@ -156,4 +160,63 @@ def test_convert_flux_to_mag(data_fixture, request, err_col, zp_form, out_col_na # Verify that if we converted to a new frame, it's still an EnsembleFrame. assert isinstance(ens_frame, EnsembleFrame) assert ens_frame.label == TEST_LABEL - assert ens_frame.ensemble is ens \ No newline at end of file + assert ens_frame.ensemble is ens + +@pytest.mark.parametrize( + "data_fixture", + [ + "parquet_files_and_ensemble_without_client", + ], +) +def test_object_and_source_frame_propagation(data_fixture, request): + """ + Test that SourceFrame and ObjectFrame metadata and class type is correctly preserved across + typical Pandas operations. + """ + ens, source_file, object_file, _ = request.getfixturevalue(data_fixture) + + assert ens is not None + + # Create a SourceFrame from a parquet file + source_frame = SourceFrame.from_parquet(source_file, ensemble=ens) + + assert isinstance(source_frame, EnsembleFrame) + assert isinstance(source_frame, SourceFrame) + assert isinstance(source_frame._meta, TapeSourceFrame) + + assert source_frame.ensemble is not None + assert source_frame.ensemble == ens + assert source_frame.ensemble is ens + + # Perform a series of operations on the SourceFrame and then verify the result is still a + # proper SourceFrame with appropriate metadata propagated. + mean_ps_flux = source_frame["psFlux"].mean().compute() + result_source_frame = source_frame.copy()[["psFlux", "psFluxErr"]]#.query("psFlux > " + str(mean_ps_flux)) + assert isinstance(result_source_frame, SourceFrame) + assert isinstance(result_source_frame._meta, TapeSourceFrame) + assert len(result_source_frame) > 0 + assert result_source_frame.label == SOURCE_LABEL + assert result_source_frame.ensemble is not None + assert result_source_frame.ensemble is ens + + """ + # Create an ObjectFrame from a parquet file + object_frame = ObjectFrame.from_parquet( + object_file, + ensemble=ens, + index="ps1_objid", + ) + + assert isinstance(object_frame, EnsembleFrame) + assert isinstance(object_frame, ObjectFrame) + assert isinstance(object_frame._meta, TapeObjectFrame) + + # Perform a series of operations on the ObjectFrame and then verify the result is still a + # proper ObjectFrame with appropriate metadata propagated. + result_object_frame = object_frame.copy()[["nobs_g", "nobs_total"]].query("nobs_total > 3.0") + assert isinstance(result_object_frame, ObjectFrame) + assert isinstance(result_object_frame._meta, TapeObjectFrame) + assert result_object_frame.label == OBJECT_LABEL + assert result_object_frame.ensemble is ens + + """ \ No newline at end of file From 657a2a71622c3af3a4fafcd990ea5879f0ce96fd Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Tue, 5 Sep 2023 10:58:42 -0700 Subject: [PATCH 07/35] Reverting changes to tests --- tests/tape_tests/test_ensemble_frame.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/tape_tests/test_ensemble_frame.py b/tests/tape_tests/test_ensemble_frame.py index d85d4bbe..0cbd6d15 100644 --- a/tests/tape_tests/test_ensemble_frame.py +++ b/tests/tape_tests/test_ensemble_frame.py @@ -93,10 +93,10 @@ def test_ensemble_frame_propagation(data_fixture, request): # Test that the output of an EnsembleFrame query is still an EnsembleFrame queried_rows = ens_frame.query("flux > 3.0") - assert isinstance(queried_rows, EnsembleFrame) - assert isinstance(queried_rows._meta, TapeFrame) - assert queried_rows.label == TEST_LABEL - assert queried_rows.ensemble == ens + assert isinstance(filtered_frame, EnsembleFrame) + assert isinstance(filtered_frame._meta, TapeFrame) + assert filtered_frame.label == TEST_LABEL + assert filtered_frame.ensemble == ens # Test that head returns a subset of the underlying TapeFrame. h = ens_frame.head(5) @@ -191,7 +191,7 @@ def test_object_and_source_frame_propagation(data_fixture, request): # Perform a series of operations on the SourceFrame and then verify the result is still a # proper SourceFrame with appropriate metadata propagated. mean_ps_flux = source_frame["psFlux"].mean().compute() - result_source_frame = source_frame.copy()[["psFlux", "psFluxErr"]]#.query("psFlux > " + str(mean_ps_flux)) + result_source_frame = source_frame.copy()[["psFlux", "psFluxErr"]] assert isinstance(result_source_frame, SourceFrame) assert isinstance(result_source_frame._meta, TapeSourceFrame) assert len(result_source_frame) > 0 @@ -199,7 +199,6 @@ def test_object_and_source_frame_propagation(data_fixture, request): assert result_source_frame.ensemble is not None assert result_source_frame.ensemble is ens - """ # Create an ObjectFrame from a parquet file object_frame = ObjectFrame.from_parquet( object_file, @@ -213,10 +212,8 @@ def test_object_and_source_frame_propagation(data_fixture, request): # Perform a series of operations on the ObjectFrame and then verify the result is still a # proper ObjectFrame with appropriate metadata propagated. - result_object_frame = object_frame.copy()[["nobs_g", "nobs_total"]].query("nobs_total > 3.0") + result_object_frame = object_frame.copy()[["nobs_g", "nobs_total"]] assert isinstance(result_object_frame, ObjectFrame) assert isinstance(result_object_frame._meta, TapeObjectFrame) assert result_object_frame.label == OBJECT_LABEL - assert result_object_frame.ensemble is ens - - """ \ No newline at end of file + assert result_object_frame.ensemble is ens \ No newline at end of file From e8de263ec6a1583160e9b81f34e715c95cad03cd Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Tue, 5 Sep 2023 14:02:26 -0700 Subject: [PATCH 08/35] Adds test for objsor_from_parquet --- src/tape/ensemble.py | 19 ++++++-------- src/tape/ensemble_frame.py | 4 +-- tests/tape_tests/conftest.py | 6 ++--- tests/tape_tests/test_ensemble.py | 43 +++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index b9d51c8f..f9f0e609 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -379,6 +379,9 @@ def compute(self, table=None, **kwargs): A single pandas data frame for the specified table or a tuple of (object, source) data frames. """ + # TODO(wbeebe@uw.edu): Merge this duplicate logic as part of milestone 4 + if self.object is not None and self.source is not None: + return (self.object.compute(**kwargs), self.source.compute(**kwargs)) if table: self._lazy_sync_tables(table) if table == "object": @@ -1213,7 +1216,7 @@ def objsor_from_parquet( partition_size=None, **kwargs, ): - """Read in parquet file(s) into an ensemble object + """Read in parquet file(s) for the object and source tables into an Ensemble object. Parameters ---------- @@ -1268,11 +1271,10 @@ def objsor_from_parquet( # Read in the source parquet file(s) self.source = SourceFrame.from_parquet(source_file, index=self._id_col, columns=columns, - ensemble=self) + ensemble=self) # Read in the object file(s) - self.object = ObjectFrame.from_parquet( - object_file, index=self._id_col, ensemble=self) + self.object = ObjectFrame.from_parquet(object_file, index=self._id_col, ensemble=self) if self._nobs_band_cols is None: # sets empty nobs cols in object @@ -1286,13 +1288,8 @@ def objsor_from_parquet( self.object["nobs_total"] = np.nan self._nobs_tot_col = "nobs_total" - # Optionally sync the tables, recalculates nobs columns - if sync_tables: - # TODO(wbeebe@uw.edu) Make this meaningful as part of milestone 4 - self._source_dirty = True - self._object_dirty = True - self._sync_tables() - + # TODO(wbeebe@uw.edu) Add in table syncing logic as part of milestone 4 + # Generate a provenance column if not provided if self._provenance_col is None: self.source["provenance"] = self.source.apply( diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index ee5096eb..55348348 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -15,8 +15,8 @@ ArrowDatasetEngine as DaskArrowDatasetEngine, ) -SOURCE_FRAME_LABEL = "source" -OBJECT_FRAME_LABEL = "object" +SOURCE_FRAME_LABEL = "source" # Reserved label for source table +OBJECT_FRAME_LABEL = "object" # Reserved label for object table. class TapeArrowEngine(DaskArrowDatasetEngine): """ diff --git a/tests/tape_tests/conftest.py b/tests/tape_tests/conftest.py index 15fe6f92..770dae91 100644 --- a/tests/tape_tests/conftest.py +++ b/tests/tape_tests/conftest.py @@ -44,12 +44,10 @@ def parquet_files_and_ensemble_without_client(): err_col="psFluxErr", band_col="filterName", ) - ens.from_parquet( + ens = ens.objsor_from_parquet( source_file, object_file, - column_mapper=colmap - ) - + column_mapper=colmap) return ens, source_file, object_file, colmap # pylint: disable=redefined-outer-name diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index a883ae33..69d89bf1 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -67,6 +67,49 @@ def test_from_parquet(data_fixture, request): # Check to make sure the critical quantity labels are bound to real columns assert parquet_ensemble._source[col] is not None + +@pytest.mark.parametrize( + "data_fixture", + [ + "parquet_files_and_ensemble_without_client", + ], +) +def test_objsor_from_parquet(data_fixture, request): + """ + Test that the ensemble successfully loads a SourceFrame and ObjectFrame form parquet files. + """ + _, source_file, object_file, colmap = request.getfixturevalue(data_fixture) + + ens = Ensemble(client=False) + ens = ens.objsor_from_parquet(source_file, object_file, column_mapper=colmap) + + assert ens is not None + + # Check to make sure the source and object tables were created + assert ens.source is not None + assert ens.object is not None + assert isinstance(ens.source, SourceFrame) + assert isinstance(ens.object, ObjectFrame) + + # Check that the data is not empty. + obj, source = ens.compute() + assert len(source) == 2000 + assert len(obj) == 15 + + # Check that source and object both have the same ids present + assert sorted(np.unique(list(source.index))) == sorted(np.array(obj.index)) + + # Check the we loaded the correct columns. + for col in [ + ens._time_col, + ens._flux_col, + ens._err_col, + ens._band_col, + ens._provenance_col, + ]: + # Check to make sure the critical quantity labels are bound to real columns + assert ens.source[col] is not None + def test_available_datasets(dask_client): """ From 34e9bbd54bc639b5d2326b0f0835523bda3ee493 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Wed, 6 Sep 2023 02:17:17 -0700 Subject: [PATCH 09/35] Addressed comments --- src/tape/ensemble.py | 9 ++- src/tape/ensemble_frame.py | 77 ++++++++++++++++++++++--- tests/tape_tests/test_ensemble_frame.py | 8 +-- 3 files changed, 79 insertions(+), 15 deletions(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index f9f0e609..712e8fae 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -379,7 +379,7 @@ def compute(self, table=None, **kwargs): A single pandas data frame for the specified table or a tuple of (object, source) data frames. """ - # TODO(wbeebe@uw.edu): Merge this duplicate logic as part of milestone 4 + # TODO(wbeebe@uw.edu): Remove this logic as part of milestone 4's removal of the _source and _object fields if self.object is not None and self.source is not None: return (self.object.compute(**kwargs), self.source.compute(**kwargs)) if table: @@ -1238,14 +1238,15 @@ def objsor_from_parquet( may be out of date until a sync is performed internally. additional_cols: 'bool', optional Boolean to indicate whether to carry in columns beyond the - critical columns, true will, while false will only load the columns + critical columns, True will, while Talse will only load the columns containing the critical quantities (id,time,flux,err,band) npartitions: `int`, optional If specified, attempts to repartition the ensemble to the specified number of partitions partition_size: `int`, optional If specified, attempts to repartition the ensemble to partitions - of size `partition_size`. + of size `partition_size`, the maximum number of bytes for partition + as computed by `pandas.Dataframe.memory_usage`. Returns ---------- @@ -1295,6 +1296,7 @@ def objsor_from_parquet( self.source["provenance"] = self.source.apply( lambda x: provenance_label, axis=1, meta=pd.Series(name="provenance", dtype=str) ) + self.source["provenance"] = provenance_label self._provenance_col = "provenance" if npartitions and npartitions > 1: @@ -1302,6 +1304,7 @@ def objsor_from_parquet( elif partition_size: self.source = self.source.repartition(partition_size=partition_size) + # Add the source and object tables to the frames tracked by the Ensemble self.frames[self.source.label] = self.source self.frames[self.object.label] = self.object return self diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index 55348348..1aa3866c 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -26,9 +26,9 @@ class TapeArrowEngine(DaskArrowDatasetEngine): """ @classmethod - def _update_meta(cls, meta, schema): + def _creates_meta(cls, meta, schema): """ - Convert meta to a TapeFrame + Converts the meta to a TapeFrame. """ return TapeFrame(meta) @@ -44,7 +44,7 @@ def _create_dd_meta(cls, dataset_info, use_nullable_dtypes=False): "No dataset parts discovered. Use dask.dataframe.read_parquet " "to read it as an empty DataFrame" ) - meta = cls._update_meta(meta, schema) + meta = cls._creates_meta(meta, schema) return meta class TapeSourceArrowEngine(TapeArrowEngine): @@ -54,7 +54,7 @@ class TapeSourceArrowEngine(TapeArrowEngine): """ @classmethod - def _update_meta(cls, meta, schema): + def _creates_meta(cls, meta, schema): """ Convert meta to a TapeSourceFrame """ @@ -67,7 +67,7 @@ class TapeObjectArrowEngine(TapeArrowEngine): """ @classmethod - def _update_meta(cls, meta, schema): + def _creates_meta(cls, meta, schema): """ Convert meta to a TapeObjectFrame """ @@ -133,6 +133,45 @@ def assign(self, **kwargs): """ result = super().assign(**kwargs) return self._propagate_metadata(result) + + def query(self, expr, **kwargs): + """Filter dataframe with complex expression + + Doc string below derived from dask.dataframe.core + + Blocked version of pd.DataFrame.query + + Parameters + ---------- + expr: str + The query string to evaluate. + You can refer to column names that are not valid Python variable names + by surrounding them in backticks. + Dask does not fully support referring to variables using the '@' character, + use f-strings or the ``local_dict`` keyword argument instead. + **kwargs: `dict` + See the documentation for eval() for complete details on the keyword arguments accepted + by pandas.DataFrame.query(). + + Returns + ---------- + result: `tape._Frame` + The modifed frame + + Notes + ----- + This is like the sequential version except that this will also happen + in many threads. This may conflict with ``numexpr`` which will use + multiple threads itself. We recommend that you set ``numexpr`` to use a + single thread: + + .. code-block:: python + + import numexpr + numexpr.set_num_threads(1) + """ + result = super().query(expr, **kwargs) + return self._propagate_metadata(result) class TapeSeries(pd.Series): """A barebones extension of a Pandas series to be used for underlying Ensemble data. @@ -207,7 +246,7 @@ def from_tapeframe( label: `str`, optional | The label used to by the Ensemble to identify the frame. ensemble: `tape.Ensemble`, optional - | A linnk to the Ensmeble object that owns this frame. + | A link to the Ensemble object that owns this frame. Returns result: `tape.EnsembleFrame` The constructed EnsembleFrame object. @@ -303,7 +342,7 @@ def from_parquet( inferred from the pandas parquet file metadata, if present. Use False to read all fields as columns. ensemble: `tape.ensemble.Ensemble`, optional - | A link to the Ensmeble object that owns this frame. + | A link to the Ensemble object that owns this frame. Returns result: `tape.EnsembleFrame` The constructed EnsembleFrame object. @@ -385,7 +424,7 @@ def from_parquet( inferred from the pandas parquet file metadata, if present. Use False to read all fields as columns. ensemble: `tape.ensemble.Ensemble`, optional - | A link to the Ensmeble object that owns this frame. + | A link to the Ensemble object that owns this frame. Returns result: `tape.EnsembleFrame` The constructed EnsembleFrame object. @@ -419,6 +458,28 @@ def from_parquet( columns=None, ensemble=None, ): + """ Returns an ObjectFrame constructed from loading a parquet file. + Parameters + ---------- + path: `str` or `list` + Source directory for data, or path(s) to individual parquet files. Prefix with a + protocol like s3:// to read from alternative filesystems. To read from multiple + files you can pass a globstring or a list of paths, with the caveat that they must all + have the same protocol. + columns: `str` or `list`, optional + Field name(s) to read in as columns in the output. By default all non-index fields will + be read (as determined by the pandas parquet metadata, if present). Provide a single + field name instead of a list to read in the data as a Series. + index: `str`, `list`, `False`, optional + Field name(s) to use as the output frame index. Default is None and index will be + inferred from the pandas parquet file metadata, if present. Use False to read all + fields as columns. + ensemble: `tape.ensemble.Ensemble`, optional + | A link to the Ensemble object that owns this frame. + Returns + result: `tape.ObjectFrame` + The constructed ObjectFrame object. + """ # Read in the object Parquet file result = dd.read_parquet( path, index=index, columns=columns, split_row_groups=True, engine=TapeObjectArrowEngine, diff --git a/tests/tape_tests/test_ensemble_frame.py b/tests/tape_tests/test_ensemble_frame.py index 0cbd6d15..d37b2ca9 100644 --- a/tests/tape_tests/test_ensemble_frame.py +++ b/tests/tape_tests/test_ensemble_frame.py @@ -93,10 +93,10 @@ def test_ensemble_frame_propagation(data_fixture, request): # Test that the output of an EnsembleFrame query is still an EnsembleFrame queried_rows = ens_frame.query("flux > 3.0") - assert isinstance(filtered_frame, EnsembleFrame) - assert isinstance(filtered_frame._meta, TapeFrame) - assert filtered_frame.label == TEST_LABEL - assert filtered_frame.ensemble == ens + assert isinstance(queried_rows, EnsembleFrame) + assert isinstance(queried_rows._meta, TapeFrame) + assert queried_rows.label == TEST_LABEL + assert queried_rows.ensemble == ens # Test that head returns a subset of the underlying TapeFrame. h = ens_frame.head(5) From 93abf4d362bb3d299833ca3dbb218b426b58c0a1 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Wed, 6 Sep 2023 09:51:49 -0700 Subject: [PATCH 10/35] Removed adding column via apply --- src/tape/ensemble.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index 712e8fae..d586a03a 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -1293,9 +1293,6 @@ def objsor_from_parquet( # Generate a provenance column if not provided if self._provenance_col is None: - self.source["provenance"] = self.source.apply( - lambda x: provenance_label, axis=1, meta=pd.Series(name="provenance", dtype=str) - ) self.source["provenance"] = provenance_label self._provenance_col = "provenance" From 8c8e7938dd00c9c22522047ec7e1cdecb44823e3 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Thu, 7 Sep 2023 11:30:45 -0700 Subject: [PATCH 11/35] Fix comment typo --- src/tape/ensemble.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index d586a03a..c9ae55ff 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -1238,7 +1238,7 @@ def objsor_from_parquet( may be out of date until a sync is performed internally. additional_cols: 'bool', optional Boolean to indicate whether to carry in columns beyond the - critical columns, True will, while Talse will only load the columns + critical columns, True will, while False will only load the columns containing the critical quantities (id,time,flux,err,band) npartitions: `int`, optional If specified, attempts to repartition the ensemble to the specified From 068870a8bc46234baa81aa8c5c8fa148c3e19cd8 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Tue, 19 Sep 2023 13:57:30 -0700 Subject: [PATCH 12/35] Fix EnsembleFrame.set_index --- src/tape/ensemble_frame.py | 111 ++++++++++++++++++++---- tests/tape_tests/test_ensemble_frame.py | 31 ++++++- 2 files changed, 125 insertions(+), 17 deletions(-) diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index 1aa3866c..f14598bb 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -1,3 +1,5 @@ +from collections.abc import Sequence + import dask.dataframe as dd import dask @@ -10,6 +12,9 @@ import numpy as np import pandas as pd +from typing import Literal + + from functools import partial from dask.dataframe.io.parquet.arrow import ( ArrowDatasetEngine as DaskArrowDatasetEngine, @@ -172,6 +177,97 @@ def query(self, expr, **kwargs): """ result = super().query(expr, **kwargs) return self._propagate_metadata(result) + + def set_index( + self, + other: str | pd.Series, + drop: bool = True, + sorted: bool = False, + npartitions: int | Literal["auto"] | None = None, + divisions: Sequence | None = None, + inplace: bool = False, + sort: bool = True, + **kwargs, + ): + + """Set the DataFrame index (row labels) using an existing column. + + Doc string below derived from dask.dataframe.core + + If ``sort=False``, this function operates exactly like ``pandas.set_index`` + and sets the index on the DataFrame. If ``sort=True`` (default), + this function also sorts the DataFrame by the new index. This can have a + significant impact on performance, because joins, groupbys, lookups, etc. + are all much faster on that column. However, this performance increase + comes with a cost, sorting a parallel dataset requires expensive shuffles. + Often we ``set_index`` once directly after data ingest and filtering and + then perform many cheap computations off of the sorted dataset. + + With ``sort=True``, this function is much more expensive. Under normal + operation this function does an initial pass over the index column to + compute approximate quantiles to serve as future divisions. It then passes + over the data a second time, splitting up each input partition into several + pieces and sharing those pieces to all of the output partitions now in + sorted order. + + In some cases we can alleviate those costs, for example if your dataset is + sorted already then we can avoid making many small pieces or if you know + good values to split the new index column then we can avoid the initial + pass over the data. For example if your new index is a datetime index and + your data is already sorted by day then this entire operation can be done + for free. You can control these options with the following parameters. + + Parameters + ---------- + other: string or Dask Series + Column to use as index. + drop: boolean, default True + Delete column to be used as the new index. + sorted: bool, optional + If the index column is already sorted in increasing order. + Defaults to False + npartitions: int, None, or 'auto' + The ideal number of output partitions. If None, use the same as + the input. If 'auto' then decide by memory use. + Only used when ``divisions`` is not given. If ``divisions`` is given, + the number of output partitions will be ``len(divisions) - 1``. + divisions: list, optional + The "dividing lines" used to split the new index into partitions. + For ``divisions=[0, 10, 50, 100]``, there would be three output partitions, + where the new index contained [0, 10), [10, 50), and [50, 100), respectively. + See https://docs.dask.org/en/latest/dataframe-design.html#partitions. + If not given (default), good divisions are calculated by immediately computing + the data and looking at the distribution of its values. For large datasets, + this can be expensive. + Note that if ``sorted=True``, specified divisions are assumed to match + the existing partitions in the data; if this is untrue you should + leave divisions empty and call ``repartition`` after ``set_index``. + inplace: bool, optional + Modifying the DataFrame in place is not supported by Dask. + Defaults to False. + sort: bool, optional + If ``True``, sort the DataFrame by the new index. Otherwise + set the index on the individual existing partitions. + Defaults to ``True``. + shuffle: {'disk', 'tasks', 'p2p'}, optional + Either ``'disk'`` for single-node operation or ``'tasks'`` and + ``'p2p'`` for distributed operation. Will be inferred by your + current scheduler. + compute: bool, default False + Whether or not to trigger an immediate computation. Defaults to False. + Note, that even if you set ``compute=False``, an immediate computation + will still be triggered if ``divisions`` is ``None``. + partition_size: int, optional + Desired size of each partitions in bytes. + Only used when ``npartitions='auto'`` + + Returns + ---------- + result: `tape._Frame` + The indexed frame + """ + result = super().set_index(other, drop, sorted, npartitions, divisions, inplace, sort, **kwargs) + return self._propagate_metadata(result) class TapeSeries(pd.Series): """A barebones extension of a Pandas series to be used for underlying Ensemble data. @@ -509,26 +605,17 @@ def from_parquet( def make_meta_series(x, index=None): # Create an empty TapeSeries to use as Dask's underlying object meta. result = x.head(0) - # Re-index if requested - if index is not None: - result = result.reindex(index[:0]) return result @make_meta_dispatch.register(TapeFrame) def make_meta_frame(x, index=None): # Create an empty TapeFrame to use as Dask's underlying object meta. result = x.head(0) - # Re-index if requested - if index is not None: - result = result.reindex(index[:0]) return result @meta_nonempty.register(TapeSeries) def _nonempty_tapeseries(x, index=None): # Construct a new TapeSeries with the same underlying data. - if index is None: - index = _nonempty_index(x.index) - data = make_array_nonempty(x.dtype) return TapeSeries(data, name=x.name, crs=x.crs) @meta_nonempty.register(TapeFrame) @@ -541,9 +628,6 @@ def _nonempty_tapeseries(x, index=None): def make_meta_frame(x, index=None): # Create an empty TapeObjectFrame to use as Dask's underlying object meta. result = x.head(0) - # Re-index if requested - if index is not None: - result = result.reindex(index[:0]) return result @meta_nonempty.register(TapeObjectFrame) @@ -556,9 +640,6 @@ def _nonempty_tapesourceframe(x, index=None): def make_meta_frame(x, index=None): # Create an empty TapeSourceFrame to use as Dask's underlying object meta. result = x.head(0) - # Re-index if requested - if index is not None: - result = result.reindex(index[:0]) return result @meta_nonempty.register(TapeSourceFrame) diff --git a/tests/tape_tests/test_ensemble_frame.py b/tests/tape_tests/test_ensemble_frame.py index d37b2ca9..678e7534 100644 --- a/tests/tape_tests/test_ensemble_frame.py +++ b/tests/tape_tests/test_ensemble_frame.py @@ -108,6 +108,15 @@ def test_ensemble_frame_propagation(data_fixture, request): assert isinstance(ens_frame.compute(), TapeFrame) assert len(ens_frame) == len(ens_frame.compute()) + # Set an index and then group by that index. + ens_frame = ens_frame.set_index("id", drop=True) + assert ens_frame.label == TEST_LABEL + assert ens_frame.ensemble == ens + group_result = ens_frame.groupby(["id"]).count() + assert len(group_result) > 0 + assert isinstance(group_result, EnsembleFrame) + assert isinstance(group_result._meta, TapeFrame) + @pytest.mark.parametrize( "data_fixture", [ @@ -190,7 +199,7 @@ def test_object_and_source_frame_propagation(data_fixture, request): # Perform a series of operations on the SourceFrame and then verify the result is still a # proper SourceFrame with appropriate metadata propagated. - mean_ps_flux = source_frame["psFlux"].mean().compute() + source_frame["psFlux"].mean().compute() result_source_frame = source_frame.copy()[["psFlux", "psFluxErr"]] assert isinstance(result_source_frame, SourceFrame) assert isinstance(result_source_frame._meta, TapeSourceFrame) @@ -199,6 +208,15 @@ def test_object_and_source_frame_propagation(data_fixture, request): assert result_source_frame.ensemble is not None assert result_source_frame.ensemble is ens + # Set an index and then group by that index. + result_source_frame = result_source_frame.set_index("psFlux", drop=True) + assert result_source_frame.label == SOURCE_LABEL + assert result_source_frame.ensemble == ens + group_result = result_source_frame.groupby(["psFlux"]).count() + assert len(group_result) > 0 + assert isinstance(group_result, SourceFrame) + assert isinstance(group_result._meta, TapeSourceFrame) + # Create an ObjectFrame from a parquet file object_frame = ObjectFrame.from_parquet( object_file, @@ -216,4 +234,13 @@ def test_object_and_source_frame_propagation(data_fixture, request): assert isinstance(result_object_frame, ObjectFrame) assert isinstance(result_object_frame._meta, TapeObjectFrame) assert result_object_frame.label == OBJECT_LABEL - assert result_object_frame.ensemble is ens \ No newline at end of file + assert result_object_frame.ensemble is ens + + # Set an index and then group by that index. + result_object_frame = result_object_frame.set_index("nobs_g", drop=True) + assert result_object_frame.label == OBJECT_LABEL + assert result_object_frame.ensemble == ens + group_result = result_object_frame.groupby(["nobs_g"]).count() + assert len(group_result) > 0 + assert isinstance(group_result, ObjectFrame) + assert isinstance(group_result._meta, TapeObjectFrame) \ No newline at end of file From db8a1ab9f2d4567c94b9c1ce41cb7b8376c392ef Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Thu, 5 Oct 2023 14:22:03 -0700 Subject: [PATCH 13/35] Add update_ensemble() and Use EnsembleFrames (#252) * Adds EnsembleFrame.update_ensemble() * Use EnsembleFrames throughout the Ensemble * Udpdate ensemble test * Extends update_ensemble test cases * Unpin sphinx to address docs build fail * Fix minor test error * Remove debug line --- docs/requirements.txt | 6 +- pyproject.toml | 6 +- src/tape/ensemble.py | 166 +++++++++++++++--------------- src/tape/ensemble_frame.py | 88 +++++++++++++++- tests/tape_tests/test_ensemble.py | 110 ++++++++++++++------ 5 files changed, 254 insertions(+), 122 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index c5a1c741..1511e27b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ -sphinx==6.1.3 -sphinx_rtd_theme==1.2.0 -sphinx-autoapi==2.0.1 +sphinx +sphinx_rtd_theme +sphinx-autoapi nbsphinx ipython jupytext diff --git a/pyproject.toml b/pyproject.toml index 6baf5850..fc1287e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,9 +36,9 @@ dev = [ "pytest", "pytest-cov", # Used to report total code coverage "pre-commit", # Used to run checks before finalizing a git commit - "sphinx==6.1.3", # Used to automatically generate documentation - "sphinx_rtd_theme==1.2.0", # Used to render documentation - "sphinx-autoapi==2.0.1", # Used to automatically generate api documentation + "sphinx", # Used to automatically generate documentation + "sphinx_rtd_theme", # Used to render documentation + "sphinx-autoapi", # Used to automatically generate api documentation "black", # Used for static linting of files # if you add dependencies here while experimenting in a notebook and you # want that notebook to render in your documentation, please add the diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index c9ae55ff..b84520a0 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -12,7 +12,7 @@ from .analysis.feature_extractor import BaseLightCurveFeature, FeatureExtractor from .analysis.structure_function import SF_METHODS from .analysis.structurefunction2 import calc_sf2 -from .ensemble_frame import ObjectFrame, SourceFrame +from .ensemble_frame import ObjectFrame, SourceFrame, TapeObjectFrame from .timeseries import TimeSeries from .utils import ColumnMapper @@ -46,9 +46,6 @@ def __init__(self, client=True, **kwargs): self.source = None # Source Table EnsembleFrame self.object = None # Object Table EnsembleFrame - self._source_dirty = False # Source Dirty Flag - self._object_dirty = False # Object Dirty Flag - # Default to removing empty objects. self.keep_empty_objects = kwargs.get("keep_empty_objects", False) @@ -136,16 +133,25 @@ def update_frame(self, frame): Raises ------ - ValueError if the `frame.label` is unpopulated, "source", or "object". + ValueError if the `frame.label` is unpopulated, or if the frame is not a SourceFrame or ObjectFrame + but uses the reserved labels. """ if frame.label is None: raise ValueError( f"Unable to update frame with no populated `EnsembleFrame.label`." ) - if frame.label == SOURCE_FRAME_LABEL or frame.label == OBJECT_FRAME_LABEL: - raise ValueError( - f"Unable to update frame with reserved label " f"'{frame.label}'" + if isinstance(frame, SourceFrame) or isinstance(frame, ObjectFrame): + expected_label = SOURCE_FRAME_LABEL if isinstance(frame, SourceFrame) else OBJECT_FRAME_LABEL + if frame.label != expected_label: + raise ValueError(f"Unable to update frame with reserved label " f"'{frame.label}'" ) + if isinstance(frame, SourceFrame): + self._source = frame + self.source = frame + elif isinstance(frame, ObjectFrame): + self._object = frame + self.object = frame + # Ensure this frame is assigned to this Ensemble. frame.ensemble = self self.frames[frame.label] = frame @@ -316,16 +322,16 @@ def insert_sources( prev_num = self._source.npartitions # Append the new rows to the correct divisions. - self._source = dd.concat([self._source, df2], axis=0, interleave_partitions=True) - self._source_dirty = True + self.update_frame(dd.concat([self._source, df2], axis=0, interleave_partitions=True)) + self._source.set_dirty(True) # Do the repartitioning if requested. If the divisions were set, reuse them. # Otherwise, use the same number of partitions. if force_repartition: if all(prev_div): - self._source = self._source.repartition(divisions=prev_div) + self.update_frame(self._source.repartition(divisions=prev_div)) elif self._source.npartitions != prev_num: - self._source = self._source.repartition(npartitions=prev_num) + self.update_frame(self._source.repartition(npartitions=prev_num)) def client_info(self): """Calls the Dask Client, which returns cluster information @@ -379,9 +385,6 @@ def compute(self, table=None, **kwargs): A single pandas data frame for the specified table or a tuple of (object, source) data frames. """ - # TODO(wbeebe@uw.edu): Remove this logic as part of milestone 4's removal of the _source and _object fields - if self.object is not None and self.source is not None: - return (self.object.compute(**kwargs), self.source.compute(**kwargs)) if table: self._lazy_sync_tables(table) if table == "object": @@ -401,8 +404,8 @@ def persist(self, **kwargs): of the computation. """ self._lazy_sync_tables("all") - self._object = self._object.persist(**kwargs) - self._source = self._source.persist(**kwargs) + self.update_frame(self._object.persist(**kwargs)) + self.update_frame(self._source.persist(**kwargs)) def columns(self, table="object"): """Retrieve columns from dask dataframe""" @@ -454,11 +457,11 @@ def dropna(self, table="source", **kwargs): scheme """ if table == "object": - self._object = self._object.dropna(**kwargs) - self._object_dirty = True # This operation modifies the object table + self.update_frame(self._object.dropna(**kwargs)) + self._object.set_dirty(True) # This operation modifies the object table elif table == "source": - self._source = self._source.dropna(**kwargs) - self._source_dirty = True # This operation modifies the source table + self.update_frame(self._source.dropna(**kwargs)) + self._source.set_dirty(True) # This operation modifies the source table else: raise ValueError(f"{table} is not one of 'object' or 'source'") @@ -479,12 +482,12 @@ def select(self, columns, table="object"): self._lazy_sync_tables(table) if table == "object": cols_to_drop = [col for col in self._object.columns if col not in columns] - self._object = self._object.drop(cols_to_drop, axis=1) - self._object_dirty = True + self.update_frame(self._object.drop(cols_to_drop, axis=1)) + self._object.set_dirty(True) elif table == "source": cols_to_drop = [col for col in self._source.columns if col not in columns] - self._source = self._source.drop(cols_to_drop, axis=1) - self._source_dirty = True + self.update_frame(self._source.drop(cols_to_drop, axis=1)) + self._source.set_dirty(True) else: raise ValueError(f"{table} is not one of 'object' or 'source'") @@ -513,11 +516,11 @@ def query(self, expr, table="object"): """ self._lazy_sync_tables(table) if table == "object": - self._object = self._object.query(expr) - self._object_dirty = True + self.update_frame(self._object.query(expr)) + self._object.set_dirty(True) elif table == "source": - self._source = self._source.query(expr) - self._source_dirty = True + self.update_frame(self._source.query(expr)) + self._source.set_dirty(True) return self def filter_from_series(self, keep_series, table="object"): @@ -535,11 +538,11 @@ def filter_from_series(self, keep_series, table="object"): """ self._lazy_sync_tables(table) if table == "object": - self._object = self._object[keep_series] - self._object_dirty = True + self.update_frame(self._object[keep_series]) + self._object.set_dirty(True) elif table == "source": - self._source = self._source[keep_series] - self._source_dirty = True + self.update_frame(self._source[keep_series]) + self._source.set_dirty(True) return self def assign(self, table="object", **kwargs): @@ -570,11 +573,11 @@ def assign(self, table="object", **kwargs): self._lazy_sync_tables(table) if table == "object": - self._object = self._object.assign(**kwargs) - self._object_dirty = True + self.update_frame(self._object.assign(**kwargs)) + self._object.set_dirty(True) elif table == "source": - self._source = self._source.assign(**kwargs) - self._source_dirty = True + self.update_frame(self._source.assign(**kwargs)) + self._source.set_dirty(True) else: raise ValueError(f"{table} is not one of 'object' or 'source'") return self @@ -657,9 +660,9 @@ def coalesce(self, input_cols, output_col, table="object", drop_inputs=False): table_ddf = table_ddf.drop(columns=input_cols) if table == "object": - self._object = table_ddf + self.update_frame(table_ddf) elif table == "source": - self._source = table_ddf + self.update_frame(table_ddf) return self @@ -687,9 +690,9 @@ def prune(self, threshold=50, col_name=None): # Mask on object table mask = self._object[col_name] >= threshold - self._object = self._object[mask] + self.update_frame(self._object[mask]) - self._object_dirty = True # Object Table is now dirty + self._object.set_dirty(True) # Object Table is now dirty return self @@ -828,13 +831,13 @@ def bin_sources( aggr_funs[key] = custom_aggr[key] # Group the columns by id, band, and time bucket and aggregate. - self._source = self._source.groupby([self._id_col, self._band_col, tmp_time_col]).aggregate(aggr_funs) + self.update_frame(self._source.groupby([self._id_col, self._band_col, tmp_time_col]).aggregate(aggr_funs)) # Fix the indices and remove the temporary column. - self._source = self._source.reset_index().set_index(self._id_col).drop(tmp_time_col, axis=1) + self.update_frame(self._source.reset_index().set_index(self._id_col).drop(tmp_time_col, axis=1)) # Mark the source table as dirty. - self._source_dirty = True + self._source.set_dirty(True) return self def batch(self, func, *args, meta=None, use_map=True, compute=True, on=None, **kwargs): @@ -1160,14 +1163,13 @@ def from_parquet( columns.append(col) # Read in the source parquet file(s) - self._source = dd.read_parquet( - source_file, index=self._id_col, columns=columns, split_row_groups=True - ) + self.update_frame(SourceFrame.from_parquet( + source_file, index=self._id_col, columns=columns, ensemble=self, + )) if object_file: # read from parquet files # Read in the object file(s) - self._object = dd.read_parquet(object_file, index=self._id_col, split_row_groups=True) - + self.update_frame(ObjectFrame.from_parquet(object_file, index=self._id_col, ensemble=self)) if self._nobs_band_cols is None: # sets empty nobs cols in object unq_filters = np.unique(self._source[self._band_col]) @@ -1182,12 +1184,12 @@ def from_parquet( # Optionally sync the tables, recalculates nobs columns if sync_tables: - self._source_dirty = True - self._object_dirty = True + self._source.set_dirty(True) + self._object.set_dirty(True) self._sync_tables() else: # generate object table from source - self._object = self._generate_object_table() + self.update_frame(self._generate_object_table()) self._nobs_bands = [col for col in list(self._object.columns) if col != self._nobs_tot_col] # Generate a provenance column if not provided @@ -1198,9 +1200,9 @@ def from_parquet( self._provenance_col = "provenance" if npartitions and npartitions > 1: - self._source = self._source.repartition(npartitions=npartitions) + self.update_frame(self._source.repartition(npartitions=npartitions)) elif partition_size: - self._source = self._source.repartition(partition_size=partition_size) + self.update_frame(self._source.repartition(partition_size=partition_size)) return self @@ -1271,11 +1273,11 @@ def objsor_from_parquet( columns.append(col) # Read in the source parquet file(s) - self.source = SourceFrame.from_parquet(source_file, index=self._id_col, columns=columns, - ensemble=self) + self.update_frame(SourceFrame.from_parquet(source_file, index=self._id_col, columns=columns, + ensemble=self)) # Read in the object file(s) - self.object = ObjectFrame.from_parquet(object_file, index=self._id_col, ensemble=self) + self.update_frame(ObjectFrame.from_parquet(object_file, index=self._id_col, ensemble=self)) if self._nobs_band_cols is None: # sets empty nobs cols in object @@ -1297,13 +1299,10 @@ def objsor_from_parquet( self._provenance_col = "provenance" if npartitions and npartitions > 1: - self.source = self.source.repartition(npartitions=npartitions) + self.update_frame(self.source.repartition(npartitions=npartitions)) elif partition_size: - self.source = self.source.repartition(partition_size=partition_size) + self.update_frame(self.source.repartition(partition_size=partition_size)) - # Add the source and object tables to the frames tracked by the Ensemble - self.frames[self.source.label] = self.source - self.frames[self.object.label] = self.object return self def from_dataset(self, dataset, **kwargs): @@ -1383,15 +1382,16 @@ def from_source_dict(self, source_dict, column_mapper=None, npartitions=1, **kwa self._load_column_mapper(column_mapper, **kwargs) # Load in the source data. - self._source = dd.DataFrame.from_dict(source_dict, npartitions=npartitions) - self._source = self._source.set_index(self._id_col, drop=True) + self.update_frame(SourceFrame.from_dict(source_dict, npartitions=npartitions)) + self.update_frame(self._source.set_index(self._id_col, drop=True)) # Generate the object table from the source. - self._object = self._generate_object_table() + # TODO this is not the object Table oh no.... + self.update_frame(self._generate_object_table()) # Now synced and clean - self._source_dirty = False - self._object_dirty = False + self._source.set_dirty(False) + self._object.set_dirty(False) return self def _generate_object_table(self): @@ -1404,6 +1404,10 @@ def _generate_object_table(self): .pivot_table(values=self._time_col, index=self._id_col, columns=self._band_col, aggfunc="sum") ) + # Convert the resulting dataframe into an ObjectFrame + # TODO(wbeebe@uw.edu): Inveestigate if we can correctly infer that `res` is an ObjectFrame instead + res = ObjectFrame.from_dask_dataframe(res, ensemble=self) + # If the ensemble's keep_empty_objects attribute is True and there are previous # objects, then copy them into the res table with counts of zero. if self.keep_empty_objects and self._object is not None: @@ -1451,11 +1455,11 @@ def _lazy_sync_tables(self, table="object"): The table being modified. Should be one of "object", "source", or "all" """ - if table == "object" and self._source_dirty: # object table should be updated + if table == "object" and self._source.is_dirty(): # object table should be updated self._sync_tables() - elif table == "source" and self._object_dirty: # source table should be updated + elif table == "source" and self._object.is_dirty(): # source table should be updated self._sync_tables() - elif table == "all" and (self._source_dirty or self._object_dirty): + elif table == "all" and (self._source.is_dirty() or self._object.is_dirty()): self._sync_tables() return self @@ -1467,29 +1471,29 @@ def _sync_tables(self): keep_empty_objects attribute is set to True. """ - if self._object_dirty: + if self._object.is_dirty(): # Sync Object to Source; remove any missing objects from source s_cols = self._source.columns - self._source = self._source.merge( + self.update_frame(self._source.merge( self._object, how="right", on=[self._id_col], suffixes=(None, "_obj") - ) + )) cols_to_drop = [col for col in self._source.columns if col not in s_cols] - self._source = self._source.drop(cols_to_drop, axis=1) - self._source = self._source.persist() # persist source + self.update_frame(self._source.drop(cols_to_drop, axis=1)) + self.update_frame(self._source.persist()) # persist source - if self._source_dirty: # not elif + if self._source._is_dirty: # not elif # Generate a new object table; updates n_obs, removes missing ids new_obj = self._generate_object_table() # Join old obj to new obj; pulls in other existing obj columns - self._object = new_obj.join(self._object, on=self._id_col, how="left", lsuffix="", rsuffix="_old") + self.update_frame(new_obj.join(self._object, on=self._id_col, how="left", lsuffix="", rsuffix="_old")) old_cols = [col for col in list(self._object.columns) if "_old" in col] - self._object = self._object.drop(old_cols, axis=1) - self._object = self._object.persist() # persist object + self.update_frame(self._object.drop(old_cols, axis=1)) + self.update_frame(self._object.persist()) # persist object # Now synced and clean - self._source_dirty = False - self._object_dirty = False + self._source.set_dirty(False) + self._object.set_dirty(False) return self def to_timeseries( diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index f14598bb..db0e27fc 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -315,6 +315,8 @@ class EnsembleFrame(_Frame, dd.core.DataFrame): """ _partition_type = TapeFrame # Tracks the underlying data type + _is_dirty = False # True if the underlying data is out of sync with the Ensemble + def __getitem__(self, key): result = super().__getitem__(key) if isinstance(result, _Frame): @@ -352,12 +354,46 @@ def from_tapeframe( result.ensemble = ensemble return result + @classmethod + def from_dask_dataframe(cl, df, ensemble=None, label=None): + """ Returns an EnsembleFrame constructed from a Dask dataframe. + Parameters + ---------- + df: `dask.dataframe.DataFrame` or `list` + a Dask dataframe to convert to an EnsembleFrame + ensemble: `tape.ensemble.Ensemble`, optional + | A link to the Ensemble object that owns this frame. + label: `str`, optional + | The label used to by the Ensemble to identify the frame. + Returns + result: `tape.EnsembleFrame` + The constructed EnsembleFrame object. + """ + # Create a EnsembleFrame by mapping the partitions to the appropriate meta, TapeFrame + # TODO(wbeebe@uw.edu): Determine if there is a better method + result = df.map_partitions(TapeFrame) + result.ensemble = ensemble + result.label = label + return result + + def update_ensemble(self): + """ Updates the Ensemble linked by the `EnsembelFrame.ensemble` property to track this frame. + + Returns + result: `tape.Ensemble` + The Ensemble object which tracks this frame, `None` if no such Ensemble. + """ + if self.ensemble is None: + return None + # Update the Ensemble to track this frame and return the ensemble. + return self.ensemble.update_frame(self) + def convert_flux_to_mag(self, flux_col, zero_point, err_col=None, zp_form="mag", - out_col_name=None, + out_col_name=None, ): """Converts this EnsembleFrame's flux column into a magnitude column, returning a new EnsembleFrame. @@ -451,6 +487,12 @@ def from_parquet( result.ensemble=ensemble return result + + def is_dirty(self): + return self._is_dirty + + def set_dirty(self, is_dirty): + self._is_dirty = is_dirty class TapeSourceFrame(TapeFrame): """A barebones extension of a Pandas frame to be used for underlying Ensemble source data @@ -535,6 +577,26 @@ def from_parquet( result.label = SOURCE_FRAME_LABEL return result + + @classmethod + def from_dask_dataframe(cl, df, ensemble=None): + """ Returns a SourceFrame constructed from a Dask dataframe.. + Parameters + ---------- + df: `dask.dataframe.DataFrame` or `list` + a Dask dataframe to convert to a SourceFrame + ensemble: `tape.ensemble.Ensemble`, optional + | A link to the Ensemble object that owns this frame. + Returns + result: `tape.SourceFrame` + The constructed SourceFrame object. + """ + # Create a SourceFrame by mapping the partitions to the appropriate meta, TapeSourceFrame + # TODO(wbeebe@uw.edu): Determine if there is a better method + result = df.map_partitions(TapeSourceFrame) + result.ensemble = ensemble + result.label = SOURCE_FRAME_LABEL + return result class ObjectFrame(EnsembleFrame): """ A subclass of EnsembleFrame for Object data. """ @@ -580,10 +642,30 @@ def from_parquet( result = dd.read_parquet( path, index=index, columns=columns, split_row_groups=True, engine=TapeObjectArrowEngine, ) - result.ensemble=ensemble + result.ensemble = ensemble result.label= OBJECT_FRAME_LABEL - return result + return result + + @classmethod + def from_dask_dataframe(cl, df, ensemble=None): + """ Returns an ObjectFrame constructed from a Dask dataframe.. + Parameters + ---------- + df: `dask.dataframe.DataFrame` or `list` + a Dask dataframe to convert to an ObjectFrame + ensemble: `tape.ensemble.Ensemble`, optional + | A link to the Ensemble object that owns this frame. + Returns + result: `tape.ObjectFrame` + The constructed ObjectFrame object. + """ + # Create an ObjectFrame by mapping the partitions to the appropriate meta, TapeObjectFrame + # TODO(wbeebe@uw.edu): Determine if there is a better method + result = df.map_partitions(TapeObjectFrame) + result.ensemble = ensemble + result.label = OBJECT_FRAME_LABEL + return result """ Dask Dataframes are constructed indirectly using method dispatching and inference on the diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index 69d89bf1..08f7a6b8 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -6,7 +6,7 @@ import pandas as pd import pytest -from tape import Ensemble, ObjectFrame, SourceFrame +from tape import Ensemble, EnsembleFrame, ObjectFrame, SourceFrame, TapeFrame, TapeObjectFrame, TapeSourceFrame from tape.analysis.stetsonj import calc_stetson_J from tape.analysis.structure_function.base_argument_container import StructureFunctionArgumentContainer from tape.analysis.structurefunction2 import calc_sf2 @@ -67,6 +67,49 @@ def test_from_parquet(data_fixture, request): # Check to make sure the critical quantity labels are bound to real columns assert parquet_ensemble._source[col] is not None +@pytest.mark.parametrize( + "data_fixture", + [ + "parquet_ensemble", + "parquet_ensemble_without_client", + ], +) +def test_update_ensemble(data_fixture, request): + """ + Tests that the ensemble can be updated with a result frame. + """ + ens = request.getfixturevalue(data_fixture) + + # Filter the object table and have the ensemble track the updated table. + updated_obj = ens._object.query("nobs_total > 50") + assert updated_obj is not ens._object + updated_obj.update_ensemble() + assert updated_obj is ens._object + + # Filter the source table and have the ensemble track the updated table. + updated_src = ens._source.query("psFluxErr > 0.1") + assert updated_src is not ens._source + updated_src.update_ensemble() + assert updated_src is ens._source + + # Create an additional result table for the ensemble to track. + cnts = ens._source.groupby([ens._id_col, ens._band_col])[ens._time_col].aggregate("count") + res = ( + cnts.to_frame() + .reset_index() + .categorize(columns=[ens._band_col]) + .pivot_table(values=ens._time_col, index=ens._id_col, columns=ens._band_col, aggfunc="sum") + ) + + # Convert the resulting dataframe into an EnsembleFrame and update the Ensemble + result_frame = EnsembleFrame.from_dask_dataframe(res, ensemble=ens, label="result") + result_frame.update_ensemble() + assert ens.select_frame("result") is result_frame + + # Test update_ensemble when a frame is unlinked to its parent ensemble. + result_frame.ensemble = None + assert result_frame.update_ensemble() is None + @pytest.mark.parametrize( "data_fixture", @@ -150,23 +193,23 @@ def test_frame_tracking(data_fixture, request): # Construct some result frames for the Ensemble to track. Underlying data is irrelevant for # this test. - ens_frame1 = ens.select_frame("source").copy() - ens_frame2 = ens.select_frame("source").copy() - ens_frame3 = ens.select_frame("source").copy() - ens_frame4 = ens.select_frame("source").copy() - + num_points = 100 + data = TapeFrame({ + "id": [8000 + 2 * i for i in range(num_points)], + "time": [float(i) for i in range(num_points)], + "flux": [0.5 * float(i % 4) for i in range(num_points)], + }) # Labels to give the EnsembleFrames - label1, label2, label3, label4 = "frame1", "frame2", "frame3", "frame4" + label1, label2, label3 = "frame1", "frame2", "frame3" + ens_frame1 = EnsembleFrame.from_tapeframe(data, npartitions=1, ensemble=ens, label=label1) + ens_frame2 = EnsembleFrame.from_tapeframe(data, npartitions=1, ensemble=ens, label=label2) + ens_frame3 = EnsembleFrame.from_tapeframe(data, npartitions=1, ensemble=ens, label=label3) # Validate that new source and object frames can't be added or updated. with pytest.raises(ValueError): ens.add_frame(ens_frame1, "source") with pytest.raises(ValueError): ens.add_frame(ens_frame1, "object") - with pytest.raises(ValueError): - ens.update_frame(ens.source) - with pytest.raises(ValueError): - ens.update_frame(ens.object) # Test that we can add and select a new ensemble frame assert ens.add_frame(ens_frame1, label1).select_frame(label1) is ens_frame1 @@ -202,7 +245,9 @@ def test_frame_tracking(data_fixture, request): assert ens.update_frame(ens_frame3).select_frame(label3) is ens_frame3 assert len(ens.frames) == 5 - # Update the ensemble with a new frame, verifying a missing label generates an error. + # Update the ensemble with an unlabeled frame, verifying a missing label generates an error. + ens_frame4 = EnsembleFrame.from_tapeframe(data, npartitions=1, ensemble=ens, label=None) + label4 = "frame4" with pytest.raises(ValueError): ens.update_frame(ens_frame4) ens_frame4.label = label4 @@ -513,10 +558,10 @@ def test_sync_tables(parquet_ensemble): assert len(parquet_ensemble.compute("source")) == 2000 parquet_ensemble.prune(50, col_name="nobs_r").prune(50, col_name="nobs_g") - assert parquet_ensemble._object_dirty # Prune should set the object dirty flag + assert parquet_ensemble._object.is_dirty() # Prune should set the object dirty flag parquet_ensemble.dropna(table="source") - assert parquet_ensemble._source_dirty # Dropna should set the source dirty flag + assert parquet_ensemble._source.is_dirty() # Dropna should set the source dirty flag parquet_ensemble._sync_tables() @@ -525,8 +570,8 @@ def test_sync_tables(parquet_ensemble): assert len(parquet_ensemble.compute("source")) == 1562 # dirty flags should be unset after sync - assert not parquet_ensemble._object_dirty - assert not parquet_ensemble._source_dirty + assert not parquet_ensemble._object.is_dirty() + assert not parquet_ensemble._source.is_dirty() def test_lazy_sync_tables(parquet_ensemble): @@ -538,35 +583,35 @@ def test_lazy_sync_tables(parquet_ensemble): # Modify only the object table. parquet_ensemble.prune(50, col_name="nobs_r").prune(50, col_name="nobs_g") - assert parquet_ensemble._object_dirty - assert not parquet_ensemble._source_dirty + assert parquet_ensemble._object.is_dirty() + assert not parquet_ensemble._source.is_dirty() # For a lazy sync on the object table, nothing should change, because # it is already dirty. parquet_ensemble._lazy_sync_tables(table="object") - assert parquet_ensemble._object_dirty - assert not parquet_ensemble._source_dirty + assert parquet_ensemble._object.is_dirty() + assert not parquet_ensemble._source.is_dirty() # For a lazy sync on the source table, the source table should be updated. parquet_ensemble._lazy_sync_tables(table="source") - assert not parquet_ensemble._object_dirty - assert not parquet_ensemble._source_dirty + assert not parquet_ensemble._object.is_dirty() + assert not parquet_ensemble._source.is_dirty() # Modify only the source table. parquet_ensemble.dropna(table="source") - assert not parquet_ensemble._object_dirty - assert parquet_ensemble._source_dirty + assert not parquet_ensemble._object.is_dirty() + assert parquet_ensemble._source.is_dirty() # For a lazy sync on the source table, nothing should change, because # it is already dirty. parquet_ensemble._lazy_sync_tables(table="source") - assert not parquet_ensemble._object_dirty - assert parquet_ensemble._source_dirty + assert not parquet_ensemble._object.is_dirty() + assert parquet_ensemble._source.is_dirty() # For a lazy sync on the source, the object table should be updated. parquet_ensemble._lazy_sync_tables(table="object") - assert not parquet_ensemble._object_dirty - assert not parquet_ensemble._source_dirty + assert not parquet_ensemble._object.is_dirty() + assert not parquet_ensemble._source.is_dirty() def test_dropna(parquet_ensemble): @@ -589,9 +634,9 @@ def test_dropna(parquet_ensemble): # Set the psFlux values for one source to NaN so we can drop it. # We do this on the instantiated source (pdf) and convert it back into a - # Dask DataFrame. + # SourceFrame. source_pdf.loc[valid_source_id, parquet_ensemble._flux_col] = pd.NA - parquet_ensemble._source = dd.from_pandas(source_pdf, npartitions=1) + parquet_ensemble.update_frame(SourceFrame.from_tapeframe(TapeSourceFrame(source_pdf), label="source", npartitions=1)) # Try dropping NaNs from source and confirm that we did. parquet_ensemble.dropna(table="source") @@ -616,9 +661,9 @@ def test_dropna(parquet_ensemble): # Set the nobs_g values for one object to NaN so we can drop it. # We do this on the instantiated object (pdf) and convert it back into a - # Dask DataFrame. + # ObjectFrame. object_pdf.loc[valid_object_id, parquet_ensemble._object.columns[0]] = pd.NA - parquet_ensemble._object = dd.from_pandas(object_pdf, npartitions=1) + parquet_ensemble.update_frame(ObjectFrame.from_tapeframe(TapeObjectFrame(object_pdf), label="object", npartitions=1)) # Try dropping NaNs from object and confirm that we did. parquet_ensemble.dropna(table="object") @@ -650,6 +695,7 @@ def test_keep_zeros(parquet_ensemble): valid_id = pdf.index.values[1] pdf.loc[valid_id, parquet_ensemble._flux_col] = pd.NA parquet_ensemble._source = dd.from_pandas(pdf, npartitions=1) + parquet_ensemble.update_frame(SourceFrame.from_tapeframe(TapeSourceFrame(pdf), npartitions=1, label="source")) # Sync the table and check that the number of objects decreased. parquet_ensemble.dropna(table="source") From 13d507b1f3de74725ad9bc856114d088417d437c Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Fri, 6 Oct 2023 15:37:45 -0700 Subject: [PATCH 14/35] Propagate EnsembleFrame._is_dirty (#264) * EnsembleFrames should propagate is_dirty * Test that a frame's dirty status propagates * Update doc strings * Address review comment --- src/tape/ensemble_frame.py | 182 ++++++++++++++++++++++-- tests/tape_tests/test_ensemble_frame.py | 36 ++++- 2 files changed, 209 insertions(+), 9 deletions(-) diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index db0e27fc..34e2b2e8 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -81,11 +81,19 @@ def _creates_meta(cls, meta, schema): class _Frame(dd.core._Frame): """Base class for extensions of Dask Dataframes that track additional Ensemble-related metadata.""" + _is_dirty = False # True if the underlying data is out of sync with the Ensemble + def __init__(self, dsk, name, meta, divisions, label=None, ensemble=None): super().__init__(dsk, name, meta, divisions) self.label = label # A label used by the Ensemble to identify this frame. self.ensemble = ensemble # The Ensemble object containing this frame. + def is_dirty(self): + return self._is_dirty + + def set_dirty(self, is_dirty): + self._is_dirty = is_dirty + @property def _args(self): # Ensure our Dask extension can correctly be used by pickle. @@ -107,6 +115,7 @@ def _propagate_metadata(self, new_frame): """ new_frame.label = self.label new_frame.ensemble = self.ensemble + new_frame.set_dirty(self.is_dirty) return new_frame def copy(self): @@ -177,6 +186,171 @@ def query(self, expr, **kwargs): """ result = super().query(expr, **kwargs) return self._propagate_metadata(result) + + def merge(self, right, **kwargs): + """Merge the Dataframe with another DataFrame + + Doc string below derived from dask.dataframe.core + + This will merge the two datasets, either on the indices, a certain column + in each dataset or the index in one dataset and the column in another. + + Parameters + ---------- + right: dask.dataframe.DataFrame + how : {'left', 'right', 'outer', 'inner'}, default: 'inner' + How to handle the operation of the two objects: + + - left: use calling frame's index (or column if on is specified) + - right: use other frame's index + - outer: form union of calling frame's index (or column if on is + specified) with other frame's index, and sort it + lexicographically + - inner: form intersection of calling frame's index (or column if + on is specified) with other frame's index, preserving the order + of the calling's one + + on : label or list + Column or index level names to join on. These must be found in both + DataFrames. If on is None and not merging on indexes then this + defaults to the intersection of the columns in both DataFrames. + left_on : label or list, or array-like + Column to join on in the left DataFrame. Other than in pandas + arrays and lists are only support if their length is 1. + right_on : label or list, or array-like + Column to join on in the right DataFrame. Other than in pandas + arrays and lists are only support if their length is 1. + left_index : boolean, default False + Use the index from the left DataFrame as the join key. + right_index : boolean, default False + Use the index from the right DataFrame as the join key. + suffixes : 2-length sequence (tuple, list, ...) + Suffix to apply to overlapping column names in the left and + right side, respectively + indicator : boolean or string, default False + If True, adds a column to output DataFrame called "_merge" with + information on the source of each row. If string, column with + information on source of each row will be added to output DataFrame, + and column will be named value of string. Information column is + Categorical-type and takes on a value of "left_only" for observations + whose merge key only appears in `left` DataFrame, "right_only" for + observations whose merge key only appears in `right` DataFrame, + and "both" if the observation’s merge key is found in both. + npartitions: int or None, optional + The ideal number of output partitions. This is only utilised when + performing a hash_join (merging on columns only). If ``None`` then + ``npartitions = max(lhs.npartitions, rhs.npartitions)``. + Default is ``None``. + shuffle: {'disk', 'tasks', 'p2p'}, optional + Either ``'disk'`` for single-node operation or ``'tasks'`` and + ``'p2p'``` for distributed operation. Will be inferred by your + current scheduler. + broadcast: boolean or float, optional + Whether to use a broadcast-based join in lieu of a shuffle-based + join for supported cases. By default, a simple heuristic will be + used to select the underlying algorithm. If a floating-point value + is specified, that number will be used as the ``broadcast_bias`` + within the simple heuristic (a large number makes Dask more likely + to choose the ``broacast_join`` code path). See ``broadcast_join`` + for more information. + + Notes + ----- + + There are three ways to join dataframes: + + 1. Joining on indices. In this case the divisions are + aligned using the function ``dask.dataframe.multi.align_partitions``. + Afterwards, each partition is merged with the pandas merge function. + + 2. Joining one on index and one on column. In this case the divisions of + dataframe merged by index (:math:`d_i`) are used to divide the column + merged dataframe (:math:`d_c`) one using + ``dask.dataframe.multi.rearrange_by_divisions``. In this case the + merged dataframe (:math:`d_m`) has the exact same divisions + as (:math:`d_i`). This can lead to issues if you merge multiple rows from + (:math:`d_c`) to one row in (:math:`d_i`). + + 3. Joining both on columns. In this case a hash join is performed using + ``dask.dataframe.multi.hash_join``. + + In some cases, you may see a ``MemoryError`` if the ``merge`` operation requires + an internal ``shuffle``, because shuffling places all rows that have the same + index in the same partition. To avoid this error, make sure all rows with the + same ``on``-column value can fit on a single partition. + """ + result = super().merge(right, **kwargs) + return self._propagate_metadata(result) + + def drop(self, labels=None, axis=0, columns=None, errors="raise"): + """Drop specified labels from rows or columns. + + Doc string below derived from dask.dataframe.core + + Remove rows or columns by specifying label names and corresponding + axis, or by directly specifying index or column names. When using a + multi-index, labels on different levels can be removed by specifying + the level. See the :ref:`user guide ` + for more information about the now unused levels. + + Parameters + ---------- + labels : single label or list-like + Index or column labels to drop. A tuple will be used as a single + label and not treated as a list-like. + axis : {0 or 'index', 1 or 'columns'}, default 0 + Whether to drop labels from the index (0 or 'index') or + columns (1 or 'columns'). + is equivalent to ``index=labels``). + columns : single label or list-like + Alternative to specifying axis (``labels, axis=1`` + is equivalent to ``columns=labels``). + errors : {'ignore', 'raise'}, default 'raise' + If 'ignore', suppress error and only existing labels are + dropped. + + Returns + ------- + result: `tape._Frame` + Returns the frame or Nonewith the specified + index or column labels removed or None if inplace=True. + """ + result = super().drop(labels=labels, axis=axis, columns=columns, errors=errors) + return self._propagate_metadata(result) + + def persist(self, **kwargs): + """Persist this dask collection into memory + + Doc string below derived from dask.base + + This turns a lazy Dask collection into a Dask collection with the same + metadata, but now with the results fully computed or actively computing + in the background. + + The action of function differs significantly depending on the active + task scheduler. If the task scheduler supports asynchronous computing, + such as is the case of the dask.distributed scheduler, then persist + will return *immediately* and the return value's task graph will + contain Dask Future objects. However if the task scheduler only + supports blocking computation then the call to persist will *block* + and the return value's task graph will contain concrete Python results. + + This function is particularly useful when using distributed systems, + because the results will be kept in distributed memory, rather than + returned to the local process as with compute. + + Parameters + ---------- + **kwargs + Extra keywords to forward to the scheduler function. + + Returns + ------- + result: `tape._Frame` + The modifed frame backed by in-memory data + """ + result = super().persist(**kwargs) + return self._propagate_metadata(result) def set_index( self, @@ -315,8 +489,6 @@ class EnsembleFrame(_Frame, dd.core.DataFrame): """ _partition_type = TapeFrame # Tracks the underlying data type - _is_dirty = False # True if the underlying data is out of sync with the Ensemble - def __getitem__(self, key): result = super().__getitem__(key) if isinstance(result, _Frame): @@ -487,12 +659,6 @@ def from_parquet( result.ensemble=ensemble return result - - def is_dirty(self): - return self._is_dirty - - def set_dirty(self, is_dirty): - self._is_dirty = is_dirty class TapeSourceFrame(TapeFrame): """A barebones extension of a Pandas frame to be used for underlying Ensemble source data diff --git a/tests/tape_tests/test_ensemble_frame.py b/tests/tape_tests/test_ensemble_frame.py index 678e7534..fcb138f3 100644 --- a/tests/tape_tests/test_ensemble_frame.py +++ b/tests/tape_tests/test_ensemble_frame.py @@ -75,6 +75,8 @@ def test_ensemble_frame_propagation(data_fixture, request): # Set a label and ensemble for the frame and copies/transformations retain them. ens_frame.label = TEST_LABEL ens_frame.ensemble=ens + assert not ens_frame.is_dirty() + ens_frame.set_dirty(True) # Create a copy of an EnsembleFrame and verify that it's still a proper # EnsembleFrame with appropriate metadata propagated. @@ -83,6 +85,7 @@ def test_ensemble_frame_propagation(data_fixture, request): assert isinstance(copied_frame._meta, TapeFrame) assert copied_frame.label == TEST_LABEL assert copied_frame.ensemble == ens + assert copied_frame.is_dirty() # Test that a filtered EnsembleFrame is still an EnsembleFrame. filtered_frame = ens_frame[["id", "time"]] @@ -90,6 +93,7 @@ def test_ensemble_frame_propagation(data_fixture, request): assert isinstance(filtered_frame._meta, TapeFrame) assert filtered_frame.label == TEST_LABEL assert filtered_frame.ensemble == ens + assert filtered_frame.is_dirty() # Test that the output of an EnsembleFrame query is still an EnsembleFrame queried_rows = ens_frame.query("flux > 3.0") @@ -97,6 +101,18 @@ def test_ensemble_frame_propagation(data_fixture, request): assert isinstance(queried_rows._meta, TapeFrame) assert queried_rows.label == TEST_LABEL assert queried_rows.ensemble == ens + assert queried_rows.is_dirty() + + # Test merging two subsets of the dataframe, dropping some columns, and persisting the result. + merged_frame = ens_frame.copy()[["id", "time", "error"]].merge( + ens_frame.copy()[["id", "time", "flux"]], on=["id"], suffixes=(None, "_drop_me")) + cols_to_drop = [col for col in merged_frame.columns if "_drop_me" in col] + merged_frame = merged_frame.drop(cols_to_drop, axis=1).persist() + assert isinstance(merged_frame, EnsembleFrame) + assert merged_frame.label == TEST_LABEL + assert merged_frame.ensemble == ens + assert merged_frame.is_dirty() + # Test that head returns a subset of the underlying TapeFrame. h = ens_frame.head(5) @@ -197,6 +213,9 @@ def test_object_and_source_frame_propagation(data_fixture, request): assert source_frame.ensemble == ens assert source_frame.ensemble is ens + assert not source_frame.is_dirty() + source_frame.set_dirty(True) + # Perform a series of operations on the SourceFrame and then verify the result is still a # proper SourceFrame with appropriate metadata propagated. source_frame["psFlux"].mean().compute() @@ -207,6 +226,7 @@ def test_object_and_source_frame_propagation(data_fixture, request): assert result_source_frame.label == SOURCE_LABEL assert result_source_frame.ensemble is not None assert result_source_frame.ensemble is ens + assert result_source_frame.is_dirty() # Set an index and then group by that index. result_source_frame = result_source_frame.set_index("psFlux", drop=True) @@ -228,6 +248,9 @@ def test_object_and_source_frame_propagation(data_fixture, request): assert isinstance(object_frame, ObjectFrame) assert isinstance(object_frame._meta, TapeObjectFrame) + assert not object_frame.is_dirty() + object_frame.set_dirty(True) + # Perform a series of operations on the ObjectFrame and then verify the result is still a # proper ObjectFrame with appropriate metadata propagated. result_object_frame = object_frame.copy()[["nobs_g", "nobs_total"]] @@ -235,6 +258,7 @@ def test_object_and_source_frame_propagation(data_fixture, request): assert isinstance(result_object_frame._meta, TapeObjectFrame) assert result_object_frame.label == OBJECT_LABEL assert result_object_frame.ensemble is ens + assert result_object_frame.is_dirty() # Set an index and then group by that index. result_object_frame = result_object_frame.set_index("nobs_g", drop=True) @@ -243,4 +267,14 @@ def test_object_and_source_frame_propagation(data_fixture, request): group_result = result_object_frame.groupby(["nobs_g"]).count() assert len(group_result) > 0 assert isinstance(group_result, ObjectFrame) - assert isinstance(group_result._meta, TapeObjectFrame) \ No newline at end of file + assert isinstance(group_result._meta, TapeObjectFrame) + + # Test merging source and object frames, dropping some columns, and persisting the result. + merged_frame = source_frame.copy().merge( + object_frame.copy(), on=[ens._id_col], suffixes=(None, "_drop_me")) + cols_to_drop = [col for col in merged_frame.columns if "_drop_me" in col] + merged_frame = merged_frame.drop(cols_to_drop, axis=1).persist() + assert isinstance(merged_frame, SourceFrame) + assert merged_frame.label == SOURCE_LABEL + assert merged_frame.ensemble == ens + assert merged_frame.is_dirty() \ No newline at end of file From 578900fd15a27fc07ad2499078ddd69a33d46817 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Tue, 10 Oct 2023 13:30:06 -0700 Subject: [PATCH 15/35] Have update_frame mark frames as dirty (#267) --- src/tape/ensemble.py | 4 ++++ tests/tape_tests/test_ensemble.py | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index b84520a0..4eef916f 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -152,6 +152,10 @@ def update_frame(self, frame): self._object = frame self.object = frame + # Set a frame as dirty if it was previously tracked and the number of rows has changed. + if frame.label in self.frames and len(self.frames[frame.label]) != len(frame): + frame.set_dirty(True) + # Ensure this frame is assigned to this Ensemble. frame.ensemble = self self.frames[frame.label] = frame diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index 08f7a6b8..eaceda55 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -83,15 +83,28 @@ def test_update_ensemble(data_fixture, request): # Filter the object table and have the ensemble track the updated table. updated_obj = ens._object.query("nobs_total > 50") assert updated_obj is not ens._object + # Update the ensemble and validate that it marks the object table dirty + assert ens._object.is_dirty() == False updated_obj.update_ensemble() + assert ens._object.is_dirty() == True assert updated_obj is ens._object - + # Filter the source table and have the ensemble track the updated table. updated_src = ens._source.query("psFluxErr > 0.1") assert updated_src is not ens._source + # Update the ensemble and validate that it marks the source table dirty + assert ens._source.is_dirty() == False updated_src.update_ensemble() + assert ens._source.is_dirty() == True assert updated_src is ens._source + # Compute a result to trigger a table sync + obj, src = ens.compute() + assert len(obj) > 0 + assert len(src) > 0 + assert ens._object.is_dirty() == False + assert ens._source.is_dirty() == False + # Create an additional result table for the ensemble to track. cnts = ens._source.groupby([ens._id_col, ens._band_col])[ens._time_col].aggregate("count") res = ( From 35de81ccfe5df19ec1ab8ebd76bb56d943f45792 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Tue, 10 Oct 2023 15:48:42 -0700 Subject: [PATCH 16/35] Remove calls to set_dirty in ensemble (#269) --- src/tape/ensemble.py | 13 ++----------- tests/tape_tests/test_ensemble.py | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index 4eef916f..13294236 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -327,7 +327,6 @@ def insert_sources( # Append the new rows to the correct divisions. self.update_frame(dd.concat([self._source, df2], axis=0, interleave_partitions=True)) - self._source.set_dirty(True) # Do the repartitioning if requested. If the divisions were set, reuse them. # Otherwise, use the same number of partitions. @@ -462,10 +461,8 @@ def dropna(self, table="source", **kwargs): """ if table == "object": self.update_frame(self._object.dropna(**kwargs)) - self._object.set_dirty(True) # This operation modifies the object table elif table == "source": self.update_frame(self._source.dropna(**kwargs)) - self._source.set_dirty(True) # This operation modifies the source table else: raise ValueError(f"{table} is not one of 'object' or 'source'") @@ -521,10 +518,8 @@ def query(self, expr, table="object"): self._lazy_sync_tables(table) if table == "object": self.update_frame(self._object.query(expr)) - self._object.set_dirty(True) elif table == "source": self.update_frame(self._source.query(expr)) - self._source.set_dirty(True) return self def filter_from_series(self, keep_series, table="object"): @@ -543,10 +538,9 @@ def filter_from_series(self, keep_series, table="object"): self._lazy_sync_tables(table) if table == "object": self.update_frame(self._object[keep_series]) - self._object.set_dirty(True) + elif table == "source": self.update_frame(self._source[keep_series]) - self._source.set_dirty(True) return self def assign(self, table="object", **kwargs): @@ -578,10 +572,9 @@ def assign(self, table="object", **kwargs): if table == "object": self.update_frame(self._object.assign(**kwargs)) - self._object.set_dirty(True) + elif table == "source": self.update_frame(self._source.assign(**kwargs)) - self._source.set_dirty(True) else: raise ValueError(f"{table} is not one of 'object' or 'source'") return self @@ -696,7 +689,6 @@ def prune(self, threshold=50, col_name=None): mask = self._object[col_name] >= threshold self.update_frame(self._object[mask]) - self._object.set_dirty(True) # Object Table is now dirty return self @@ -841,7 +833,6 @@ def bin_sources( self.update_frame(self._source.reset_index().set_index(self._id_col).drop(tmp_time_col, axis=1)) # Mark the source table as dirty. - self._source.set_dirty(True) return self def batch(self, func, *args, meta=None, use_map=True, compute=True, on=None, **kwargs): diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index eaceda55..cdcca0f4 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -573,7 +573,14 @@ def test_sync_tables(parquet_ensemble): parquet_ensemble.prune(50, col_name="nobs_r").prune(50, col_name="nobs_g") assert parquet_ensemble._object.is_dirty() # Prune should set the object dirty flag + # Replace the maximum flux value with a NaN so that we will have a row to drop. + max_flux = max(parquet_ensemble._source[parquet_ensemble._flux_col]) + parquet_ensemble._source[parquet_ensemble._flux_col] = parquet_ensemble._source[ + parquet_ensemble._flux_col].apply( + lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float) + ) parquet_ensemble.dropna(table="source") + assert len(parquet_ensemble._source.compute()) == 1999 # We dropped one source row due to a NaN assert parquet_ensemble._source.is_dirty() # Dropna should set the source dirty flag parquet_ensemble._sync_tables() @@ -610,7 +617,13 @@ def test_lazy_sync_tables(parquet_ensemble): assert not parquet_ensemble._object.is_dirty() assert not parquet_ensemble._source.is_dirty() - # Modify only the source table. + # Modify only the source table. + # Replace the maximum flux value with a NaN so that we will have a row to drop. + max_flux = max(parquet_ensemble._source[parquet_ensemble._flux_col]) + parquet_ensemble._source[parquet_ensemble._flux_col] = parquet_ensemble._source[ + parquet_ensemble._flux_col].apply( + lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float) + ) parquet_ensemble.dropna(table="source") assert not parquet_ensemble._object.is_dirty() assert parquet_ensemble._source.is_dirty() @@ -659,7 +672,6 @@ def test_dropna(parquet_ensemble): # parquet_ensemble._sync_tables() # Now test dropping na from 'object' table - # object_pdf = parquet_ensemble._object.compute() object_length = len(object_pdf.index) From 683c362756cb4c4b86a757a858458223397d4241 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Thu, 19 Oct 2023 15:50:41 -0700 Subject: [PATCH 17/35] Update refactor (#274) * Add ensemble loader functions for dataframes * Updated unit tests * Lint fixes * Always update column mapping * Addressed review comments * Ensure object frame is indexed * adds a dask_on_ray tutorial * add performance comp; add use_map comment --------- Co-authored-by: Doug Branton --- docs/requirements.txt | 3 +- docs/tutorials.rst | 1 + .../using_ray_with_the_ensemble.ipynb | 184 +++++++++ pyproject.toml | 3 +- src/tape/ensemble.py | 358 ++++++++++-------- tests/tape_tests/conftest.py | 61 ++- tests/tape_tests/test_ensemble.py | 82 ++-- 7 files changed, 495 insertions(+), 197 deletions(-) create mode 100644 docs/tutorials/using_ray_with_the_ensemble.ipynb diff --git a/docs/requirements.txt b/docs/requirements.txt index 1511e27b..3ee39d08 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,4 +6,5 @@ ipython jupytext jupyter matplotlib -eztao \ No newline at end of file +eztao +ray \ No newline at end of file diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 7f18d5fd..8cb1c6cf 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -13,3 +13,4 @@ functionality. Binning Sources in the Ensemble Structure Function Showcase Loading Data into the Ensemble + Using Ray with the Ensemble diff --git a/docs/tutorials/using_ray_with_the_ensemble.ipynb b/docs/tutorials/using_ray_with_the_ensemble.ipynb new file mode 100644 index 00000000..f0ba09a0 --- /dev/null +++ b/docs/tutorials/using_ray_with_the_ensemble.ipynb @@ -0,0 +1,184 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "bcb10f72-948f-475e-a856-4f5c9516fd5e", + "metadata": {}, + "source": [ + "# Using Dask on Ray with the Ensemble\n", + "\n", + "[Ray](https://docs.ray.io/en/latest/ray-overview/index.html) is an open-source unified framework for scaling AI and Python applications. Ray provides a scheduler for Dask ([dask_on_ray](https://docs.ray.io/en/latest/ray-more-libs/dask-on-ray.html)) which allows you to build data analyses using Dask’s collections and execute the underlying tasks on a Ray cluster. We have found with TAPE that the Ray scheduler is often more performant than Dasks scheduler. Ray can be used on TAPE using the setup shown in the following example." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ace065cd-5c75-4282-bca5-36ebe6868234", + "metadata": {}, + "outputs": [], + "source": [ + "import ray\n", + "from ray.util.dask import enable_dask_on_ray, disable_dask_on_ray\n", + "from tape import Ensemble\n", + "from tape.analysis.structurefunction2 import calc_sf2\n", + "\n", + "context = ray.init()\n", + "\n", + "# Use the Dask config helper to set the scheduler to ray_dask_get globally,\n", + "# without having to specify it on each compute call.\n", + "enable_dask_on_ray()" + ] + }, + { + "cell_type": "markdown", + "id": "e6e9fa72-5811-4750-8ba8-bcd762eb80fa", + "metadata": {}, + "source": [ + "We import ray, and just need to invoke two commands. `context = ray.init()` starts a local ray cluster, and we can use this context object to retrieve the url of the ray dashboard, as shown below. `enable_dask_on_ray()` is a dask configuration function that sets up all Dask work to use the established Ray cluster." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04453edd-b22b-43cb-abc3-e61e0c958b04", + "metadata": {}, + "outputs": [], + "source": [ + "print(context.dashboard_url)" + ] + }, + { + "cell_type": "markdown", + "id": "f9ad55cc-2203-4145-be1c-0af331805624", + "metadata": {}, + "source": [ + "For TAPE, the only needed change is to specify `client=False` when initializing an `Ensemble` object. Because the Dask configuration has been set, the Ensemble will automatically use the established Ray cluster." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2cf7c608-fc46-455e-a7f7-04e8b64d52ec", + "metadata": {}, + "outputs": [], + "source": [ + "ens=Ensemble(client=False) # Do not use a client" + ] + }, + { + "cell_type": "markdown", + "id": "6a1b904e-7bf6-4dd5-b1e6-0c6229a98739", + "metadata": {}, + "source": [ + "From here, we are free to work with TAPE as normal." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0e3bf1a-f9b9-45be-9fea-390d25380794", + "metadata": {}, + "outputs": [], + "source": [ + "ens.from_dataset(\"s82_qso\")\n", + "ens._source = ens._source.repartition(npartitions=10)\n", + "ens.batch(calc_sf2, use_map=False) # use_map is false as we repartition naively, splitting per-object sources across partitions" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "c5692d75", + "metadata": {}, + "source": [ + "## Timing Comparison\n", + "\n", + "As mentioned above, we generally see that Ray is more performant than Dask. Below is a simple timing comparison." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f128cdbf", + "metadata": {}, + "source": [ + "### Ray Timing" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd960e10", + "metadata": {}, + "outputs": [], + "source": [ + "%%time\n", + "\n", + "ens=Ensemble(client=False) # Do not use a client\n", + "ens.from_dataset(\"s82_qso\")\n", + "ens._source = ens._source.repartition(npartitions=10)\n", + "ens.batch(calc_sf2, use_map=False)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "228e5114", + "metadata": {}, + "source": [ + "### Dask Timing" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24a8f466", + "metadata": {}, + "outputs": [], + "source": [ + "disable_dask_on_ray() # unsets the dask_on_ray configuration settings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1552c2b8", + "metadata": {}, + "outputs": [], + "source": [ + "%%time\n", + "\n", + "ens = Ensemble()\n", + "ens.from_dataset(\"s82_qso\")\n", + "ens._source = ens._source.repartition(npartitions=10)\n", + "ens.batch(calc_sf2, use_map=False)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + }, + "vscode": { + "interpreter": { + "hash": "83afbb17b435d9bf8b0d0042367da76f26510da1c5781f0ff6e6c518eab621ec" + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index fc1287e1..51cbc490 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,8 @@ dev = [ "ipython", # Also used in building notebooks into Sphinx "matplotlib", # Used in sample notebook intro_notebook.ipynb "eztao==0.4.1", # Used in Structure Function example notebook - "bokeh", # Used to render dask client dashboard in Scaling to Large Data notebook + "bokeh", # Used to render dask client dashboard in Scaling to Large Data notebook + "ray[default]" # Used in the Ray on Ensemble notebook ] [project.urls] diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index 13294236..42056f57 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -12,7 +12,7 @@ from .analysis.feature_extractor import BaseLightCurveFeature, FeatureExtractor from .analysis.structure_function import SF_METHODS from .analysis.structurefunction2 import calc_sf2 -from .ensemble_frame import ObjectFrame, SourceFrame, TapeObjectFrame +from .ensemble_frame import ObjectFrame, SourceFrame, TapeObjectFrame, TapeSourceFrame from .timeseries import TimeSeries from .utils import ColumnMapper @@ -963,6 +963,136 @@ def s2n_inter_quartile_range(flux, err): else: return batch + def from_pandas( + self, + source_frame, + object_frame=None, + column_mapper=None, + sync_tables=True, + npartitions=None, + partition_size=None, + **kwargs, + ): + """Read in Pandas dataframe(s) into an ensemble object + + Parameters + ---------- + source_frame: 'pandas.Dataframe' + A Dask dataframe that contains source information to be read into the ensemble + object_frame: 'pandas.Dataframe', optional + If not specified, the object frame is generated from the source frame + column_mapper: 'ColumnMapper' object + If provided, the ColumnMapper is used to populate relevant column + information mapped from the input dataset. + sync_tables: 'bool', optional + In the case where an `object_frame`is provided, determines whether an + initial sync is performed between the object and source tables. If + not performed, dynamic information like the number of observations + may be out of date until a sync is performed internally. + npartitions: `int`, optional + If specified, attempts to repartition the ensemble to the specified + number of partitions + partition_size: `int`, optional + If specified, attempts to repartition the ensemble to partitions + of size `partition_size`. + + Returns + ---------- + ensemble: `tape.ensemble.Ensemble` + The ensemble object with the Dask dataframe data loaded. + """ + # Construct Dask DataFrames of the source and object tables + source = dd.from_pandas(source_frame, npartitions=npartitions) + object = None if object_frame is None else dd.from_pandas(object_frame) + return self.from_dask_dataframe( + source, + object_frame=object, + column_mapper=column_mapper, + sync_tables=sync_tables, + npartitions=npartitions, + partition_size=partition_size, + **kwargs, + ) + + def from_dask_dataframe( + self, + source_frame, + object_frame=None, + column_mapper=None, + sync_tables=True, + npartitions=None, + partition_size=None, + **kwargs, + ): + """Read in Dask dataframe(s) into an ensemble object + + Parameters + ---------- + source_frame: 'dask.Dataframe' + A Dask dataframe that contains source information to be read into the ensemble + object_frame: 'dask.Dataframe', optional + If not specified, the object frame is generated from the source frame + column_mapper: 'ColumnMapper' object + If provided, the ColumnMapper is used to populate relevant column + information mapped from the input dataset. + sync_tables: 'bool', optional + In the case where an `object_frame`is provided, determines whether an + initial sync is performed between the object and source tables. If + not performed, dynamic information like the number of observations + may be out of date until a sync is performed internally. + npartitions: `int`, optional + If specified, attempts to repartition the ensemble to the specified + number of partitions + partition_size: `int`, optional + If specified, attempts to repartition the ensemble to partitions + of size `partition_size`. + + Returns + ---------- + ensemble: `tape.ensemble.Ensemble` + The ensemble object with the Dask dataframe data loaded. + """ + self._load_column_mapper(column_mapper, **kwargs) + + # TODO(wbeebe@uw.edu): Determine most efficient way to convert to SourceFrame/ObjectFrame + source_frame = SourceFrame.from_dask_dataframe(source_frame, self) + + # Set the index of the source frame and save the resulting table + self.update_frame(source_frame.set_index(self._id_col, drop=True)) + + if object_frame is None: # generate an indexed object table from source + self.update_frame(self._generate_object_table()) + self._nobs_bands = [col for col in list(self._object.columns) if col != self._nobs_tot_col] + else: + # TODO(wbeebe@uw.edu): Determine most efficient way to convert to SourceFrame/ObjectFrame + self.update_frame(ObjectFrame.from_dask_dataframe(object_frame, ensemble=self)) + if self._nobs_band_cols is None: + # sets empty nobs cols in object + unq_filters = np.unique(self._source[self._band_col]) + self._nobs_band_cols = [f"nobs_{filt}" for filt in unq_filters] + for col in self._nobs_band_cols: + self._object[col] = np.nan + + # Handle nobs_total column + if self._nobs_tot_col is None: + self._object["nobs_total"] = np.nan + self._nobs_tot_col = "nobs_total" + + self.update_frame(self._object.set_index(self._id_col)) + + # Optionally sync the tables, recalculates nobs columns + if sync_tables: + self._source.set_dirty(True) + self._object.set_dirty(True) + self._sync_tables() + + if npartitions and npartitions > 1: + self.update_frame(self._source.repartition(npartitions=npartitions)) + elif partition_size: + self.update_frame(self._source.repartition(partition_size=partition_size)) + + return self + def from_hipscat(self, dir, source_subdir="source", object_subdir="object", column_mapper=None, **kwargs): """Read in parquet files from a hipscat-formatted directory structure Parameters @@ -1158,147 +1288,26 @@ def from_parquet( columns.append(col) # Read in the source parquet file(s) - self.update_frame(SourceFrame.from_parquet( - source_file, index=self._id_col, columns=columns, ensemble=self, - )) - - if object_file: # read from parquet files - # Read in the object file(s) - self.update_frame(ObjectFrame.from_parquet(object_file, index=self._id_col, ensemble=self)) - if self._nobs_band_cols is None: - # sets empty nobs cols in object - unq_filters = np.unique(self._source[self._band_col]) - self._nobs_band_cols = [f"nobs_{filt}" for filt in unq_filters] - for col in self._nobs_band_cols: - self._object[col] = np.nan - - # Handle nobs_total column - if self._nobs_tot_col is None: - self._object["nobs_total"] = np.nan - self._nobs_tot_col = "nobs_total" - - # Optionally sync the tables, recalculates nobs columns - if sync_tables: - self._source.set_dirty(True) - self._object.set_dirty(True) - self._sync_tables() - - else: # generate object table from source - self.update_frame(self._generate_object_table()) - self._nobs_bands = [col for col in list(self._object.columns) if col != self._nobs_tot_col] - - # Generate a provenance column if not provided - if self._provenance_col is None: - self._source["provenance"] = self._source.apply( - lambda x: provenance_label, axis=1, meta=pd.Series(name="provenance", dtype=str) - ) - self._provenance_col = "provenance" - - if npartitions and npartitions > 1: - self.update_frame(self._source.repartition(npartitions=npartitions)) - elif partition_size: - self.update_frame(self._source.repartition(partition_size=partition_size)) - - return self - - def objsor_from_parquet( - self, - source_file, - object_file, - column_mapper=None, - provenance_label="survey_1", - sync_tables=True, - additional_cols=True, - npartitions=None, - partition_size=None, - **kwargs, - ): - """Read in parquet file(s) for the object and source tables into an Ensemble object. - - Parameters - ---------- - source_file: 'str' - Path to a parquet file, or multiple parquet files that contain - source information to be read into the ensemble - object_file: 'str' - Path to a parquet file, or multiple parquet files that contain - object information. - column_mapper: 'ColumnMapper' object - If provided, the ColumnMapper is used to populate relevant column - information mapped from the input dataset. - provenance_label: 'str', optional - Determines the label to use if a provenance column is generated - sync_tables: 'bool', optional - In the case where object files are loaded in, determines whether an - initial sync is performed between the object and source tables. If - not performed, dynamic information like the number of observations - may be out of date until a sync is performed internally. - additional_cols: 'bool', optional - Boolean to indicate whether to carry in columns beyond the - critical columns, True will, while False will only load the columns - containing the critical quantities (id,time,flux,err,band) - npartitions: `int`, optional - If specified, attempts to repartition the ensemble to the specified - number of partitions - partition_size: `int`, optional - If specified, attempts to repartition the ensemble to partitions - of size `partition_size`, the maximum number of bytes for partition - as computed by `pandas.Dataframe.memory_usage`. - - Returns - ---------- - ensemble: `tape.ensemble.Ensemble` - The ensemble object with parquet data loaded - """ - - # load column mappings - self._load_column_mapper(column_mapper, **kwargs) - - # Handle additional columnss - if additional_cols: - columns = None # None will prompt read_parquet to read in all cols - else: - columns = [self._time_col, self._flux_col, self._err_col, self._band_col] - if self._provenance_col is not None: - columns.append(self._provenance_col) - if self._nobs_tot_col is not None: - columns.append(self._nobs_tot_col) - if self._nobs_band_cols is not None: - for col in self._nobs_band_cols: - columns.append(col) - - # Read in the source parquet file(s) - self.update_frame(SourceFrame.from_parquet(source_file, index=self._id_col, columns=columns, - ensemble=self)) - - # Read in the object file(s) - self.update_frame(ObjectFrame.from_parquet(object_file, index=self._id_col, ensemble=self)) - - if self._nobs_band_cols is None: - # sets empty nobs cols in object - unq_filters = np.unique(self.source[self._band_col]) - self._nobs_band_cols = [f"nobs_{filt}" for filt in unq_filters] - for col in self._nobs_band_cols: - self.object[col] = np.nan - - # Handle nobs_total column - if self._nobs_tot_col is None: - self.object["nobs_total"] = np.nan - self._nobs_tot_col = "nobs_total" + source = SourceFrame.from_parquet(source_file, index=self._id_col, columns=columns, ensemble=self) - # TODO(wbeebe@uw.edu) Add in table syncing logic as part of milestone 4 - # Generate a provenance column if not provided if self._provenance_col is None: - self.source["provenance"] = provenance_label + source["provenance"] = provenance_label self._provenance_col = "provenance" - if npartitions and npartitions > 1: - self.update_frame(self.source.repartition(npartitions=npartitions)) - elif partition_size: - self.update_frame(self.source.repartition(partition_size=partition_size)) - - return self + object = None + if object_file: + # Read in the object file(s) + object = ObjectFrame.from_parquet(object_file, index=self._id_col, ensemble=self) + return self.from_dask_dataframe( + source_frame=source, + object_frame=object, + column_mapper=column_mapper, + sync_tables=sync_tables, + npartitions=npartitions, + partition_size=partition_size, + **kwargs, + ) def from_dataset(self, dataset, **kwargs): """Load the ensemble from a TAPE dataset. @@ -1373,20 +1382,73 @@ def from_source_dict(self, source_dict, column_mapper=None, npartitions=1, **kwa ensemble: `tape.ensemble.Ensemble` The ensemble object with dictionary data loaded """ - # load column mappings - self._load_column_mapper(column_mapper, **kwargs) - # Load in the source data. - self.update_frame(SourceFrame.from_dict(source_dict, npartitions=npartitions)) - self.update_frame(self._source.set_index(self._id_col, drop=True)) + # Load the source data into a dataframe. + source_frame = SourceFrame.from_dict(source_dict, npartitions=npartitions) - # Generate the object table from the source. - # TODO this is not the object Table oh no.... - self.update_frame(self._generate_object_table()) + return self.from_dask_dataframe( + source_frame, + object_frame=None, + column_mapper=column_mapper, + sync_tables=True, + npartitions=npartitions, + **kwargs, + ) + + def convert_flux_to_mag(self, flux_col, zero_point, err_col=None, zp_form="mag", out_col_name=None): + """Converts a flux column into a magnitude column. + + Parameters + ---------- + flux_col: 'str' + The name of the ensemble flux column to convert into magnitudes. + zero_point: 'str' + The name of the ensemble column containing the zero point + information for column transformation. + err_col: 'str', optional + The name of the ensemble column containing the errors to propagate. + Errors are propagated using the following approximation: + Err= (2.5/log(10))*(flux_error/flux), which holds mainly when the + error in flux is much smaller than the flux. + zp_form: `str`, optional + The form of the zero point column, either "flux" or + "magnitude"/"mag". Determines how the zero point (zp) is applied in + the conversion. If "flux", then the function is applied as + mag=-2.5*log10(flux/zp), or if "magnitude", then + mag=-2.5*log10(flux)+zp. + out_col_name: 'str', optional + The name of the output magnitude column, if None then the output + is just the flux column name + "_mag". The error column is also + generated as the out_col_name + "_err". + + Returns + ---------- + ensemble: `tape.ensemble.Ensemble` + The ensemble object with a new magnitude (and error) column. + + """ + if out_col_name is None: + out_col_name = flux_col + "_mag" + + if zp_form == "flux": # mag = -2.5*np.log10(flux/zp) + self.update_frame(self._source.assign( + **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / x[zero_point])} + )) + + elif zp_form == "magnitude" or zp_form == "mag": # mag = -2.5*np.log10(flux) + zp + self.update_frame(self._source.assign( + **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + x[zero_point]} + )) + + else: + raise ValueError(f"{zp_form} is not a valid zero_point format.") + + # Calculate Errors + if err_col is not None: + self.update_frame(self._source.assign( + **{out_col_name + "_err": lambda x: (2.5 / np.log(10)) * (x[err_col] / x[flux_col])} + )) - # Now synced and clean - self._source.set_dirty(False) - self._object.set_dirty(False) return self def _generate_object_table(self): diff --git a/tests/tape_tests/conftest.py b/tests/tape_tests/conftest.py index 770dae91..a62c6e2e 100644 --- a/tests/tape_tests/conftest.py +++ b/tests/tape_tests/conftest.py @@ -1,4 +1,8 @@ """Test fixtures for Ensemble manipulations""" +import numpy as np +import pandas as pd +import dask.dataframe as dd + import pytest from dask.distributed import Client @@ -44,7 +48,7 @@ def parquet_files_and_ensemble_without_client(): err_col="psFluxErr", band_col="filterName", ) - ens = ens.objsor_from_parquet( + ens = ens.from_parquet( source_file, object_file, column_mapper=colmap) @@ -137,6 +141,61 @@ def parquet_ensemble_from_hipscat(dask_client): return ens + +# pylint: disable=redefined-outer-name +@pytest.fixture +def dask_dataframe_ensemble(dask_client): + """Create an Ensemble from parquet data.""" + ens = Ensemble(client=dask_client) + + num_points = 1000 + all_bands = np.array(["r", "g", "b", "i"]) + rows = { + "id": 8000 + (np.arange(num_points) % 5), + "time": np.arange(num_points), + "flux": np.arange(num_points) % len(all_bands), + "band": np.repeat(all_bands, num_points / len(all_bands)), + "err": 0.1 * (np.arange(num_points) % 10), + "count": np.arange(num_points), + "something_else": np.full(num_points, None), + } + cmap = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="err", band_col="band") + + ens.from_dask_dataframe( + dd.from_dict(rows, npartitions=1), + column_mapper=cmap, + ) + + return ens + + +# pylint: disable=redefined-outer-name +@pytest.fixture +def pandas_ensemble(dask_client): + """Create an Ensemble from parquet data.""" + ens = Ensemble(client=dask_client) + + num_points = 1000 + all_bands = np.array(["r", "g", "b", "i"]) + rows = { + "id": 8000 + (np.arange(num_points) % 5), + "time": np.arange(num_points), + "flux": np.arange(num_points) % len(all_bands), + "band": np.repeat(all_bands, num_points / len(all_bands)), + "err": 0.1 * (np.arange(num_points) % 10), + "count": np.arange(num_points), + "something_else": np.full(num_points, None), + } + cmap = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="err", band_col="band") + + ens.from_pandas( + pd.DataFrame(rows), + column_mapper=cmap, + npartitions=1, + ) + + return ens + # pylint: disable=redefined-outer-name @pytest.fixture def ensemble_from_source_dict(dask_client): diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index cdcca0f4..e2aecd6f 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -67,6 +67,42 @@ def test_from_parquet(data_fixture, request): # Check to make sure the critical quantity labels are bound to real columns assert parquet_ensemble._source[col] is not None +@pytest.mark.parametrize( + "data_fixture", + [ + "dask_dataframe_ensemble", + "pandas_ensemble", + ], +) +def test_from_dataframe(data_fixture, request): + """ + Tests constructing an ensemble from pandas and dask dataframes. + """ + ens = request.getfixturevalue(data_fixture) + + # Check to make sure the source and object tables were created + assert ens._source is not None + assert ens._object is not None + + # Check that the data is not empty. + obj, source = ens.compute() + assert len(source) == 1000 + assert len(obj) == 5 + + # Check that source and object both have the same ids present + np.testing.assert_array_equal(np.unique(source.index), np.sort(obj.index)) + + # Check the we loaded the correct columns. + for col in [ + ens._time_col, + ens._flux_col, + ens._err_col, + ens._band_col, + ]: + # Check to make sure the critical quantity labels are bound to real columns + assert ens._source[col] is not None + + @pytest.mark.parametrize( "data_fixture", [ @@ -123,50 +159,6 @@ def test_update_ensemble(data_fixture, request): result_frame.ensemble = None assert result_frame.update_ensemble() is None - -@pytest.mark.parametrize( - "data_fixture", - [ - "parquet_files_and_ensemble_without_client", - ], -) -def test_objsor_from_parquet(data_fixture, request): - """ - Test that the ensemble successfully loads a SourceFrame and ObjectFrame form parquet files. - """ - _, source_file, object_file, colmap = request.getfixturevalue(data_fixture) - - ens = Ensemble(client=False) - ens = ens.objsor_from_parquet(source_file, object_file, column_mapper=colmap) - - assert ens is not None - - # Check to make sure the source and object tables were created - assert ens.source is not None - assert ens.object is not None - assert isinstance(ens.source, SourceFrame) - assert isinstance(ens.object, ObjectFrame) - - # Check that the data is not empty. - obj, source = ens.compute() - assert len(source) == 2000 - assert len(obj) == 15 - - # Check that source and object both have the same ids present - assert sorted(np.unique(list(source.index))) == sorted(np.array(obj.index)) - - # Check the we loaded the correct columns. - for col in [ - ens._time_col, - ens._flux_col, - ens._err_col, - ens._band_col, - ens._provenance_col, - ]: - # Check to make sure the critical quantity labels are bound to real columns - assert ens.source[col] is not None - - def test_available_datasets(dask_client): """ Test that the ensemble is able to successfully read in the list of available TAPE datasets @@ -190,8 +182,6 @@ def test_frame_tracking(data_fixture, request): """ ens, source_file, object_file, colmap = request.getfixturevalue(data_fixture) - ens = ens.objsor_from_parquet(source_file, object_file, column_mapper=colmap) - # Since we load the ensemble from a parquet, we expect the Source and Object frames to be populated. assert len(ens.frames) == 2 assert isinstance(ens.select_frame("source"), SourceFrame) From 5a542f3655db4ad471f55e33fcc1099455dd11ee Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Tue, 24 Oct 2023 14:00:28 -0700 Subject: [PATCH 18/35] Merge main into tape_ensemble_refactor (#277) * Add ensemble loader functions for dataframes * Updated unit tests * Lint fixes * Always update column mapping * Addressed review comments * Ensure object frame is indexed * adds a dask_on_ray tutorial * add performance comp; add use_map comment * sync with map_partitions * sync with map_partitions * sync with map_partitions * sync with map_partitions * coalesce with map_partitions * use dataframes instead of series * add descriptive comments * implement suggestions * Update TAPE README.md Update the project description for TAPE to better reflect the current state and goals of the project. * Set object table index for from_dask_dataframe * add zero_point as float input :q q * add ensemble default cols * S82 RRLyr notebook * Move rrlyr nb to examples * Update requirements.txt to unpin sphinx * Update pyproject.toml to unpin sphinx * add calc_nobs * add calc_nobs * add calc_nobs * reduce scope of sync_tables * address divisions issue * add temporary cols test * improve coverage * add temporary kwarg to assign * add temporary kwarg to assign * drop divisions * drop brackets * fix bug in sync * Issue 199: Added static Ensemble read constructors to tape namespace (#256) * Added static read constructors to tape namespace * Removed @staticmethod as python 3.9 didn't like it * Reformatted via black * Changed read_dask_dataframe to call from_ method * Collapsed create dask client args to single arg * Fixed dask_client parameter * reformatted via black * Added missing unit test * Resolved code review comments from PR 256 * Fixed failing unit test Removed reference to Ensemble._nobs_band_cols field * fix bug in sync --------- Co-authored-by: Doug Branton Co-authored-by: Konstantin Malanchev Co-authored-by: Olivia R. Lynn Co-authored-by: Chris Wenneman <57197008+wenneman@users.noreply.github.com> --- README.md | 2 +- docs/examples.rst | 8 + docs/examples/rrlyr-period.ipynb | 141 +++++ docs/gettingstarted/quickstart.ipynb | 1 + docs/index.rst | 1 + docs/requirements.txt | 2 +- .../binning_slowly_changing_sources.ipynb | 44 +- .../structure_function_showcase.ipynb | 528 ++---------------- docs/tutorials/tape_datasets.ipynb | 3 +- .../tutorials/working_with_the_ensemble.ipynb | 316 ++--------- src/tape/__init__.py | 1 + src/tape/ensemble.py | 317 ++++++----- src/tape/ensemble_readers.py | 328 +++++++++++ src/tape/utils/column_mapper/column_mapper.py | 18 - tests/tape_tests/conftest.py | 279 ++++++++- tests/tape_tests/test_ensemble.py | 325 ++++++++++- tests/tape_tests/test_utils.py | 10 +- 17 files changed, 1369 insertions(+), 955 deletions(-) create mode 100644 docs/examples.rst create mode 100644 docs/examples/rrlyr-period.ipynb create mode 100644 src/tape/ensemble_readers.py diff --git a/README.md b/README.md index c5595795..c679832e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Package for working with LSST time series data -Given the duration and cadence of Rubin LSST, the survey will generate a vast amount of time series information capturing the variability of various objects. Scientists will need flexible and highly scalable tools to store and analyze O(Billions) of time series. Ideally we would like to provide a single unified interface, similar to [RAIL’s](https://lsstdescrail.readthedocs.io/en/latest/index.html) approach for photo-zs, that allows scientists to fit and analyze time series using a variety of methods. This would include implementation of different optimizers, ability to ingest different time series formats, and a set of metrics for comparing model performance (e.g. AIC or Bayes factors). +Given the duration and cadence of [Vera C. Rubin LSST](https://www.lsst.org/about), the survey will generate a vast amount of time series information capturing the variability of various objects. Scientists will need flexible and highly scalable tools to store and analyze O(Billions) of time series. The **Time series Analysis and Processing Engine** (TAPE) is a framework for distributed time series analysis which enables the user to scale their algorithm to LSST data sizes. It allows for efficient and scalable evaluation of algorithms on time domain data through built-in fitting and analysis methods as well as support for user-provided algorithms. TAPE supports ingestion of multiple time series formats, enabling easy access to both LSST time series objects and data from other astronomical surveys. In short term we are working on two main goals of the project: - Enable ease of access to TimeSeries objects in LSST diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 00000000..2088c92b --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,8 @@ +Examples +======================================================================================== + +Some examples of how to use the TAPE package are provided in these notebooks. + +.. toctree:: + + Use Lomb–Scargle Periodograms for SDSS Stripe 82 RR Lyrae diff --git a/docs/examples/rrlyr-period.ipynb b/docs/examples/rrlyr-period.ipynb new file mode 100644 index 00000000..2dfa0089 --- /dev/null +++ b/docs/examples/rrlyr-period.ipynb @@ -0,0 +1,141 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7bc777c97b317198", + "metadata": { + "collapsed": false + }, + "source": [ + "# Explore SDSS Stripe 82 RR Lyrae catalog with period-folding\n", + "\n", + "This short example notebook demonstrates how to use TAPE to explore the SDSS Stripe 82 RR Lyrae catalog. We will use a Lomb–Scargle periodogram to extract periods from r-band light curves and select the RR Lyrae star with the most confident period determination. Then, we will plot the period-folded light curve for this RR Lyrae star." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "initial_id", + "metadata": { + "ExecuteTime": { + "end_time": "2023-09-20T13:16:49.339804Z", + "start_time": "2023-09-20T13:16:48.655140Z" + } + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "from light_curve import Periodogram\n", + "from tape import Ensemble" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fecf2313f49ad1ac", + "metadata": { + "ExecuteTime": { + "end_time": "2023-09-20T13:16:53.703300Z", + "start_time": "2023-09-20T13:16:49.340873Z" + } + }, + "outputs": [], + "source": [ + "# Load SDSS Stripe 82 RR Lyrae catalog\n", + "ens = Ensemble(client=False).from_dataset('s82_rrlyrae')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c2dd5a5fd58ce00", + "metadata": { + "ExecuteTime": { + "end_time": "2023-09-20T13:17:00.548389Z", + "start_time": "2023-09-20T13:16:53.706738Z" + } + }, + "outputs": [], + "source": [ + "%%time\n", + "\n", + "# Filter out invalid detections, \"flux\" denotes magnitude column\n", + "ens = ens.query(\"10 < flux < 25\", table=\"source\")\n", + "\n", + "# Find periods using Lomb-Scargle periodogram\n", + "periodogram = Periodogram(peaks=1, nyquist=0.1, max_freq_factor=10, fast=False)\n", + "\n", + "# Use r band only\n", + "df = ens.batch(periodogram, band_to_calc='r')\n", + "display(df)\n", + "\n", + "# Find RR Lyr with the most confient period\n", + "id = df.index[df['period_s_to_n_0'].argmax()]\n", + "period = df['period_0'].loc[id]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f79ad1eb83d0d125", + "metadata": { + "ExecuteTime": { + "end_time": "2023-09-20T13:17:00.655691Z", + "start_time": "2023-09-20T13:17:00.548017Z" + } + }, + "outputs": [], + "source": [ + "# Plot folded light curve\n", + "ts = ens.to_timeseries(id)\n", + "COLORS = {'u': 'blue', 'g': 'green', 'r': 'orange', 'i': 'red', 'z': 'purple'}\n", + "color = [COLORS[band] for band in ts.band]\n", + "plt.title(f'{id} P={period:.3f} d')\n", + "plt.gca().invert_yaxis()\n", + "plt.scatter(ts.time % period / period, ts.flux, c=color, s=7)\n", + "plt.xlim([0, 1])\n", + "plt.xlabel('Phase')\n", + "plt.ylabel('Magnitude')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf157e25e291651a", + "metadata": { + "ExecuteTime": { + "end_time": "2023-09-20T13:17:00.655819Z", + "start_time": "2023-09-20T13:17:00.647036Z" + } + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "3.10.11" + }, + "vscode": { + "interpreter": { + "hash": "83afbb17b435d9bf8b0d0042367da76f26510da1c5781f0ff6e6c518eab621ec" + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/gettingstarted/quickstart.ipynb b/docs/gettingstarted/quickstart.ipynb index b661ebe8..110442df 100644 --- a/docs/gettingstarted/quickstart.ipynb +++ b/docs/gettingstarted/quickstart.ipynb @@ -71,6 +71,7 @@ "metadata": {}, "outputs": [], "source": [ + "ens.calc_nobs() # calculates number of observations, produces \"nobs_total\" column \n", "ens = ens.query(\"nobs_total >= 95 & nobs_total <= 105\", \"object\")" ] }, diff --git a/docs/index.rst b/docs/index.rst index 10eb10b0..60c4d4dc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,5 +32,6 @@ API Reference section. Home page Getting Started Tutorials + Examples API Reference diff --git a/docs/requirements.txt b/docs/requirements.txt index 3ee39d08..a1b35287 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -7,4 +7,4 @@ jupytext jupyter matplotlib eztao -ray \ No newline at end of file +ray diff --git a/docs/tutorials/binning_slowly_changing_sources.ipynb b/docs/tutorials/binning_slowly_changing_sources.ipynb index c68fea34..853e62b8 100644 --- a/docs/tutorials/binning_slowly_changing_sources.ipynb +++ b/docs/tutorials/binning_slowly_changing_sources.ipynb @@ -60,9 +60,9 @@ "outputs": [], "source": [ "fig, ax = plt.subplots(1, 1)\n", - "_ = ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 500)\n", - "_ = ax.set_xlabel(\"Time (MJD)\")\n", - "_ = ax.set_ylabel(\"Source Count\")" + "ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 500)\n", + "ax.set_xlabel(\"Time (MJD)\")\n", + "ax.set_ylabel(\"Source Count\")" ] }, { @@ -90,9 +90,9 @@ "source": [ "ens.bin_sources(time_window=7.0, offset=0.0)\n", "fig, ax = plt.subplots(1, 1)\n", - "_ = ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 500)\n", - "_ = ax.set_xlabel(\"Time (MJD)\")\n", - "_ = ax.set_ylabel(\"Source Count\")" + "ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 500)\n", + "ax.set_xlabel(\"Time (MJD)\")\n", + "ax.set_ylabel(\"Source Count\")" ] }, { @@ -120,9 +120,9 @@ "source": [ "ens.bin_sources(time_window=28.0, offset=0.0, custom_aggr={\"midPointTai\": \"min\"})\n", "fig, ax = plt.subplots(1, 1)\n", - "_ = ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 500)\n", - "_ = ax.set_xlabel(\"Time (MJD)\")\n", - "_ = ax.set_ylabel(\"Source Count\")" + "ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 500)\n", + "ax.set_xlabel(\"Time (MJD)\")\n", + "ax.set_ylabel(\"Source Count\")" ] }, { @@ -150,9 +150,9 @@ "ens.from_source_dict(rows, column_mapper=cmap)\n", "\n", "fig, ax = plt.subplots(1, 1)\n", - "_ = ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 60)\n", - "_ = ax.set_xlabel(\"Time (MJD)\")\n", - "_ = ax.set_ylabel(\"Source Count\")" + "ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 60)\n", + "ax.set_xlabel(\"Time (MJD)\")\n", + "ax.set_ylabel(\"Source Count\")" ] }, { @@ -179,9 +179,9 @@ "ens.bin_sources(time_window=1.0, offset=0.0)\n", "\n", "fig, ax = plt.subplots(1, 1)\n", - "_ = ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 60)\n", - "_ = ax.set_xlabel(\"Time (MJD)\")\n", - "_ = ax.set_ylabel(\"Source Count\")" + "ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 60)\n", + "ax.set_xlabel(\"Time (MJD)\")\n", + "ax.set_ylabel(\"Source Count\")" ] }, { @@ -209,9 +209,9 @@ "ens.bin_sources(time_window=1.0, offset=0.5)\n", "\n", "fig, ax = plt.subplots(1, 1)\n", - "_ = ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 60)\n", - "_ = ax.set_xlabel(\"Time (MJD)\")\n", - "_ = ax.set_ylabel(\"Source Count\")" + "ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 60)\n", + "ax.set_xlabel(\"Time (MJD)\")\n", + "ax.set_ylabel(\"Source Count\")" ] }, { @@ -259,9 +259,9 @@ "ens.bin_sources(time_window=1.0, offset=0.5)\n", "\n", "fig, ax = plt.subplots(1, 1)\n", - "_ = ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 60)\n", - "_ = ax.set_xlabel(\"Time (MJD)\")\n", - "_ = ax.set_ylabel(\"Source Count\")" + "ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 60)\n", + "ax.set_xlabel(\"Time (MJD)\")\n", + "ax.set_ylabel(\"Source Count\")" ] }, { @@ -290,7 +290,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.6" }, "vscode": { "interpreter": { diff --git a/docs/tutorials/structure_function_showcase.ipynb b/docs/tutorials/structure_function_showcase.ipynb index 4090914d..592436fe 100644 --- a/docs/tutorials/structure_function_showcase.ipynb +++ b/docs/tutorials/structure_function_showcase.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -53,30 +53,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Text(0, 0.5, 'magnitude [unit]')" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from eztao.carma import DRW_term\n", "from eztao.ts import gpSimRand\n", @@ -134,20 +113,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAHJCAYAAABpOFaGAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy88F64QAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOydd5wcdf3/n5+Z7Xt3e70lufReCQSSACEhQkQp0kQQFAVFxYJiRSWgKIhfBRuCgoAiwhcEhS9dIPQSSALp/ZJLrre9u72tM5/fH7M7u3u7d7lACv74PB+P6O7MZz7zmdnl5rXvKqSUEoVCoVAoFIoPIdrhXoBCoVAoFArF4UIJIYVCoVAoFB9alBBSKBQKhULxoUUJIYVCoVAoFB9alBBSKBQKhULxoUUJIYVCoVAoFB9alBBSKBQKhULxoUUJIYVCoVAoFB9alBBSKBQKhULxoUUJIYXiA84bb7zBmWeeSV1dHW63m6qqKhYsWMCVV16ZNe6WW27hrrvuOjyLTHLvvfdy8803H9Y1DMY111yDECLvv9///veHdW2DfXb19fUIIQ755/rGG29wzTXX8M9//nPQMYZh8Otf/5qPfvSjjBw5Ep/Px9SpU/n+979Pd3f3oVusQvE+EarFhkLxweWxxx7j9NNPZ/HixXzhC1+gpqaGpqYm3nrrLe677z727Nljj50xYwbl5eWsWLHisK331FNPZd26ddTX1x+2NQzGNddcw7XXXsuTTz5JIBDI2jd27FiqqqoO08oG/+yi0SirV69m/PjxVFRUHJK1vPbaayxbtgzDMAiHw/zpT3/i0ksvzRnX19dHbW0t559/PieddBLl5eWsWrWK6667jpqaGt566y28Xu8hWbNC8X5wHO4FKBSKwbnxxhsZO3YsTz31FA5H+j/XT33qU9x4443ved54PI4QImvODzL9/f34fL4DMteRRx5JeXn5AZnrYON2u5k/f/4hO9/KlSv56Ec/yqxZs3j44Ye56qqr+OIXv4jD4eDiiy/OGuv1etm5cydlZWX2tsWLF1NXV8e5557LP//5Ty688MJDtnaF4r2iXGMKxQeYjo4OysvL8woWTUv/5ztmzBjWr1/PCy+8YLt7xowZA8CKFSsQQvC3v/2NK6+8khEjRuB2u9m2bZvtLhrIXXfdhRAix7Jz7733smDBAgoKCigoKGDOnDnccccdgPUQfOyxx9i1a1eW2ylzDQMtHvlcPxdffDEFBQWsXbuWk08+mcLCQpYuXQpALBbjuuuuY8qUKbjdbioqKvjc5z5HW1vb/t7aHIZyQwkhuOaaa+z3qfu2fv16zj//fAKBAFVVVXz+858nGAxmHWuaJr/73e+YM2cOXq+X4uJi5s+fzyOPPAIM/dkNtqaXX36ZpUuXUlhYiM/nY+HChTz22GNZY1Kf4fPPP8+Xv/xlysvLKSsr46yzzqKxsTHnGt9++21OPvlkli5dyn/+8x8qKir485//zNVXX80ll1zCPffckzVe1/UsEZTi6KOPBqChoSHvfVYoPmj8d/wcVCg+pCxYsIDbb7+dr3/963z6059m7ty5OJ3OnHEPP/ww55xzDoFAgFtuuQWwrAmZ/OAHP2DBggXceuutaJpGZWXlfq3l6quv5qc//SlnnXUWV155JYFAgHXr1rFr1y7AinP54he/yPbt23n44Yff4xVbxGIxTj/9dC677DK+//3vk0gkME2TM844g5deeonvfve7LFy4kF27drF8+XIWL148bFeMYRgkEgn7vRACXdff0zrPPvtszjvvPC655BLWrl3LD37wAwD+8pe/2GMuvvhi7rnnHi655BJ+8pOf4HK5WLVqlS0yh/PZZfLCCy9w0kknMWvWLO644w7cbje33HILp512Gv/4xz8477zzssZfeumlfPzjH+fee++loaGB73znO1x44YU899xz9pg1a9Zw8sknc8EFF/C73/0uS2Rfc801jBw5kksvvRRd1zn//POHvCepeadPnz6MO6hQfACQCoXiA0t7e7s87rjjJCAB6XQ65cKFC+X1118ve3t7s8ZOnz5dnnDCCTlzPP/88xKQixYtytm3fPlyme/PwJ133ikBuXPnTimllDt27JC6rstPf/rTQ6734x//uBw9evSga3j++eeztu/cuVMC8s4777S3ffazn5WA/Mtf/pI19h//+IcE5D//+c+s7StXrpSAvOWWW4ZcW+paB/4bMWLEoGtJAcjly5fnzHXjjTdmjfvKV74iPR6PNE1TSinliy++KAH5wx/+cMi1DfbZ5VvT/PnzZWVlZdbnn0gk5IwZM+TIkSPtc6c+w6985StZc954440SkE1NTUOu6b2wZ88eWVVVJY866ihpGMYBn1+hOBgo15hC8QGmrKyMl156iZUrV3LDDTdwxhlnsGXLFn7wgx8wc+ZM2tvbhz3X2Wef/Z7X8cwzz2AYBpdffvl7nmN/Gbje//u//6O4uJjTTjuNRCJh/5szZw7V1dXDDhL/z3/+w8qVK+1/jz/++Hte4+mnn571ftasWUQiEVpbWwF44oknAA7YfQuFQrzxxhucc845FBQU2Nt1Xeeiiy5iz549bN68eZ9rBGxL3oGis7OTj33sY0gpuf/++7OsSgrFBxnlGlMo/gs46qijOOqoowAr0Pl73/seN910EzfeeOOwg6Zramre8/lTMTgjR458z3PsDz6fj6KioqxtLS0tdHd343K58h4zXFE4e/bsAxYsPTBGJuXSCofDgHXfdF2nurr6gJyvq6sLKWXez7K2thaw4sr2Z40Hal0nnXQSe/fu5bnnnmPcuHEHbG6F4mCjhJBC8V+G0+lk+fLl3HTTTaxbt27Yx+ULivZ4PICVpp0ZlzJQVKRSt/fs2cOoUaP2e82Z58lkMPGSb62pYN8nn3wy7zGFhYX7va7hrHGgsNgfKioqMAyD5ubm9yVEU5SUlKBpGk1NTTn7UgHQhzojrquri4985CPs3LmTZ5991rY4KRT/LSjbpULxASbfAw9g48aNQNoKANYv/f39lZ/KTnr33Xeztj/66KNZ708++WR0XeePf/zjkPMNtobBzpPKnBoOp556Kh0dHRiGYVvIMv9Nnjx52HPlo6qqCo/Hk7PGf//73+95zlNOOQXgPd+3gfj9fo455hgeeuihrPGmaXLPPfcwcuRIJk2a9J7Xu7+kRNCOHTt4+umnOeKIIw7ZuRWKA4WyCCkUH2CWLVvGyJEjOe2005gyZQqmabJmzRp+9atfUVBQwDe+8Q177MyZM7nvvvu4//77GTduHB6Ph5kzZw45/8c+9jFKS0vtjCaHw8Fdd92Vk/o8ZswYrrrqKn76058SDoftlPENGzbQ3t7Otddea6/hoYce4o9//CNHHnkkmqZx1FFHUV1dzUc+8hGuv/56SkpKGD16NM8++ywPPfTQsO/Fpz71Kf7+97/zsY99jG984xscffTROJ1O9uzZw/PPP88ZZ5zBmWeeuR93NxshBBdeeCF/+ctfGD9+PLNnz+bNN9/k3nvvfc9zHn/88Vx00UVcd911tLS0cOqpp+J2u1m9ejU+n4+vfe1rwP59dtdffz0nnXQSS5Ys4dvf/jYul4tbbrmFdevW8Y9//COvNe1gEA6HWbZsGatXr+bmm28mkUjw+uuv2/srKioYP378IVmLQvG+ONzR2gqFYnDuv/9+ecEFF8iJEyfKgoIC6XQ6ZV1dnbzooovkhg0bssbW19fLk08+WRYWFkrAzt5KZWw98MADec/x5ptvyoULF0q/3y9HjBghly9fLm+//fasrLEUf/3rX+W8efOkx+ORBQUF8ogjjsjKaOrs7JTnnHOOLC4ulkKIrIy0pqYmec4558jS0lIZCATkhRdeKN966628WWN+vz/vWuPxuPyf//kfOXv2bHsNU6ZMkZdddpncunXrkPcylenV1tY26JhgMCgvvfRSWVVVJf1+vzzttNNkfX39oFljA+camG0npZSGYcibbrpJzpgxQ7pcLhkIBOSCBQvko48+ao8Z7LMbLJPtpZdekieeeKL0+/3S6/XK+fPnZ82XuZaVK1dmbR8sg29/Sa1tsH+f/exn39f8CsWhQrXYUCgUCoVC8aFFxQgpFAqFQqH40KKEkEKhUCgUig8tSggpFAqFQqH40KKEkEKhUCgUig8tSggpFAqFQqH40KKEkEKhUCgUig8tqqDiPjBNk8bGRgoLCw9ZoTKFQqFQKBTvDyklvb291NbWDtkEWAmhfdDY2PieeispFAqFQqE4/DQ0NAzZMFoJoX2QauTY0NCQ0w1boVAoFArFB5Oenh5GjRq1z4bMSgjtg5Q7rKioSAkhhUKhUCj+y9hXWIsKllYoFAqFQvGhRQkhhUKhUCgUH1qUEFIoFAqFQvGhRQkhhUKhUCgUH1qUEFIoFAqFQvGhRQkhhUKhUCgUH1qUEFIoFAqFQvGhRQkhhUKhUCgUH1qUEFIoFAqFQvGhRQkhhUKhUCgUH1qUEBqEP/zhD0ybNo158+Yd7qUoFAqFQvH/JX1dEfZs7qKvK3LY1iCklPKwnf2/gJ6eHgKBAMFgUPUaUygUCoXiALHhlUZW3LMJKUEIWHzhFKYdW3vA5h/u81tZhBQKhUKhUBxS+roiPJ8UQQBSwoq/bzosliElhBQKhUKhUBxSulvDMMAfJU0ItoYP+VqUEFIoFAqFQnFIKa705mwTGgTybD/YKCGkUCgUCoXikFJQ4mHU1BL7vdBg8aenUFDiOeRrcRzyMyoUCoVCofjQU1jmBbqYsqCaY04fd1hEECiLkEKhUCgUisNALJIAoHxU4WETQaCEkEKhUCgUisNALGwA4PIcXueUEkIKhUKhUCgOCPHmZkKvv0G8uXnfY5MWIZdXP9jLGhIlhBQKhUKhULxvuh98kG0nLmX3xRez7cSldD/44JDjo+GkEFIWIYVCoVAoFP/NxJubabp6OZimtcE0afrx1UNahlIxQkoIKRQKhUKh+K8mVr8rLYJSSEnnX/826DHxSDJGSLnGFAqFQqFQ/DfjGjM67/bOu+7KaxWSUhJLuca8yiKkUCgUCoXivxhndTUFS5fm7jBNYrt252xOxEy7z5hyjSkUCoVCofivx3f0vNyNmoZrdF3O5lR8kBDgcB1eKaKEkEKhUCgUiveNECJ7g6ZR85NrcVZX54zNdIvlHHeIUUJIoVAoFArF+8YI9mS9r16+nOJzzsk79oNSTBGUEFIoFAqFQnEAMHqyhVDzNdcMWkso9gEppghKCCkUCoVCoTgAJFoGZIdJSdPVy7OyxpqCYV7d3k5LZ39qCH1dkUO5zBwOv01KoVAoFArFfz3xltbcjcmsMWd1Nfev3M0PHlqLKeGj/U5m4qCzMcRfr3qVxRdOYdqxtYd+0SiLkEKhUCgUigOBYeRuS2aNNQXDtggqMGFGLO0SkxJW/H3TYbMMKSGkUCgUCoXifWNGwtkbhLCzxna2hzCTdYNKDA1BdqaYNCHYOuD4Q4QSQgqFQqFQKN43ZjJrzJFMl6+44go7a2xsuZ9UlnyXbiKRWccKDQKV3kO32AyUEBqEP/zhD0ybNo158/IUiFIoFAqF4v9zejva2b3uXXo72oc1PpU15qyqAkDzuO19NQEvX1o0DoA+Dbq0tBASGiz+9BQKSjwHaun7hQqWHoTLL7+cyy+/nJ6eHgKBwOFejkKhUCgUh4y1zz3NM3/6HVJKhBCc9MWvMfPEkwcdb0ajyGgUAL2i3NoWi2WNOX5iBX98YQcAVX438d44iz41ibGzyw+bCAJlEVIoFAqF4r+OeHMzodffyNvQ9P3S29FuiyCwGqQ+8+ffD2kZMoJB64UQOErLrOMGCKGoYXWnFxLivXEAqsYWHVYRBEoIKRQKhULxX0X3gw+y7cSl7L74YraduHTQooXvla6mRlsEpZCmSXdz46DHmEm3mFZUhHBbLjEZzRZCsYQlhI6Kpp1RD97wFhteGXzeQ4ESQgqFQqFQHEKaQ8282fQmzaH9t+bEm5tpuno5mJaowDRziha+X0pqanP6fwlNo7h68Do/qfggvagI4XICGRah4F7Y+SJabyMFJpwQSQuhw506D0oIKRQKhUJxyHho60Mse3AZlzx9Ccv+uYyHtj60X8fH6nelRVCKZNHCA0VhWTknffFrWdtO+sJXKSwrH/SYlGtMLypCS1mEYjFY9Ve4eQbcfRofeXIpZ/LOByp1HpQQUigUCoXikNAcauba167FxBIypjS59rVr98sy5BozGrQBj+5k0cIDSWZgdOWYcdQdeQw7d+4kmIoFGkDKNaYHihAuFwCx+m10/eOn7IlMo88oQ2BypeuOD1TqPKisMYVCoVAoDgm7e3ZjymxrjilNGnobqPZXD2sOZ3U1ld/7Lq3X32Bt0DS7aOHBos/t4+abb7YzyE477TTmzp2bNcbo6bWWUxQgsnETAFt2Otns/CMIDYHJ4qJbmOZ7lk5HkLJEMXD4U+dBWYQUCoVCodhv3kvWVl1RHZrIfuxqQmNU4aj9OnfhiSfar6uvvtouWghWA9M9m7sOWMyN6XDSItxZGWSPPvpojmXI6LHeC4eD3qefJuIuZvOk8y2lA0g0VvR8me5EOe2aZYOZPL+Kz/xs4WHrMZZCWYQUCoVCodgPuh98MB2wnLTIZIqRwaj2V7N8wXKWv7ocAIFg+YLlw7YGpTBDIfu1cKYf4xteaWTFPZuQEoTggDQyNV0eGBA4LaWks7Mzq8ZeyjWGNEFKwt6K3OPQ+VXsEoS03GBVYwKHPXUelEVIoVAoFIph836zts4YfwYApT2Sr8rFnFa4cL/XkCmEjF7LJdXXFbFFEBy4bCwtFoEBqfRCCEpLS4F0BlxfRwsAjhEjQAi84bZ8K+dhZuNL3jpvoet9re1AoYSQQqFQKBTDJLpz5/vK2oqZMZa8Y3LLLQbH3/DMe6oDlCmEzD7rdXdreKBeOSDZWFoiTpFId5VPxQgFAgHu23QfJz94Mpc8fQmvbn4aAEdFHe4zLsGbbQxCYFBXt5o+DfymtbM7nF1n6HChhJBCoVAoFMMg3txM1z/uy92xH1lb4cY9XPaEid1q6z3UAcoWQn0AFFd6B3qiDlg2ljeZ5VVUVMQVV1zB3LlzaQ4187M3fmZngPkiEufoY+lfX4mLo/Evu4E6Z3pBn6m4jPLircyM6hRLa/tr92zm7nvWve/1vV+UEFIoFAqFYh90P/gg25acSN/TT2fvEGK/srbCO3egDbDc7G8doEwhFG+1XFIFJR4WXzgla9yBy8ayhIsQwo4L2t2Tvd5isxj3nIvsGkFCaMz26XiSWqhA76A/5ODksNMeoyHoebmF7bu6D8Aa3ztKCCkUCoVCMQR2XNBA3xNQfO65wwqUTmGMqMQcYLnZ3zpAoVdftV/3Pv6E7VrLDIyeccKI/Q6UHizjLGnAwTDSLrK6ouz1BvQqRE5GnMCvpy+2P+xFG1BMUUOwfUf+2kSHCpU1plAoFArFEOSt5pxEOPT9mitRHuDeEwQXrkiLqv2xKMWbm+l54smsbU0/+jHuSZPxzpppb3O69m9dG15p5Pl7NkEy4+yET0/OGZMphAZmuhV46uw6QylMKQkZ6ev0u7owkVliyEQyflyAw4myCCkUCsUhJBgMDlmhV/HBwzVmdE4qeEoW7WzdvF9zxcwYLSXpuYTPR+DMM7MHJXtzEdybe3z9rryWqfrzzqPrgXTQtZlnzGCkMs7IyjjbYO+XSeGSKYQyOWN1If6pZ2eJICklm0JhIhnLGC038oY7kV4jkqLjqhg/unjYaz0YKCGkUCgUh4hVq1Zx8803c/fdd3PzzTezatWqw72kDwXvpfhhJs7qavzHHWe/NwS8OtV66G/Yu2a/WmTE//0E33w4bV2S/f1Et2xJD8jozcXNM6z3GYTXDRJcLCV7r70u/dYcvhDKn3GWscakEEokEgyktEdy4duVOW4xIQS98eyssHHRDYQc3QB0CZNpl07lsxfOGPY6DxZKCCkUCsUhIBgM8uijj+6zQq/iwNL94INsO3Epuy+++D2lqqfQS4oBeGaO4PKv6KwfbYkDZ0LS0NswrDnizc3oN/4558H7xgN/sF4E98Kj37Dy3sH6/0evsC1D8eZm2n71q0HnNzNmlvk9eXkpzpdZJtOiJxUjZJomjd39WcNquiT0tiAHnFBKSX88nrVNACOEVXixT5NUVvuHv8iDiBJCCoVCcQjo7Oy0RVCKVIVexcHh/RY/zCTR0grAplEanUWCWDLC1p1g2C0yYvW7EHlcVm0vP2dZlTq35yoYaUDnDvv4fG6xFIYjXaDQSOyHEspL+vhQNC2KFv3iWe5fuZuHtj4EQFOJwIh2E13zt6zvt9G+mZiRFkJSCiTQbZQAEBVg7If77mCihJBCoVAcAkpLS7NiKCC7Qq/i/TPQBZY3yHk/U9VTJFqsNPXj51iVoWNOa/sET92wW2RoPh/5Hv0jWiUNPbuhdDwMyKpC6FA6DkjGKg2CBP6+OC2E4tH88Tz56M5bdDF9fE84w7IjJT985GWuefUaADqLBA8cJ4jveiVLxMlwJ5pMzxGXbnbpY4jIQgBiQmLsh/vuYKKEkEKhUBwCAoEA48aNy9qWqtD738b7jbk5GORzgeULct7fVHWwLHfxVssiNH3qIgDbIlQqhu/eMfv7B8ocAIojUPnyZuJhHaZ9Ir1D6HDazRAYAVixSnpJSXo+YEuN9frNifDGFKe9LxEbvhAq8uiUO4Rd8wfANHrSy8jYrmOiF7+ML1pEbXAC/miAtydYUkJoGZlqDk+W9Soh3XSKEtzJTVEBLbE4L3f10hg5vBWmVfq8QqFQHCIy44EqKyuZO3fuIT1/vLmZWP0uXGNGDztdeyDvteHowST87lqafnx1+sGbdIFNeO5Zij72MXoee8zanlzv/l672deH7LdiY/qTBQpTFiEzGgWGd29dY0Yjybb5pN73//jnbNNuoObChRSndl78GIxekDWHcDqz3m+vEUxqklT0glOmLUKR1k7izc37vNaeF/fQ8/hOji1wIKVkTdhgR++7JPqfscc4Miw7DkeQqUacE1Zdg4aGicmm0vtAvJ69Tt2NNNK2lo5QDS5nP65kwNGOMW4u2dOI3GNd/68mj+KC2rIh13qwUBYhhUKhOAR0dXXR3t5uvw8GgzkxQweTAxE0fCBjbobLvsoNdD3wAPWf/GRu7EzSBeaZmq62PPKWW96TaEskr08LBAjr1rVHHdYDXUYiw763RrGkZ0kJRrG11hybjWnS9LdXiPcnH81atq0i3txMImmZAusBvmy1NVdtB3xx6mX2vtCmbfv8nLufqqfn8Z32eyEEc7w6zmi2qHGSjhEq0ns4Ycd5aEn5oKExtfNTRDzZLl7hcBN3pq1lj/T/jI7QLNxAj1ewYU6R7SaUwHc2Nxw2y5ASQgqFQnEI2JJMkR4xwnJzRKNR+vv7hzrkgHGgBMx7ibl5P260fZUbiDc303z18vwHJ11gRkY7ChLx/GP3QTwZKO2srKQ/YX1mKYuQEQoN697u2vVnXnn1eELnttByXZzNl5Zw57nFua4yKYn1JgVQf0fWrs6//nXgaDRppfN74nB0OB20HXMVEnEWDfo5977QQN/zudluQgjK3LUDN9ovA5FSWwTZu9GI+CqzL8PpI+HIdBtqrAt+Cr8p6CzUc1yWBrAzHM1Zz6FACSGFQqE4BGzduhWAqVOn2nFBHR0dQx1ywDhQQcN5g3WHiLl5P1ao4ZQbGCqLqvTizwLp7uwAoTfemyBLBUo7qqoIJ6zA4lSMkIxE9nlvd+36M9u234BdsVCDgiNaMKZVkJPbJcBVmLTAdGy1N8ebm+m8866ctRkCWpNhZv0b0/WIIt4KXp3/Uxorj8n5nBPBKMEn6vd12TYyo0ZQLy670aq9HwNvtCt7m9ObG5+Fhs+E0l4j53PTgbFe97DXdCBRQkihUCgOMrFYjJ07LRfEpEmT7EyxQ5U6f6CChnPiTYaIuclrhfrx1YTfXTuscw2n3MBAYRZxF9NVPJGIu5jOv9zJthOXElm/3t7f9bd72LbkRFpuvHG/BFGiNSWEKumPJy1CKSEUjw95byORJrZt/0XOnELALrGdZ45IH2sI8C3ScPqS9+zpH9sFFQcTff93tGBPhTVHqH+ArBIamyafT6y4Jvt62vNliSWvR0qCYuyA60kHQUc0k3dcacuaiUl9wX144qGsQ3TdjScnR85EICgKS2reTQtaDfjl5FHUelwcDpQQUigUioPMzp07MQyDQCBARUWFLYQOlUXIWV1N0akfT28YomP6cF1ZWlkpE557dtCYm7xWKCmpP++8YVmGhlNuwFldjWus9dBurF7Aq/N/yuo5V1iWkOoFYJqEB1bvltIWScO1UMWTFiFnhkUomopZNgz8CzICmgeIw/5wPeRJmpcSoqbk7YnWNTaWwOVf0Wkd25Y5yi6oGHr9tdw5gNemCBqTMcZ9jXk+M6HTT0HWJke5NydLP5Maf3YXe7S0VNCR7HWmhdBjk66h2/l6llgC0Fw+Tg44qXOlTmQyveheUrfNtydMeXLeP00fc9gCpUEJIYVCoTjopOKDJk2aZMVglFl/9A9lMUVHWbn92n/ccbaAyRQ+3Q8+yLYlJ1qurDyWEzMj3sY9ctSQGUmaz5fHNQJIOaz4pEAgwGmnnWa/F0JklRuIRJro7HqNmKOXiLuYTZMvgJQLJ2kJibiLBz9B0kI1HMtQojllEapOxwhlxDHr5emH+Lgnn8wShz7vGPKpDiHAowmizmTQtYDuQhgVH9DGQhrEN79Nx623AVbAdXSSiVFsNb7wxGFvmTWH1pjb90xoEBhQOdoRcFNy1kTbxZVpeRNCMNvnwqsXppeQIWg1IZnVvsN+/z9/7mT2Dmml+udco2C2V8cj4ISiWxnjeRl3MmssJiTx5GnHeA+PJSiFEkIKhUJxEJFS2vFBEydOBDjkrjGA2O50nEh8zx5gQAzPkhOzU9DzWE5ie9NNQIV78HiOrgcepP688wavgjzM+KTZs2fbr0844QS73EBj4//yyqvHs3r1hTR+o5He4xxpEWQvUCdYNMDFMxAp6fzr3/a5jniGayxlEYpnCCGjMx0f4wgUZR3r8dQwYfz3cmxCUsJFs6+0LUvuBCxv76R6QGPTeNhB6wPPAxBaaNByXZyOKxK0XBcntNCguUSwtzQppsK5IuroyX0UJFP+M3GNgv4+K4txoOVNE4ICZ3F6Q8b+0SLMSbvfsd+3F5dREplKW0n+wqCaEPh1gVNEcQgDV0YdoUjy+1HgyBVRhxIlhBQKheIg0tLSQk9PDw6Hg7FJN07KItTR0XHIUujjDWnhEdu1i+ju3dkxPFLmFy4ZWVDxPWkhJDPq52S60sLvrqX5xz8eshXEcOOTotF0FpGWdKNEIk1s3HQVmYHH8TMbcXhyReX6aZ+nafTiIc/Reeed+4xbSrXXcFZV2TFCCEE8mUIfzxCIZiQ388ldfhpZHS8krGp2cFTVUfj8VoHEUunlrFB27E73dh/b/l1Jz6NPYhRLghcY6ae2Bt0XGBgl0naNJRLZ4nT+G9fiv+0Hea1eXet3EvVYBw78DppS0hfvTm/IEEKlkaDdhHX1WBcXXPcbvn3Fjznvqqv51whnTiC1KSUhQ2JIJ7qUFApBuUNQAESTY/364ZUiSggpFArFQSTlFhs7dizOZDG84uJiwAqiDoVCgx16wJCmSWx3MlVa08A06X/99dwYnsFIWnAiGzakN8VjOVlhjd//gWUJGgohqLzyW8MqapgphCKRCDBIzI0G3vId5CA0No45i2DhEKJLSuo/+Uk67rgj/+5YDCMZy5WZNQYQS7q1YjvTtXiC/3o4Z46mf/wN54BbfWRNgi1rzmJmMo5ZROJw6s32/ni/RtPKdNXxRKXMeWILDSockpBXINwm5oC6Q654yP7strb08q93m9ja0gtA2FeJlhQ0O6IZrTGkZE1fJ2GjN70tw9q2R3gxhU6PV/DYPD9mUqCamsbPp7vZ5dGyXG7vhA0iEvra/BjGiZxc5ODYAge3O9JxS35dWYQUCoXi/1tSbrFJkybZ25xOpx3rcijcY4nWVsuC43DgnTULSFp08sXwDELXvffS/off2+/jzS05WWHBf/1rUEuQXlFhvZCS1l/9eliByvmE0GAxN7UL/kRg7Eu5kwidt+d+h63jPjFkzFDLL/8nrxhKtFnBy8LpRC8psWOEAOJ6ngaqv/ltlgUm3tyM+6Y7s/s42MuXzCtptOJ94gnkjE/aQ6xaQunrNJ0yR/9ZAdfJNxMmYGrZVafDnjK6Sibym63tLFq/jS91tHD8+m0sf2ErZVNGoCenb4pLYsm+X6/2JdgVG3CijO+JR/fz4uiZdBbqyAHfH1MIdhRodCcrUb8WSrA7OVdT35HIxEW2Gy7qTBVkBK82/O/hwUAJIYVCoThI9Pf3sycZj5OKD0qR6R472KTig5y1tbiTlZYTrW145x2VHiQEruQaPZnbk/Q+9VTWg9js7By+RQkw2jKyoYZZ0DElfiAtijyeGiorT8kZK4Sk+sh7cHjzCEuh0VB3UjqbLA8CaP3Vr3LWFM+oISSESLvGADNfC1XTpOfJp7Iav0p98PukCehbbAkHM5SRUu4wSd3w0EKDri8buf1YBbiTT3GzpiRHCK086vu8MP+b/K62Ji1ahOA2o48/t3Tg0szkdUBK+zg1AQOrG2UInmNiOtXuaZT2GogBoleTkup+k0QyzsmQ6eMilTMRGZal/qQRyC9ETozSoUYJIYVCoThIbNu2DSkllZWVtjssxaEMmI4nhZCrrg73hAkARLdts+N8AIovuMB+zhYuOuGgr2k4AdP5LEKQsgrlIjQTV0Fb3n3JAUNnk5kya02RSBOdrS9hFEscVVUAWa6x0CDx4q033JDV+DVRMfiSAEIfsbLAZF/aHWUmNEDkxgZlL5e2hPWpGeVuYs4BDWCFltdygxD8sqmNvVXbiLs7qXSAPzn/PJ/O6GQgvCmkPT7jrAgE26ud2TJQSq5aHyUQNQknBY+ecVifKZAZ3en7k/FV/sMcKA1KCCkUCsVBIxUfNNAaBBzSWkKp+CBXXR3u5FoimzYR3ZROtza7Om0rhnfmjKzaMYPhGjcu/UbTKFy2bPiLGkbAdKYQynwdT3RbLwa6ikyNWF9+1eHwduKr2ITDF2TPiMX7XJOVmbaIza7fWhlaR1kCKFMIhYcqhJy0ejV1PUr7VYkhBgIaJCokZl+3vcmqLi3zxgYBICWvtgqCycame8Q4mmqOzRmWr4ozWDafd+Y8wo5FV1I9/hXbKiOEYI6/CK9eSCLp+ssUUlJIOz5ooGt1QXuCsGHSk3R1OTJ2R0yTsHzWfh9K6p9CpxJCCoVC8f8lhmGwbds2IDs+KMWhrCVku8bqRtlCKNHUlGURimzYiNlrWSQ8M2ZS85Nrs8RQ0emnZ0+q68R2pAOUC5YspuDEE61dVVVUfufb6eM1jaLT0zWBhtsFfjCLUKzXEmyuVe6MbH9B89sXkgjnpnEHxr7E+FO/T92SXzH+1O8TXOTOsQrJjCKTVmbaD7FdRBo0z3qHSKQpK0Yo5hjapWMUGWxt+gWI/HFTNiY42gRmW729yekz8ZbFcLSKHE8VQKA7xrqwDwB/NMCOpmPyxnwVhSVL1/TniCFNGlTRBELSMv1u4u709zCVPm9oeSxCwhzUynRfndWetS+5KzMsaqT7XUzxLiEiNGqdrNKtz/ZwB0rDh0QInXnmmZSUlHDOe+g6rFAoFO+FPXv2EIlE8Hg8jBw5Mmd/pmvsYKfQx3bvAsBVNxpHSQl6ebq4ol5ipW/H6usB0AoL0Qv8FJ9zDuP/8wzCb7lbis86K3vSAfVu+p59jqbvf9/a1dKCHggw4blnqbv7biY89ywjbrwRrdAq0ld3x+3D6gI/qBDqs9LZfWsTGHusZp+ta84huPP4nDkc3k6qj/obIilGhJBUH/V3+quKs8b5rvm+vSYrM21gnIwkHN5FOJ62CKWyozJbe2SSqJTkqyqdiZSgPayjdwvkg5dbG3UP3dt9hDtc6N2CwL25YkH3lRN1WYUSR3ZWMNTjfOGWKOXB9OclpMEl3EYZSfEjTOK+Fnt/Kn0+ZRHKFEISc1Ar09/Humh1Qzh5zY6M44odTbxpzuE+9ys87lrNdu924PCnzsOHRAh9/etf5695uvYqFArFwSKVLTZhwgT0PL96S0pKEEIQi8Xo6+s7aOuQUhJPucaSbh/3xAn2/sKPfCRrfKaVxlVbi/8oK3B6++2/Hc7J7JdNya7w/mOOtud0VlfljBuKwVxjMT0GQCJWgN5p3VtPvD1v2whXQastglIIzcRZ0Jq1LXzNDXYmW97MNKnh8dTZrrGPr3czY/cgrT2SDGbNyUTrThB/x7oGM2GtMx7zJlPnk3E0r+Z+fwxNEDUtceMLt4Ic+kQlGX3IvmP+jMWk3VTS1HCELEFppc93EDZ6MYXEEDK7WKWQFIUl03fFcs5hCkGjT6c/KYQyY4SC0s8On1VBGyDusOxFjtg+3IaHgA+FEFqyZAmFhYX7HqhQKBQHiMy2GvlwOByHJIXe6OrC7OsDIXAmLVPujJgl14TxOCrScTWOmmx3VWPUCj52vmz17DKG2w4hTzB06jyJtrZh9TTLtALF43EMw2DDK40Eu624qnXjvkS/UQtA0UjJkgun5OiXWF8lUg7YaIKvcUD9pozWHx5PDSUlx2SNH+f7IrqrnIRMUNojuejRENF9tPbQuwX+F4Z+zDqRdnVpMxn4HOvLTp0fWKQQICFjRJIqa1d5kMlb7h1SYJoZt6DsmXjaUCUFLW9fSGOPteadUZP6SI91jCYxByxfJs85pjXOQDQpKes3SNnMMj2HPfiyPpu4bgkhEc4tQHmoOexC6MUXX+S0006jtrYWIQT/+te/csbccsstjB07Fo/Hw5FHHslLL+WpFaFQKBQfELq7u2ltbUUIwYQJEwYddygyx2K7LLeYo7oaLZkNZATTadqtN/wC4fPZ753V6U7ljTvWUvB6uogigBbOtQTkJU8wdEoI9Tz9TLqn2WDNT4N7iXY0ZG3qfPt5VtyzCd1liRgjVkCfGA+Aq1ow7dhajr6yhL1FW+1jEuFSWteca783JQTutVxROZgm/avXAODxjLI3l9yqM2LUBbY1qKZLokkIeyvytvYIe9PC0rln6DgiXRO2EJKGNdZVXpAt6Jy5x4UTMSKmgTfhRXdWEh3zOnoyfkmP5RbpzIzpibSV2fOXbj+N7h3HkpI1Zsb/SgHmwBo/Seva7ooBi0pmjRVFpe0aK9DAkzzcZ5pZXsKUECorzO6Ddjg47EIoFAoxe/Zsfv/73+fdf//993PFFVfwwx/+kNWrV3P88cdzyimnsDujb86RRx7JjBkzcv41NjYeqstQKBQKm5RbbOTIkfgyRMZADkUtoXhDOmMMrAJ/PY88mh4gpZ1eD+DMsAg1bVqV85AQpN0bgzJIMHRKCPU9+2zaepGvptDbd8NN04nueDnr+LaHb0CSQHdaliIjVkAiYlnVhKMbAL1Q0uXNtjL1Nc6xX7+7chS+PK6mFI1XXkn3gw9iGulYIDMAzsoKu4ZQe5kLU4BmRO3rsLPSPJ30ZFSyNrIbv+fiK7Abr0YNa13OsgJqrroSu47QcUbOYVKD6t46Tmk4hUXNi1hZdw59RZYlxxvN/T4JX7f9uuuCDDErdUbuWYGZvA5rBdb5/BGZI4SkMOnxCtaOybUMLmhPkEAyMSmrHIUOSse4KSjSCCXqKO1NWyJTQqgy4M+Z51Dj2PeQg8spp5zCKafkFsdK8etf/5pLLrmESy+9FICbb76Zp556ij/+8Y9cf/31ALz99tsHbD3RaDTLF93T03PA5lYoFB8OBjZZHYwDYRHq7Winq6mRkppaCjM6zKcIr7csOo5kh/RY/a5cF0pm93FvWrjVTJlLlwAtY7gEojp4hgjtGHPffXhnzczZnumCyyLpRnNWV0NwLzz6DUASJTs/3e3otq1BAGbch5EUQmjWPTSkQULLtlppjvTf9GPWJfKFEmWtpenq5STuSheVNMa7ES4X4X5LHEXK/Lxx9tmE2+aDEATGvmQHZEspaK6+kKp/FOOJdpMoHzp2x8Swe5aFymdSweuguym+6FKafnEzRkGCnrNz55C6ydG9k4mnrkZo9BVtxRUpxh0Lkhl15vB24iyMAtb9j2jpJqyGHqWqbQMunwvGL6Gy4x1wWt+Bym5Jc1nuijsL9dwMNSFo8GloUTgGP/8a4eTn092YQqBJyRlvOVhQH6O/cBcRESOWFELuljAMbjQ9JBx2i9BQxGIx3n77bU4++eSs7SeffDKvvvrqQTnn9ddfTyAQsP+NGjVq3wcpFApFkng8zo5kWvlg8UEp3q9FaO1zT/Pnyz/HAz+9ij9f/jnWPvd01v7uBx+k6+67Aeh5/Am7wN9QNYJaf/EL21VVO24mu770sax433fGZHde1+fOznooOkaOzCuCYAghlOlG69xOyhISJdvqoOlhCj1WpW4jZkXeGnFLCBmmJYTivc2U9tdkHScyhJAcTt0a0yTR322/jY80iTc305/oJ6CbzNR8xEJTQGhDZqWFFhqEjxs6MDxBHDPpZYqEk/LF4bEyCRPG4HWEnHDMMQ8zYuS6jAsFjU40Izt+J1zWTR9p01SUTCEUw5HoxxWzfvQ7pEnKIoTQc6o+SyHzZ41Jyah+EzeCVrdmiyCwgqj/daSfiF+zRWjKIsTadhLBwxsn9IEWQu3t7RiGQVWyomeKqqoqmvdRmj2TZcuWce655/L4448zcuRIVq5cOejYH/zgBwSDQftfQ0PDoGMVCoViIPX19SQSCYqKinL+dg3k/aTQ93a088yffmcfJ6XkmT/9nt6OdsBygaUyt5ID7PdZNYKEyP51nxE0DFB7/kW8PD29e1Z9dlmc8KQRVs2gJJ4hrGB5hVCGG623o53drQl645YlKCWERFKK9ZlFRB3WNiPmR0MSDxVZ781ejNd+R/h/v8eo4NTsU2QIIcOdLYQi7mI668YQPNpDeK6BUSxB05Du9D2JVkbYdPYS+p69ieU1Ec4p386EU79PYOxLg2al6VXNVkXoQcxPIlmOyHBISLbgiPVrRIyZJGQpMm6JmVhddmxN1hwCxo5dzYgR660NEor0CC53Osh89VgXv1h4JHvEaHvbLkbTgfXdM/UIjkQYaVrni3jLSZ2wt2gsiIFVI02KwpLynmx3XUnYpCoq6cFkl0/YIiiF1AR9hQ7iWKbElBByJOIk2sMcTg67a2w45ChSKferN8lTTz017LFutxu3e6hyoQqFQjE4mdWk9/V3qri4GCEE8Xicvr6+/cpu7WpqzBFPUprsuf8fTP3K1ywX2MBeYEkXVPE55+A/7jhiu3aT6Oyg8ZvfyjvOWV1NU/06jsuIl9YAf8YP+KLiKsxQ2l3Vt2IF3Q8+mLdOUI4Q0jTG/+cZXLW1rH3uaVvYCeZxUs1WIsXW3+ICp6Q3Ds2JsehuS0GYUS8mGsS9SMOB0BPEVlxNOHEEYoD60BxpYbB2gk7lCkufNFYvoOmjdVTP+xtCQAhAGowOnY4h301P4ILWa2MgVpAKmUn1Nqt/9ns514kJLiNEaAhTgwiB9AFCoHslztHHUlB1Ee1xDbaYBN6wmrH2fsIcVExZ64Cx41bR1jYaV9sRFGlt9DssF2mqAvTA4ofPiWU8L0/iUm7lo3oPjkQ/elII9RfWQdfa5Egdl1lIpn1JpkRf8v8m746yuc6NK/lVKzcjhLsdaFJmiSFhSmpC7Wz2WgMTybiodY53WNvsZt74owe/yIPMB9oiVF5ejq7rOdaf1tbWff7SUigUiv2lOdTMm01v0hwavsU5EynlkG01BuJwOOweZPvrHiupqc23AMK3/pl4c7PlAhsoxJIuqN6OdpraWzEnjMN3xBG5rrIMV1X31g3EXNkFAzNn9Wse2v94a9YaBmuo2vfKK9kbTBPN6cy1biF4pmkiUWFlFAWqLGtGmzkSLRkjpNnaS2AmrHXFXBqasxlzQPGeTItQd6mH+tmVRNzFbJtzii2CSE/HroKHiYYH9CzTyBEkQjPRHdluKGlaBRBdO7Qhawi1FQlk8hHs9xfhnnNRRlNSjeDjDRgj/cN6SgsBmn8P3nANDiNCl8/67uWtAJ1ap9C4g8toc7rQzQTOhCUWNZGZrq+B0AccZ7J6rIv2gLV98yjLQpdIrrMg7kf2mVy1Ptvd9fG3QoyMWy7jquqtuIosN+D0iS/y9trfEszIZDzUfKCFkMvl4sgjj+SZZ57J2v7MM8+wcOHCw7QqhULx/yMPbX2IZf9cxiVPX8Kyfy7joa0P7fccbW1tBINBdF1nXGYfriF4rwHTBaVleLzZGTcOw0TLsOb45mfUwkm6oDZteDcrrmjThnezXWUDMr72dJdnFQzcOu4ThDMqKJvh8KCWp0zizc20/OznOdcR3rgxr3XLFJrdvdyut2RUobstBeTKfM4mU7P6fDrC2cWL4+7PEkOZQmh+2Twmjz+Z1oojcBW25etKAZhEYvt+MEtTEOvNtnKZvz8e/6tWer7nrcFNOWGgz7DW6C8ozerMbk0Obn1OfjGVJ9Z9b79lHIgbDlsA5+sQn7VWodPksu6dTFguK2sVqRghDbRsIdTndmb3GUv+f1wY9tIMCZ/Ymy0Qj9gZo9GYgssVYuLE1wnhTx7nZMLE12lt3czh4rALob6+PtasWcOaNWsA2LlzJ2vWrLHT47/1rW9x++2385e//IWNGzfyzW9+k927d/OlL33pMK5aoVD8/0RzqJlrX7sWM1md15Qm17527X5bhlLWoDFjxuByDa/w4Httvrpn4zoi4RCaYTJ3ZzO+SIyEQ+ftMVU0h/vo7Wgn0dgEQPnXv8aE555FX7I4N67oz79HX7I4qx1Gyq3V1xXBu3ZWVsHAhrqTeC2jgnJjkTmkRSlFXlcdENm4kYJklWGnP05BbQinP47MeAAXFVlxQCEK01ljmhVoHhj7Epq3HoBNkwqQ5Tqbql7n4Rm/JqUYsmKEwnGe7JzPtgnnEOutzFuDUJrkLU4oZXqzlLB16zEEGtZnjUmErZYlRrEkctTgIiSGIJyscmj4+vPGiBWMvZiqDRfnxgjl0Vf+iBMjvhtNdNnbisKSZatyawql0KRBlbS+I5hJISTIqFKtIQecrMvvyWtliukacXcnibKN4B1E1GsmXm8vL4iltGIJ7d9xJS+IE/F4ewdd58HmsMcIvfXWWyxZssR+/61vWb7qz372s9x1112cd955dHR08JOf/ISmpiZmzJjB448/zujRoweb8oDwhz/8gT/84Q8YRm79BoVC8f8Xu3t22yIohSlNGnobqPYP3Rg0k1Ta/L6yxTJ5r81X1yUzxOp0N9U9IQqiMV6aNJJuv5d//fHXCCGY3N9NIFBAzamnWvE+697NjSsyTRq3bMRbGKBkwjicGSn43a1hRL7fy8kKyqVdG7m58R5KPyr44pMCYcpBawjZ2WoDxFBk3Xr6fvNbRpxeQPlJ7QhhPYd3vzKKkARMAy0l3ISJR7dcVu2R2oyMrdS6BK46F4Emk7bCBjqPeYnaHRU4PN32+YJtQVJKIhEppfmtz1B91F9z3GNaqsihmdSBUrB16zE4nRHGjl1DV1c1LS0TCejpJqxSChIOK/V80GwveyxEkrdCyL0YbRtxVE5L7bXXWNy4mK6RK4gV1w86lxBQF15HuG8T2wCHtwKHZxYAU/bEefLI3GO0ZL+xUq01eZ2WEPJHOklZhKQZpt/VB6RDUYrDIYSUOWIo7hDsWHQlCEmJFHRvuBg4NfuahUG3VsTtfMq2JEmhcYe8jHN29zAqtyXfIeGwC6HFixfvM1viK1/5Cl/5ylcO0YosLr/8ci6//HJ6enpss6xCofj/k7qiOgQiq5WBJjRGFQ6/fEZLSwu7klWchxMflOK9uMYioT62vGGVEBnZZGWJVVz8WeRrz9ljpJRsqikDIXjzu5dz0he/xogp03MnE4LHfnOjnYRy0he/xswTrZIl3lINicwJPraOsyooh929PDtB8M44B3+deSPVk+fk7Srv9BrUfP3TNP3271liqO+ZZzCKJeUfaU97WzSoO7aB5jdDxENOzKiVVWRqCdzCag5qxAryZ2wJwRiXSZ0rwfzCv6GNzjbuCN164EuzF9Poonv7HKrm3oPQzYw50qExm/45BqdH8tGv/oKXXnqMsjLLW+HQDZDg68/w0Zkafb4qIu5inK1dllsrUwxJDYR1nuJEgCKzHdw6brfEDDaALYSy77dueBgKKQWRkB+w1pII/wfdVYfQiok58rvnruJqprIJqZck127dF6cRwdv9Fr1ucHh2UlHiIOgKEYtZrqxdZaXZBiopQQgMTcdMaj8hJC3T7sZtfIxoZlNVkSDoK0AOcAOaQmfTO81Mnx7FETj0yUqH3TWmUCgUh5tqfzVjAmOytl09/+phW4NWrVrFH//4R/t9fbKT+3DItAgNN4V+0ysvkohFKSmroLC1Hb24GHHiiXmL3EHaBbZt5Wu5+yU5rrJUCv79Tz0++CKkgTfchhTF1AYnEHYV0Ty5LK8IYtVf4eYZFDf/ggmnNlK3pJ2SyWlXSNdIR26nCg283l6EaVJcnmwIKhI4tW4AzJg/bx8xKSWfLYuxtCiRkeGVMa8eJxFdSzR4O/G+B4n2/BlNHzyqORZ0MXfiJxg9fh76RJ143BIlTleEgp6JeCIZNYqkTlfZdF6d/1NaPAspWD0iY2EaVRs+a78dFamhJmpdtNNtgiNT7Ayw2mm5fb0GuuhisewK5sf7f8ns3X/G9OcvCvwzfsoKliJTbsOkEJK6k15XiNLJ3Uy7YBtTTtrE0cc8RFX1VvpcHlZMnZZ1Q0VGEFMi07YiTFymkbGvFykSeIMJxADrqyYNxnQVHrY0eiWEFArFh57eWC8NPVbNMC35Z3Fy6eRhHRsMBnn00Ueztj366KPDzoLJTKHv7R1enMS65y232Fi3HwEULFlC6chRQ6brS9Pk7cf/nbXtlMu/Rc5D1zTpbm5kx94GzJcr8luDpMmUzf+gs2Qqixuu4fQNX+PTq64hsSFPP4lUpejkwy/idNBR4MUotO5zQ2khqwtrc+vzmRAOF1JcXk7cmXKjGHaM0LRjJ2NES9n0zhdYL2dYdXGSkwxsj5WJL7CJRP9/7OvWHEOHP8ze1sK0I+cDEC+IE4tZgsXliuIN1/CvE9MPf2kmzUhCo37K+RTqp1tje0Yx7sX/oXjvCfZYIR1ocUu8OF0gMoSQ7skUVyY49+asK/VRm6ZOS/MEpJ7p4BH0FZTSUCnoLc0fIySFxu1cxquOubQVl9quMeH0UFjkZtSipnRomICJE98gVOTIcYnJjKyyeGZTNKnhyuj0WlX8ELWe1bj7NKbHttjbNWnwzcZ1jOgN4Cg/PH3HDrtrTKFQKA43rzS+QkImGBsYy9TSqTy+83H+te1fzCifsc9j81lypJR0dnYOy62u6zrFxcV0dXXR2dlpBwYPRmv9Dlp2bEPTHVS+uxGAwo8spbCsnJO++DWeufW3Vi+wpMsik/5uK5BWCIGUkuLqmoHTIzSN4upa3ti8JX98EKB130hpVy+vzv+pPUZDY/WDTcw8YiwFJRnWjc7ttgha213FM00Tk842yeSKDst91y/o2e0nMDqUvH9Qv3ICsZifWCnc8sIdTGIUphYBn/XAHj97HGtqa/nR7hJMsQwhTS4zbuF4x/ND3j93RSNOv494yHpoa8487SuScUFGXOCSAi3ZLy7eH7ctQroeQ+gxur3hjOPSoqDA4UAm3XCOWABntHTASTRcRj8gLCHkTFt0jEhG5ef2F6G0BmI7sw+XlitX1w0KCtqJ6WkRsn7Wp/nVgkmYQhuyI70UOr/Xv8ktPzP57nNvco4BujvACdUnsUdbkzVWCEk1TQhpZrm2NGlgIkBoxHEB/UgTqjd+lvik9DizoAcjWs3G6tGsc022b/Q53S9y/rqjKPrY2MPiFgNlEVIoFApeaHgBgMUjF3PGhDMAeGLnE0SNfZf+Ly0tzbHECCHs2J/hsD+tNlJtNMZOnoa2txHh9eI/9lgApkybxeJNuzhm214mN3UM+hBMCTchBNUT0oHdQtM46QtfpbCsnHGjR2XFTEG6vsxgndelCcHWAe6N0vGAoDfuskWQNZewY5gAyLAedL7ppG2XVSdpS89mSkIpF1McM1kxoM0s4kcNzXbRPik0bnN8mXY59H0XAtxF6V5kmsO6JiOmkYhYczW8bLn3ZEIjoWto/mQAdDiBYTgxTeu6dXcv0pHu6iVlWgj1JXqJu7rtOzcQh7Ydp2EFWjumLAZX/u6smn8MDqeVgZjyKEkpaGkZa9vq5hzxJOXjrAzHvsBMnkyJoNQF7wNT07hx6TG0JKtpu/prcjrrSilw9micuOPNrO2XcBuuZMnFlEWo+YVpuPoriWVk3vd7/DS5C3lx0hwyg8EeLD6BFrfANWJf3WkPHkoIKRSKDzUJM8GLe14E4IRRJ3BM9TFU+irpifWwomHFPo8PBAKcemo6O0YIwWmnnUYgEBh2gcbhBkwnYjE2vWytaUzyoVNw3LFoHsuCEKvfhTeWoCwUYXxbkCUbdzFlb9tg0xHq6sLp9qQWziW//bMdKD2qujqreamJiSiw3EgurRRvuC1HaAkNdGc/u9e9a8cZERgBE5fRFfPmpGJbMUrWHO6S9LnC3igOl+WwEAmDit5klrCWQCa9J3vivpwSO1LoPG58bNDrBet00Z50aQPNkSyZkBAYUeucesa2uK6h+f0Q3IvsiwDCtgo53D2YjrTrSSSzrRKRd3DVXU/7pAcA6C9bT3ftCrpHvGCPDZb10O+1lILwOBGu/F3YE8WFxCLdAOx8dhTvvnMSa1Yvo6pqR1Ypn3GzN+D0x9FL29MiaLAbkAcz2TQVwBktpXLTBVmH7Nh4DLGYn4l7sr/LJ8hncQwQQtWLN7B73i+IZiyj21nEXm8gx7VmCo0GHzhcqqDiB44//OEPTJs2jXnz5h3upSgUioPImtY19MR6CLgDzK6Yja7pnD7eiu3497Z/7+Noi8wssW984xtMm1bDv9feyCcfPmlYBRqHW0to68rXiIT6KCyvoHDVGgAKli619w9sqOqNG9T0hAeNHQp1d9HXlRRfUtqWB4C1uzbhNN2YGEy80M3HfzyZutFWGrUpfXii3QSC2+zxQsD42W38/aov5TaBrZ1Dc7iAfJUAJzd1IISZZaUJGbPxFVhWGG9EUNpvWYR0d7rlfeEgFpSnHafbfbTyEely2W4xSLvGzLhGImIJE6ffOo+ZtAiFVt/H7p8di9ZrrTEVJ6R7epla0mrPpXv6KBrzDCUT76fmmNaMtH5omX43LdPuSi9ECFoqLVeQafQg9NxIle4RL7DzhG8TM6xMOW95hGCwGl03cuPiNYm7KEZxsAXkEHFPyfoEAwOWkZINRenvjr9lrv1686aFtO61vuM5JY0EORYhIazA6UwXWrzHTSDcn1PgUUiTUf0mdGcX4DyUKCE0CJdffjkbNmwYskGrQqH47+eFPdav9ONHHI9Dsx5GKSH0SuMrtPUPblFJkQqMLioqor//aV559XgK2m7jxzX9HONP7LNA43BrCa17zuqbOPWIo4lt3gK6TsEJ6QBcZ3V1TpXocVdfzUlf/BoiuU1oGiOnWrFPoe4uQl3pc/ZlvH597WoAIkU9nHzcsYwbMQpfICmUhBX75Ixb1pCoFmbK5wzWPX+PbXHIzEDr7erkpbaxZKWFJ0UQ/uNxl3iyvGzurpNx+S2hUx4rRJPWA9aRzHB6UX6E01bvyHuPpNBpoQZz0NCYQpz+dBaWw2MVfpRmIUY0KYQKrP1mQqPT7+Evdz7NA7tmUNBrXXfKIuQqaOb4inZ7LiGg5qj/pXZ+norVQmZ3q00dABiJLoSeHR8Td3dawinjmOrZbbhcIcLhwjzN3wXRHhee3hBHdmYXecxBaCzh6QF1BQS/n+ymxS2IutpZ7/xfe5dpOhHJKtg5Vj3AiSUQExnB0jGyC4oaiSn4IzGO3LXJ3qZJg092/ZuqqCAhc+PVDhVKCCkUig81KffXCaPSgmJsYCyzK2ZjSpP/2/F/eY9LBKNEtneTCEbp6bFSlEtKYMOmq7AzkgScVxIjoJt2gcZ8ZLrGzDzVlwG6W5rZve5dEIK6uDXGd9RROEpKssYVn3NOTpXomSeezBd+/xc+efXP+cLv/8LIaTMBCLY0EQunCwKmRNFDWx/ijXVrANjr2m5bs3xFyVgVvZyu4olEk602XKab0nBPjtsllYHW1ZEuYmgjBOGC6ewa/VE8JX1ZuzTdRLisBrRaX9i6nSY4HDE6KOVP4rJB23hp0qAKq1rysz25VhZvaQfTLthG6eTu5BYrCNmIhUlEB1qEBO2FPvvhbyYtZvFU5lhRc06GmtAGCcuR5MTdpN4ntIHp8xD3teQIp1RJgVjMz9at87NS6LdtPIp4yMnuikm8XTozzwLSaNJgGutyFmoKwcbR77LzhG/jO/aN9Hg9YRXLBIyBVcQBR7KjfKb4GSiEtk88gla3n4rebgCqZCM38yXmhV+nl34co4dfs+tAo4SQQqH40LKrZxf1PfU4NAfH1h6bte+MCWdQHi9mw9urSHRHsvaFVjbTfMObtP95Lc03vMnmlVan8h7ns4gBzgNNQIVDDlmgsbi4GE3TSCQSg6bQr3ve6rk4euYc5CtWPaDCj3wk71hndTX+Y47OqulTWFbOqOmzKCwrpyApnlrrs60qjc07aQ4188vnrmNkaxHS7KXH1WFbs/xJi1C0YD6r51xBb6EVuyPQqH3iuwx0nKQy0EQinLNPImkedSoIga8iOOC4KM5kgLKIR5FmD07DwOmM0kxtTkE++zhpcgm3UUYnmoAlhYn84zQYtagJpz+edo0lhG0RchdFk9vSqkYKDZLuq1g8JVpkjuVJmiI3DEdC+ZZPJttlJNcuNUp3nAZAQoYRA3p6Ofurcj2JyZICAC3NE9m7d4r1umUsza2TMBxOmuqmDaLEsu/RJDbnuMeElIRG/yvncLerP1UHMif+qINS2yKUmT4fJ9vC9cQIF788dgkbq6z/Bsppo4xOhCb5X89rvLt9H1asg8iw0ud/+9vf7vfEn/vc5ygsLNzv4xQKheJQkbIGHVV1FIWu7L9XSzqO4pht16Gh0bRxJZ4JAYTbQaIjTKI5bUVBgrE9yNgJbzOydhMDkRLaEoJPTvrkoAUaUyn0nZ2dedPuTdNg/Qv/AWDavAWE/25ZaAqXnvierttXbAmhjr3ZFqrbXv0dc5rf4OznahG8RZS3mR1fSq/naBp6G/BqyY73AxpuWtfgpc7Xze7+kuQuwfHnf5ZVjz/CWyu6sCxC6dYRwQJBtRagaMyLVMxKu5cAtle9xljNar0kDANpNOLtD+FwRCmnMSeFO1Uq4DQeZjHP2ps1kbeKgDWvZmWP2cHScQ1X0iXm9FnbHJ60kJLOjId8Ughpjjgv97hZFEhnF7ZtOA0zuorqI/fY27wdUynbZQVx+9tnEve14OyvQmoxOsc/QiLRm7wME1Mk0HHhjJaiRwMYngyRKKCktJGW5onJdaTq7migaZhuL4FwaPCLBn4cuoHJ/rch7uETxgs87F1i30MpBMu5gfP5G6fyiH2Myx2ym8KaA0xg3+A2yrHipOJZrjEnA5FCUF9hxXutZxYrWMo0sQOJVXtr/Pjxh6WTw7CE0BVXXMHIkSPRdX3fg4GGhgZOPfVUJYQUCsUhoTnUzO6e3dQV1e1Xb7CUEFo8anHW9kQwSuSRPXZxRQFEt2VbLeLuTst9IQUFU++mtNB6GLgLZhPtW4vdNlxAoQYbOjbYbSzyUVpaaguhsWPHZu2rf2cVfZ0deAqLqOruo9U08UybhrO2dtjXmok/YIkVOcANV9inI5/alFFEUZLof5bjt32eMqOarVu7GIy+cDEhbyGVZh298S5enrAb+fc7B4xKi6FKM4ru7aRm3j05z+xI4VpCjUdZR5gGWmQrrlgchzNGGZ2MCjew25e0RkmDI7pjrCrxog90mA2uB5CmlT3mLbdEjJQQGJPtovOWR3H641abD4dlDROGIB6zBIju7mFTFBZlHFMx/VFa383+Dvq7ptqvndHSZE0hg0LPzewETDOMFAmIRUm44ui4iLs7aXXrbGEhIJnEZspEJxMnvkFXZy2xmB/TsJ7JmmYJNt2EgmiYeTs3sHJcnnYqQCAaBz9oCS8LolssIZQpnITGP+RnAGwxZBhOy8olocedXfRQCo02aQXRZ8YIvUa2hdUmQ0TfIS/jav06a579qL11oBl2QcW33nqLysrKYY1VAkihUBwqHtr6kN05XhMayxcs56yJZ+3zuGA0yOpWKyD4hJEnZO1LtPbnK/2Cf2Etzho/9a/fng5kleASEI+7ebSllItP/gFHFY8kHN7F7oa7aG9/hvNK4/y65R3eanmLedX5M1HLysrYtm1b3syxtc9a2VfTjl9C//MrACj4yNKcccOloCR/VlVB2JGnkrQEowezycW2t1qz9yT7dQnhpDm2iBMLl6EVaUhpUr33SXayNs9ZBF69kGI5ErNgV06vMICz1pu8HGuHggKEaSATe3HFS3A6orRSSYPXcq98sv6X7EiYTPRdxKoSL5EB7phAw2K6Rq3IX2laQOHIkG0REprM26HEXRQjHnIindbcjkSR7RpzFrRRETcGHCOpnNWUvc3MtY60+39IrbEOsJrcmo4IIhSl32U5lR6uM7hR3GbXahLS5FJuZbF4lniRyd5EOV7dEg2alhSADjfupl1MKHGzkum4ZJiYGCBcEklBl/DiMlKVsHMv/D55EQt4mTI6rfpIpgk46fHlydZLHp+KC+qglH9zTu64AZhCp91Rlpxi/2pvHUiGJYSWL19OQcHwix1dddVVh+2CFArFh4fmULMtggA7O2th7cJ9WoZe3vsyhjSYUDyBkYXZba/Dm3Kzt0whaZkV5u/1N3HCtCeyUqOlhHVrT2R90TpGFY7C46nG46nB5xvHa12vMcrVxwJ/gtvX3j6oEBqsllCou4sdq6widtPnH0fnr34PQOHS/PFBw8EXKM563+OLU9TvxJkQeZqsCnRHcY4uTETX2q0qvHohtWVfst0nQmjMHnE0wdC79HYlstLVxxbMZF75RxFCIyba2ZHHaqOFIe5JHmMYxEQEb7wAhyPG83wEKTTmayFWaO8yLzoDv27FE0UzhZAUlOz8OPc73mFZdRepIsuZXr1Ri5ro3GJV8jZjWo5HKbPmkHRaz0DNcFNclLL+tXNGHsGcoyvyCKEqI241KJUupIhhOPrR43GiAlpcgl+OHZs1kRQat8vL6JV+7p92IVIIhFzIpcAszaowbri9ODv2oHktK1eM3GatvWYpNVgWIZcxeJiwFBotssaKt9ISYEqEFASMPFbB5I2zCyoOFsc14AZr0qBCttLMKLv21uFgWMHSy5cvx+fz7Xtgkh/84AcUFxe/1zUpFArFsNjds9sWQSmGys7KJFVNOtMaFG9upvuR1wi90mhtSP7NNjD4TfXfOfeF81nX/Fxey4GuJ/jy/C9nCTC3u5Lx474FwKnFcd5tfoX1HfmDQgerJbThpecxDYPqCZPw1DcgYzGcdXW4Jw2/w/1AHC4Xbn+6gF9HwAp29UfSv417/UXsrh1LuOJjLPnMPGrGB+zrlmZvVr+uQmeJLYLAqn9Tf8J3qPv4pqwMLa9eYIsgAFesHE9wnH1cKshYJATxZEyOhs8Sm0hwJngBKy7qaG0zEpjcVkFB1HrgR0laP6RG5YbPckvpU6xphQ33TmDvK5V5au9A6WQr4694Qi/B+uwf/L17vbaIMx2W9aSvbDsj69KWrrzWpoHZYXlSzgsTllvToXmT8/cjE2GElDT4tZzCg9a0OveLz9j7pNC4g8sI6pYXRibCIAShRLJNS545OilL7jMwPa05++0VZ2Tf6ZphJQFIjWJHUgil+rpJg7FY9aRSrrHqZBzXQKY27rSPSwVtTzYMPjXlNObOnZsz/lChssYGQRVUVCg++NQV1eW4cjQGz85KETfjvLz3ZSAdH9T94INsP+Usep63/tA7yvvRvz6G79XdzMUTfszTxa8CVuDzwAeblIJoNMC5M87NOdeIEZ+msGA6Pg1OL47xl7V/ybumVC2hrq4uO4W+p72NVU9YcRozTzyZ3metgOnCpUuHbLA6HFJxQgAdyWKG7oSOQPDulCO57dPf5v7TL+EPZ89nzVg3BSUeFl84BaGBaXSR6TvsjXdZzUHJrX+TytAqLHSztLrXFkFxdyfByjeI+SzR2fZyLcF6q7lpd1kdUkv1MLNCMhwzp/OW/wiCogSX2c3o/jX4wzrTmyrwJL1TjvajGLXye4x78X+4XlvF08Wv0uNLEA056N5ZlKf2TraFKDA6O0aocETYFnEpIeQt6Bm6a4UUFLQeMWBjriiImHNJyDJcDn9y/jDEwwgJo0Jm/grQ0sxTmVmnw5ESNwIpBJFUH5I8c9R4rIzDeHE9ZmH+WkwAH+NRyrCsk5qeAGlZCvtjlugqposfyqu5mS8xAqspbMoiVEYnx5Guop1ax5jOFor7rcDwL3Mzi3kWt9B4cU0T96/8Lymo+M4773Dddddxyy230N6eHeXf09PD5z//+QO6uMOJKqioUHzwqfZX57iaHLqDrsjgQb0Az+56lt54LwFXgJnlM4lsbaD1N/fiPfrLaO5CjO7ddN31Xfa0redd/xband32sUFDw1G0MD2ZFGzdegwuvRItT40VTXMwefJPAMHRfoPtTU9SH6zPGRcIBLJS6Nc+9zR//urn6Uu2qkhEovStsB4uhcn4oKZgmFe3t9MUDOfMNxSRSBOBMXG7sGBnUQwzKWx6/UU8vegMW4hIBN/Z3MDqYIjOaYWctHwep1x2fNZ8YaOXle1WscfB6t+cMOFkNNcEpJR0j3iBHYu+RfOcPyJdEaQEPW5gxizPQ1fFhOS9BU23Cu295Crkfv+nAIiJAI/Gyjk+WoZH9+M1rPNFhQtf11Sc0VKOjqbLEKwf22PF+WSE8+RLrBrYozTlPnP640hnsv1Gx3hkjsUnTeWmT6NHsxvnSpFPCC2hOXonImZZoQxnPzIRQUhwl69gqXwyz+wiR9xo0qDatISIdDhAaCSSqfg17EUbUGW6OpDuZO8Qg5QXkAbLeCx9Ds2wosulRjRhiSwncaaxnjI60Q1rHhmpso+pYxcAs9tizNvwDgC9Hh9mcm0VtCfvjUEBMa56aN1+f48PFMMWQk8//TRHH3009913H7/4xS+YOnUqzz+f7vIbDoe5++67D8oiFQqFYjBihmXN+Nz0zzGrfBYxI8aX//NlGnryu8ce2voQ33nxOwAEY0Feefwp2u+ox3fsN9FLxiATUcJv3gqJGNVdoA2IddCERjRi1RWqrT0fz/ZraGmeiN905ZwrRSAwh9ra8wA4uyTKXevuyBmj6zolyfo+e3bu5Jk//S7robfib7cTCofQS0vxzpnD/St3c+wNz3HBn9/g2BueG/Yv6sbG/+WVVxcRmLmCaRdso2JWB7M1DVdSFHUFymwRlMIAPrZqK+es2c4JG7bzUqkf3TUna8zOPquWUr76N0gNV7gaF2exV+yiZdqdWUY1IaDkhBY0mazX5LDWIqQOZphefxFPTDs6y3yzwv9x+go8uHUf3uSzPpyR2Hyc50ImNhRwzvMjmLkzgKwuRHNknzMf+cSRuyiGdFjWDk/bFNoaT8kqZphJpGgHwVErsrYZTsvSFCJCo9ZJiFRdKg3ZYbn1Uq4xnN20TLuLapGnCrkQlnsphZTJuklW9XOpO0AIEskM7yqauZkvcaFMf9+Coth+nSqEOJDz+LttDYJkVpo0rbj55Hcj81iHYX1ehpF2LfZjidpRIZPRTZb46vH4SSRrMbmT90BqBmWEMaSkvj2jLMUhZNhC6JprruHb3/4269ato76+nu9+97ucfvrpPPlkPtWqUCgUBx/DNNjctRmAT0z8BLeedCuTSybTEengsv9cxsaOjVlNT3cGd3LNq9fYx5fHixnz8oBml7oTzARoGtWT57B8wXJbDAk0zOZTMSNrAGh4xkf7i5bLzNtt0vXAg4OudcL4byP0Impdku7Wf9ISaskZk4oT2ru73u4Qn0JKSb/LSeHSE2nui/GDh9baxfxMybB+UUciTWzc9ENSrhqhQe38VpbNaWf6BdspndxNSbADMbC6tUz3oTeBHzU001s0gnw4o6U4QuUZx2pUbfisVRcHDYpC+UJmEBo4dGv9QreCfYXUMKJvWOJsYPCt0Hnd5ycacNkWobCenrhq9zhO3DAav15EqX8M+phsK00+pMwVNtKESK/bFkKOhJfdO2cRjVqxPWt6x7KeGXZ/s57aV3Our3Pco2we+Qj/cL/C467V3Od+hc265RLU4qkYIcs1Jr0dICQB8gUlm2ysTZdW0KTBYp5F05PtL3QHUhO2RchNlDI66aHYPuZb3MIKLIuiPogQup8L7TFgxQgZbh3TEcFMKsUsIWRaSjSzjlA4KYT8CUlNhyXUej0+e22elBgUBnO1ej6lr2BM+fBjkQ8kwxZC69evt11fQgi+853v8Kc//YlzzjmHRx999KAtUKFQKAajvqeecCKM1+FldOFoCl2F/PEjf2REwQgaehv45P99kkuevoSTHzyZzz35Oc595FxkhrmiNlZp1wpKIYSGVlBJxTe+jrO6mrMmnsVTZz/FL4/9I6Ft32O8XoBTM+gIlVD7h7/SZ1rBtn69kNZf30G8OX8/MaezhCkTrwLg5MIo/1h3S9b+RDBKQLdEWTRPoywhJb5YnIKlS9nZFsqpaDycX9T94XoGxqtklI9h1KImSung5Bf/TZbJY2BcCtBZMMD9lfH0N13pStxjXv0pxXutgHRTSgrC5XlLE0gTZJ+V9eU2LLeJ5VGSlAQ78igUg0B7Fx7Nb1uEIhlCSCCYXHQkp436EtOrTyIczo0RyppOQveOQoSw0r/XM4N2WUrDizXEor7kGImQDmSviWG4WcFSflV0Iz8X1/INbrPEwyCp+nLqw7jcVm82KeBlx0ZCRNAS1tyRwl3EPV04ZQAkeIlmzyFNa+qMz8IUOh2UWq4rQOpOEBp9LsvKFCTANsbzKGemp0kGWHdQii7zCyEpNG5PjgFwe/qIF7pAgGlbhDL6tZnW68wiiv22EBKUBa3r7vH4iOlpkWadywAc/Nx5OzXkZmseCoYthNxuN93d3Vnbzj//fO644w4+9alP8fDDDx/otSkUCsWQbOjYAMDU0qnoqdgDXwU/P+7nWeMkkrda3iJqZj9cGl2tmAOEgZQmZl8riYzsrWp/NUViCkY8wOwKK+ura0c5OoKw13p4F0gPjuo5NG3YOuh6a2rORngm4NaA9v+lO9INpFt2uNZaQqZl656s44QQzNjThs/pxr9gASvrcx8YQrDPX9Q+7xiG+rOfcgNN27LGFh4VbU05IkQHJojXKagN2XFGDs1yDSacvZiudNCx9aBLigigOF6Ft2ty9omlQHtmLCKUTH9P1vaRUmBCjoVKmCbLXvg3p73spioSwGNbhDKmRDIlcDRCaARMH7Fobn8ue6wJzavKKB7by384mW9wGz8X13KFuI2XPIvtYooxLYZAoEkn7WY5t/Ml21JlCwyZv3SMEBKvNx23JAUEtT4YZRXq7BnxCns+9yTx2k0gcnt1zebtPFYxQQs1GULIwdqJs1lTNwmATWIGy7kh5zhT6Ly2/TSadk/Ju1ZrfTpP8XHA6m/mclvfTSM5l0469iglhOIi7XvsTlqhpJQUh6zPL+grsIVcpmtM4kLDhM7Bg7cPJsMWQnPmzMmKCUpx3nnncfvtt/P1r3/9gC5MoVAo9sXGTqt+ytSyqVnbE2b+X7rfnfddrllwje3q6nT10FOR0dtLgG+Whox0E/znQxh9IXvX1tY+BCazyi0hNPkVy7XVnywt4pceHDVzOPuR3YPG6wihMW/mzZgSZnpj/Hvtz0kEo3Q9tBUkFElrrr7eMF69kHmnn80nr/45Z845llGdvRQcfzybOqL84fntyfnScwc8Tkp8g8cpAXg8NUyZfJ39Pp8bKNrjJly21O5gbzocjN29xR6jAT90vMLkj7xCyWl9cIEHfRZ4/NBfspH+4s1Zc8a9bcm1CjvTTUtY7qBAwwk41n+Tjnd+R8fGYkTUergabuuBGtfjrJzex7YxU0AI6uROvp/4CZf9/X+YtWkVAoFL8+Z1jUWqpJ2h5sdDkfTS0jyRxkZLhMUjlvUi3O5mw70TCDX66dRKuZMvZombJ445i55CS9yEHH1IzSChh/PWyjGFTnfvvPwWLynsPmFgxZPX+X9EUL6dHqRB15inQA6oiQS8w1G5kwJVNCH0VKB7Ic8sOCX7izEwAhwrdV1vczEqNhKHjDMYj3MaHZQiBHg9PclrtK45isu2GOlGyjVmfW4rWMq7WJlzd03xsHWk5SpNxQcBuJP9ySyh7EQKHUrTpRQOJcMWQl/+8pfZu3dv3n3nn38+d999N4sWLcq7X6FQKA4GGzuSQqg0WwjVFdXlDXI+afRJnD3pbJ46+yn+suwvPHX2U5R0WmYEGdtE9fePpvT8Y3GNHYsZChFMWrrX7Q1y/eMbqSvcQ7GnBzOqUbkhTMhfZAshn+lAL6plQjwxZLxOYeFUZMCqhePr+hfBLQ32gzOQFEK9IkJVxTgWnvtpRk2fhfmylervXryEr/1jFTHD5CNTK3nle0u4+/PzqChw0x2Oc8/ru2gONWfFRQ2ksvJk+3XLmrT1QprQ+MYyNMeX6c3oXh72+ChMpjxrUvLyJMnk2K95QVtqWU70a/nFgp+w+/wyGub9gqY5v886X9ybnWEMkPBY8S8vmB/lU9OP5etH1PD1b1zFq5XW5yidluVO1yVTzRPYMs9yrc3nVSbGt1MQsh7KLs2LJjQ8CesG9usmb2tWtfBEqWZnwkHaYxVLZqa5vEkrRkQnHnISDbpoNmtz3IBS0+gusbKholo/kYpWYp42yuKdObVyrAyuDGtdKrtMamzdcgyxmOX6FBKWGE5cvpY80dkS0VNLfIBFaKicfU2XgElficcWsAOPTa1VSJNFW96hIBZBl45BA6at5eu0UIOUkGhxgISd5VYWX4MYa7sDnSnXmNDooJTb+ZK9XikEfzz7k/jC6f8enDJqWYDAqmcknYjTboZA/rizg82whdCZZ57JTTfdNOj+888/P6/FSKFQKA4GpjTZ1Gk1OR1oEar2V2cFOadab6SKHabS7sv6C5BGAVKaFJ00AUfAjdA0Si66EICue+6hNRjmC399i0jc5IypVsaOb4NE6F6m/u0uIl7LupEIWg/8b7VsR0vEh4zXWTTrl/SaDkodBvVr07FCfulGkwJDmIw7ZTEOl4vozp3Etm0Hh4Pf9VexvS1EZaGbG8+ZTW2xjxMmVfKdZZaV47cr/86yB5dxydOXsOyfy3ho60M5545EksUijUKa36wiZTzb8dQ59Dacg9AK6fZnBL16fPR7krVuhCD+9lt0iNIct9BfRDKmZMDzOq8QcnfRQSm/Gj0xXRxQ0/jrzJOtOZzph2Zcd7LHYwU6H8lK4nEXZvIcnmRFaUc82S1e06HTEkl9e5tZ6bUy2TbrjQSFNaeRyK7yXDiin9LJ3VbhxNW5FglhSooi1rExEaG/NGi154j3M4vV6YFSciwv4i9eSYewYozcq6+26xq1tKQLYH4qeizTTB++sJFjrZFS0BbXbNfSvkiJkeqaLXim7s1bO0hIg2v4Pj+UV/Pp159marOV2r5VaxlSCGnJooqG4cDsgHerZ7CtMl2FPeUOjAnL4hPTtPyWMk2nMJb+TD0Z8U9SM6B4Isz9zLCu92CgCioOgiqoqFB8sGnobaAv3kdABtA6NYLB7KaoqSDnlOUnX/+x4CNWnTCzp56ik9K1cYrPOAOtsJDYrl3c9LO7aQpGGFfh55jAWwB41mrU/uQa4rXWr2NDCh7yWUXtKkvH85mNTw4Zr+NxFePXLrDWMOoxEpVdIKxikIXSEla+5AOn77nnAAhPm81f13YgBNx03hxK/WmLwVlzRzCuOo5Z+oAd85RqNzLQMpQSQr6C0TgDlxDrqwDA6Us94CTdBelgG6lpdAXK7Pebt47kdbkwr1uohZqca+0v3kLcnbaSSJHAcPfQTK2dgZSeQ8uKeQFoKKnE0HRqtQgjaCCRcPP2tF4kkjEFluWqKMON1Jc6tjmEO2wSIsLLjo30uT3sLS6nx5n9uWTWChKbst1PwpR8/K0Q/mSdnJiI4Nb8IKFDltrun9REr7CI/+N0O8bo3LkzeLBWp22A98mPB4kTT8ykpjlDFCRrUr3iWsjD5BbnzEdKjIgJndwnLsqxHFk9ym5jAtuZxnoKYulAdokYXAglhZ2VRi+I+ot4dfL43MB5oRPWrLT5hHTnrSqtSYOKSDrmzp0phEQCqWf3QzvU7LcQ+t///d+DsY4PHKqgokLxwWZjx0bG9I5haf1S7vnbPdx8882sWrUqa0zK8jNY37HQBusBLStBONLxC5rfj+PU0wGY9vqTFHoc3LLUQ0RYv6QrJ55J4IwzbPHl8fl51unClBK9dBxn73mHTU++wGCEVrUw5T8n4uuYjtQTbD76NuRZRTzXdC+e5HOp8YXNSCnpfcaqJn2vy0qbvmzReI6dUJ41n0PXOGe+N6eBab52IykhpDtKaa5bjRmyWj24CqyYJ0OL0VmYXYSvK5B2oV12RDX3ap/LW9gv1ZIhk1hgFzsWXUn3COt+JNzdAFQZzWh5YleqaMoSQvVJV8zxvk4EkIi7OKPSxY7xJpMD1g9VpwQ9mUYX15NtHrzjmMMRBLV+NtSM5p75y3h09nH8eszlWanhkA4SL29MZ0B7wv185aHXmb0zSiJZTFFLgH/9SPw942jVqvOKwfu4KMtS9vsRc3i4NB0H1Ofy8FapTpPLsrKVdVsfuNZby5tvnMn2zpk8UHTukK6wgZhCZ4uYmre/1xKeZjHP5j9QikFT6FPCzspKSzBixAl51ySkSWnSHRjHRRmdXMqt2a005G1U9KRj8dykrUNSGFZT18PIfgmhW2+9lSuuuOIgLUWhUCiGz7q965jbPtdO25ZS8uijj+ZYhgbjkf97G81hCYorOkVWgPP9K3dzYXsdJoKjWjfzncQmov/7XQDcLT5GfOengFVRH6C2soxHfrCYeFUy/qRmFvovfsLePQO6tUtJz4oGuv53C8IE0XMWCQmO2CZef+lntEUacASs6+no6KT3+W2E37Gq8q4om8LskQGuPHlS3us5dvSkPC0kBG4qsuKGItGkEFrn4St9J1Ebt+Jf5lT8A9O3jc6KN+gqzo5vMjOCXO0WDxkPxVTfqLLB0p+FpGXa3ewtCvJGRYxtjGdvZBTn1Ge4D6Vk2e4Xk00+k0HTQrCr1FrfYr8l1OIJN4vco/hyIh18Ddgp9MJdlFyetS/qLODFSXMyYlbS6eP2qU2rueqeonS2my/Sh6/9cZZ8eQy4ksUU414Egjij6JWFeaPN84mjSMC6fxurR/P3+cv40jwfJy+awy1nPEXkxF9a6zWdxKI+gr6C/A1Lh0CTBpPkRutCBvA8J2Vda9Zy5RAWoeTatzIZTZOMDCcQeXqUfIq/EYhac6SyxhbzLMXJOkjfljcw69kGnNH02jItQggT4rnrPpQM+25fd911/OhHP+KJJ544mOtRKBSKYbGzaWdOnzEpJW+88QaGYRAMBtm5c2deYdQUDBN86FWE0Ojt2csafxnf++dajrvhWRbc8Czf++damvxl7CiyrBHzHryVvgpLQFRPOx/NZbmlUkKoqKiImoCXyiMty5McdQwV/V28+PWrMJKWCmlKuh/ZTs+T9QAULBrBjAvP4eWQZRkomLiGkvExps632kv0iH56nm1EuArYXDyKcKCM355/BE49/We7ryvCns1d9HVFCPUXYkTTrikpBdGms3hq+4ss+2c6bmhHs2XldoYtd5crbAmNsE+jo7CRqpqthAusB9pQGUWZXM5Ng1sdkqwQSzhz/giumDaZ5fyCGwt+xANj0m6qca17mdFuNe/UNAOfs5iaU84m5nQhYgbjse5bIuEi6iqjVt+ZVXQylTnWr2c/rDv83rz9uVJuPCmh8Y1K4iEnMVe6W7swDHbXjmHl9rcskxFQy3jemBzmd6cVc1/1OdkXKKU9LhMhDTzBBH0uDy9OmpMlJH/S7eZ7qyzx1aMHGVO/G0cinjfOZzBSIrS0q5+lPJWzXw5wWbpc6UzIHNdYnvP+nm+xgqVUxCOcuzHdNBVpcj5/5VQewRG3hGJMZLhUk/9tTnnrM+ze7qAwkhbX7gExQmY82wJ5qBmWELriiiv45S9/yWOPPcbs2bMP9poUCoViSKSUbIpsyiqOmOLVV1/l17/+NTfddBN33313XpfZzrY+jtatuIbXE+kHw57uCJFwE5NLtjBe7mJcj+XqSZSbRKZa5yorOs4enxJZRUWWFcIz3RIXnrIJmC4/cza8woO//isybtL5j02EXrPmC5w6juKPjaPAXUBlxUXEIzouv8Hoj2wnanyXquqt9LoiYGi4Z57Hq7Uz+OknZjC6LF0Fe8Mrjfz1qlf5902r+etVryJ29KFl9I6KNH4S2T+Je7b9ClOm44a6gladI2fEsoa5+q2mpl1eDy53iLqJqwgKq9VHLdn1jAZDH9hUNO5OZ0yBnUlkDrAmZQqUhMOBaVoPUk0zkN0+1oaTHehbI7R0Jy1CcTcRZzEFzgRCCFsMpYSQ4cyONxkVMnMsGZluPCEg3GYJoNUzjkmvuaya+0+/hKuKJ7GxZgwAIZeXZ2bX5rWKDebKWtD5Nu4+LWnpye1K/0LNfMv9pBvsGj2KhMO5X26xE2LPsJhnCfeVc3xmo9M81wpw9DEPUVu5DSFBSi2rMOLUpl25wdspC5pPsLCpA2/UijH6Dj/jVKyGwKk54hlCyCBZODFaCEiKoplCKCNOSRjEQv2sfe7pYV/zgWZYQui3v/0tv/rVrzjmmGP2PVihUCgOMk2hJtqMtqxiiEIIpk+fjsfjIRTK+NWbx2U2Ys1KfCVWzM3fCi3xogn409mN/PKEa/nuvN/z/ZN/TXhhgtBCg9ZrEuACJHTtfdWeJ2URCgQCADjLvTiqfAgJsY9+AYAx9/6Znb99k/DadtAFpedPofC4dJrw5B0ShzvzF7HJxIlvEHa2YUgD58ijmbTgZM6am87W6euKsOKeTVnFAVc/vINAxv3QHf18YWlhTsFIj9N6CKUsQs5+yyIkPVG83h4eF6fbY3czZrCPIIs+CrLeu0M1VG242C4L0CxzM4kGEnG6MM1klp9mIBJenuu1rCVaa4TOHqseUSLhJqIXkDBnAdAVbea5pntxJlvQVwSyMwhfK3dky2UpOYVHM9/irYjQ6y/i5XkZsUMZYu3FSbPpc3kGETNDIE3GbbHchYH+vrwWF5kRIC41bb8tQi+4LNeXQFJIX/qisETQQJelEDBu8us4XSEY4Bob27sbjVwroCl0mnwaGgItefnFpP97Kh5hWRljeYSQUyQAgS8SsdeV5RrTDITQefkvd9HbkZtheCgYlhA6++yzWb58OTt2HJ6qjwqFQpHJho4NFMWK0NFxOp185jOf4YorruDcc8/lrLNys8OklLS2thIMBll711103/UvhKZjBBuYsHcVmoBffKIavfdGRFI4aEIS/LRB8NNG+i+lgJ2xO4hErF/Yma6xFN6kVah2zNH0jJ9P9fyv4mqLg1un/PMz8M2usMd2Nzexa/XTuaVkhMTtDhLcZbmbTkmUYEbTYmnnu+15iyEWRkrs9x8/0sFn581DExrl8WJmhSZRFQ9QmKza7IhYcSOOcAnSFOi6QbcW4CHOy1zIkJ9DineZk/XeHYlQvPcEKjZbc1WHctPEBxJ2ugk6ClnPDLq1ALtr2wn6dVwStI4I4agVc5KIu4hqfqKm5Z1oiuy0YquSLsNmr0aLW/BWqc76Io2fT3fnXMdj4hN8ndu4l4voFKXUHtNKT1Ugr2sLLLES9BYQ6O/LjZMZglk7NtpZWgWxCDP3bMszuWQH49E0g01Vo/jX3MX7ZRFKCSldT5BIFjT008sP5dXczJfyuiyFkHh9vTkxQl5nH05y3VSaNBgRC6Oj2YY+kSGwXSJdRyiFkVwL7nb69OlowrStSWKAJVfX4NQRl9H9Uv2wr/tA4tj3ECtT7Etf+hJLly7l5ZdfZsSIw1P0SKFQKMASQqVR60E+cuRIxo1L13+pqqrKcpmkeOihhwgni7qJI2s5LtHImMZVfGPLEyz/yefxVjSxevUAF0/e55FJOLwLj6cmxzWWeVB8dx8jZlr9Gc1wNy/Gd/PpcQuyZnrhnr8Q7tRz2nmlqhB3tLxGYMwi9CD0PLOL4lPHsX11K688kKeNh4Cgp81+u7dvFwWxYn4pvkNdRx/u/hpMLUb9uO8jTR09blU53hoWOCLlGL42un2F+x2oC/AWx9BBqW15KI5Z8VSmw/rlX1awkYXyJV4VgxfdDbk93DX3TKQ4C6GZ1I2x0v4X+N1scDpw6ZaVL5FwE+nYQ8S0Utdbw7t4d8qRbE5aIK6f5kFgWXKElLkWnIzmao/xCR6Xp3OpditVWhv5+qqBFYcTCPfhj4YZsXkreyZPHJZYqQ73Zb0f29HM2lETswcJwf3yQiZrG3lh0hH7Z3EinWknHcWEk/fARZxprB/0mNT3y+mKZAmhAk83DhJZXc5sq5I2hR5KbAmTKWbSrrFMIWStpeXIX2FEp7OxfBFht+WCfJ1jmcm7tkiTwkATDngjTGJJFEcgu6r2wWZY33ghBLfddhuf+tSnOPHEEw/2mhQKhWJINnZupCRqWT8G/jALBAKcdtppdtaQEAK3222LIEg3vezpXIcmTYq7WpN9uIbzENLwekeTSCRsF1zKNZYIRul9Pru9hkQSfuMWpj95G/9+7A17++5177Bt5Wskwm5GVX3bDi4FaOkaQSzmp8/vofgE6zr7XtnLuw9s4cnb1mEkJKW16XghBLiXdBJyB3GYpQR0k7pmkyfuvhq99hr2zvslOxZdSXDkCusKwiV2oLlfF3ac0DjPpv1yy9jXKDRaZK393jHpJOLuNjrG/9veViuGjjcyNT1dXFFo7CpLpc1rLJpcQYHTutfxuJtQQwMmpUgZY4ejl6cXnYH92QmRMY/Y5/WkYmBecy+ksLE3Z7+QkkVb3qGou41diZe55fS5w7bYhP06e4vLaSkIsK2i1s6Ay7l2obNFm4zMVxV6HxwbW21l2ulx2wqTLxMs7Ua1ahXFYv6c9PmR1Rtz4r2u5ocs5lkMbzsawr7NWoYQctpCyDq/BIzka4dIMH3BOl6YPCdDhIqszD2ZKpcgIdGevyL7wWRYFqEU119/PVVV+T9IhUKhOBRIKdnQsYG50bmAZREayNy5cxk/fjydnZ2UlpbS0tLCvffemz2PsDKz3JqGa3QdTk81FeUn09aeyrzRmDrlZwBs3PRDrJ7r1jaPp4auLstVo+s6vmSbjUR7OKfPlEDQO3Yq/tX1iJ9fw/Yj72NsZSHP3/1nAGafdAqTZ3wZWo/g9y9dzCdK4pQHmqh3humrqKRo2VziXZuJvNuO/mYzApixeCTHnTuBV/+5jXee28Po6aWsGvUybIGTAmWcFNiDJl61Hn6pZ7aQVh8rwB2utNfnRNCrWTaAGSNeZwbvsC7p6sq0qFSFTVq8WupDGGjCYlxrJSQfDzsiz1E63mX1kkgSGhBHlDVHPktMUsR8vLaCEXEP/m7rAZlIuOgxU4Vunbiq5g4tIIYhWkyhs949lUQoV0Cc2LCeic270GMR0GP4/AGgL3eSPLw05kj7OoZskSENJouN+xyXSWG4j15vAWPjbeAC3REjQbKFh/VVzeLNN87C6+0lHC60W30MdI05RTynrpA3WfPHES9Ek1qGYM8QQjJuNYrVHEgJZsbJdQz2aKPyZ+7JGsuKKFLtNsBRfuiLK+63/FR1hBQKxeGkLdxGT38PRXHLHTXQIpRKKddNN2PHjiUQCFDh9ebEdggp8faFML75PZzVVtq7KWPJOS/k2IUvUlv7SWprP8mxC19k7hF/t7dBdsZYyvrkKPfmGpUETL3mCiJuL5M6d/HI965n9TNP0L67Ho+/gIXnfhqAyZXz2Rqdz66ohkM3GDFiI32jRhANG7ze1E/MlAR0wUeOrmTRpyah6RoTjrKUR9P2HnZ17yagm5wceNcOaM1NUkoGq0bSBRld/i4SJenYlUAyCPZE+RSf2PO4vX10KG0pGB3ZgSbTsSR14SieynSLpQ5KeGlEAx0yXZG6a2Atm2FkXCHgtajkuHHptPZEwk0s+bAWQvARx5E53en3FyFNZsV2M8XdlrNP6slaR4aBwxB0GftR/C9fdlnOCSSXJCs/T2qpH/bUxSFLjMWTlch1PW7HCOWzCMVifoLBalsEIS0hlClaQtKf7gGWJCItN5XDcA9qETL2pgPUDRzp+CBAw6BaNuXN3KuUVqydFAkQUHLWxEPuFrPWuJ+8+OKLJBK5NzmRSPDiiy8ekEV9EFAtNhSKDyYbOyy3mEAQCAQoLEx39B6YUr728Y30Pv88XZd/laNWrszwD0AwWMxXF13JxIstISKlSTBopdnX1pyNx5OuveLx1FBSMj9r28CMMQBHwE3JWRMzrDDWH3f/5DpKr/oRAIvffpwV99wFwIJzL8BbaAm6FZtb2bLlGJ7psR4iNbWb2ePs5a7rVrBra5CNycai/p1BEh3Wr/TKMUW4fQ5i4QQ9e2JUOGROdekskrtE3GfHUDkKWrKe0z1Y1zOJzVQbaWEwJkMITY5u5Ga+xBeaNwDgkoYtslaQbMaqXcs3xK2skFYmVidpUTR8BN/Z3ECPtB76sYQbKTVbCAHUxARnv/biexdDSdfXjERTVnf0FBVBa93CSOAwtZx2Ge8XfyTMCdKKlSmJ9gz7uNlh63OKCkskulwhWwi5tEjO+KrqjLgyCUcnJvBO8TTWcKS9+ZfiR8TJ7sXWFUpWZdcTaGigJStGZwimghHpytkxnBgZ0sKBQRkdnNrylN16Q5MGn4v9ya5ILU4fQfX3j8Y/L38F+IPNfrnGAJYsWUJTUxOVlZVZ24PBIEuWLMEwDm9hpAPF5ZdfzuWXX05PT0/WHzqFQnF4yQyUzrQG5Uspf/Hfe0i8fjWeaDeTJy5hXPRYHnK/QVwYmL4RlFZW43Fav/BDoW0kEkE0zUtBwdSc8w4kX8YYgH9eNe5JJSTawzjKvfYv3LpPnsmqp/5Dff27EI/gKa9m9kkfA6CtN8q3H3gHmQgQ3a6hxTUctQlGj9hN/5savoCDo786B56sJ7o9SNfD2yi/ZAaaJhg1tZRtb7dS0FzNVq/AUmEZYijLPWb9X/eYpzB6q6luOgFPf3WWRyaYFEJFBHHJ9COiNJZ+8HlFP2V0MjfYB9XQp1t1gzpESU4z1jv4EjNZTcjU4D10UjCA7aFedCAct6wfMZF+zpjSZNqGNwjsWsUdn7oif/f1IZGM6rTEYFzPXWA0+eAv1cvpkx00xQavxPxeCHm8RA0vHj1Ml69w3wckqY5Z1xlL9llzuqJDWoQmTnyDrs5aYjE/RdJDlWMMj42clmOZ65HZz7uwmSx6qcXR3D2IpBsrM1jaJWL26zjOrDgjHcvac17l7Yx+22BkDfhq/0WZq9P+b7W/IHxYLEEp9tsiJKXMKmueoqOjA7/fn+cIhUKhODDEm5vpfu1lqvqsAOLM+KDu1nBuXKzQCXsrEJ5i3NPOoxAvYw3rR9xCrYejy624lb6uCPVbXgIgUDQbTXOyL/JnjFk4Am4844uz/rgLIaj77lfZVW49aGKNcTrDBqYp+fYD79DeF2N0VT9HbwkQfstq/lk7YiOdgY1MvtRNxagiSs6cCA6N6LZu+ldb7TvqpluicFRwCrqzHL83IzNNapRv+STVq7+cHbskJL0z/kqvq5ONiV527phr7+rJEEKbfXX29lsnpK/lkaJzWMFSAjFLOPQ4dKo2XEyzHJGnxYSV3o3mSZ46+ZCUGYUOMz64gQ07daBW7wagN249lFMWIYnk7c6nCRu9lPV1U2DGyCHPObJIpsZLSV6LUCRZqXpa4VF8yXEFu3Z355/nvSIEe6X1PY47hm+bSCQfw9GkEBKCIYWQEBKv1woGL5cFNPi1/OUCBjzf/+OxLHr9dKEFGpBJ2ZAphIQEV9IiF8dlZ4wBtqtNCMkITwOjau60swtTp+rr2jns6z4YDPuup2pzCCG4+OKLcbvT/1EYhsG7777LwoULD/wKFQqFAuh+8EGafnw1Z0jJI2ecTsSbbRHy+PP8OZMm3nAbWmEVIvlHv86sYAtNNGjtzPZ72PBKIyvu2UT1vOcJjIF47+RhrSefa2xfvPzvB5BCUNETYt7O7dx0/V/R5s3nhS1tuB0aV446kt3rxrIzKhkd2ozXHyR+xF8ZX2u1c3CUeylaWkfPU/V0P7Id4dIYMcKyIlT21THBPRm31yAUhpL6kynZ9VG0aDHrfBvwDPz9qpl0eBt4M9oBe6cydtwqpBD0Ygk7A40HKz+WHp/xgJRC4w55GccbmwHodwhCHYs55o05kF0hAE1KqmginAzkvZLrccsolTTTH/wRPwiMynn4CmkghY4O/HLyKMpEPS2AIS3hGhMJHJVeyi+ZyUcS0+hubiRcWsmNGwc0fZWS2Q1beaduEnWdzTQVlRF3ugacy6Qo3Je0COURQkmt4MaJJjR27wlC1b6F8v6wV46igTp2Fo+y120FWZuAyBtjdPdY6zoipK8nFZuj56kFlEqZB3BJF2WhMEhPrhgaELC93TWBbYxnQsl6tNotSKyCm5lCKL5zHM4xJjE0Ejhtt5kuE+nQaglO9KwA+hTh/sMrhIZtEQoEAgQCAaSUFBYW2u8DgQDV1dV88Ytf5J577jmYa1UoFB9S4s3NNP34apCSfp+PiNeLME0qMtwg21flBroK06pqa/a12DExI8xSdKnRq0Xw+xO2O81bZgUMb1xRTF9XbozFQAZzjQ3GzjVvs2PVSjRdZ9o0K/bwlMdv518vbQLgtHEV7H6yK/kA1GjcaVXyP648TIWn2J6ncNEI9CIXMmLQec8mem5dQ12JgUBjXN8sQqEtABS0HsVDrnV8r+p29nRVIGX2w1SaGt39BUl3mUYs5qUfv5323M/QzT9NofNEeZVtaTlzkZ+nSsuzxmhSctX6MGVmt501VsteprGecjp4t/2dnIe8FBpf5SbuGh9h5YJpXFBbRiLeDYDfa8XrxEiQqLTcjoVl5YyaPovNRPNmnsWS3eh1wyTuGCBgpOSYHespTBY9HMo15pbWsa2e/U9x3xfbGc/tfCkruFpIg8/tuJ8TtqzJsKKlRUQqC2uPXmCnoQ+0CNnDpZZOmQc8uHDH+/how4vZFrhBsve2MgVZuwUEmElpkymEwg1TcZmW+IrhxExahAYKMhkuzmq9kiIe2u8onQPKsM9+5513AjBmzBi+/e1vKzeYQqE4ZITfecf+q95ZZv3RD3QHkY1NMGoU0XCCd5+36tQsvmAy7r4mXrlnLX1Fo9k45UKObPpP0qUvcaJTY5awR++gt60BKb3oniCuwjbrV3P7OIKtYQpKPIMtBxjaNTYQI5FgRTJd/oiPnkrtsrNZ98LrjOpr46vv/JNb5l5E8VvdZP427d4zh8iEV/B4QjQ23s+oUZ+15uqLY/RmuIAkzMZNq0hQ0VlBtDbZXb63ln+MuxWH4SYRKaH77YsoOfIeK1VZavSuvohoqAQCuwCIxbz0uq0YHJ8MMYIGRJ5u6imENLhvRKn94DSF4JZJaU9BacTkb6/3UxWVbB5XTtRn3c8CO/VcwxOM5xQ91KTJRDZzlC9MuceydsTj1r1OCOteSyH547oGZq0s5Lx5lvtOj7eAdGRbOKTEaVqiwNS0vA/5it50q4i4lvtIjKYsQsl4qRZPSgjkVEp4z+zRxubcZyl04pqLqc27GNXZwvaKEbw2YWbOsfXeKr4hb+NSbrVFSkoIdXXWsmfPdGb1zqXFbLSP0aQgYPqY2b6Zj436O+sjc9i+fR4eQvzftMU5pREmsiljk/UiK7ss0YXb0MFpxQi5knWFMlPxhQBNTxBoOIFg3YrU1AgB3Xva6e1op7AsW0gfKvZb2i5fvlyJIIVCccgwYzE6bvuT/b6jzLIKlHV24BptPQTXrthDLJygpNrHtONq8f7nXmZsvBNdJugqmYJ7/mcA8GnPUO78AetNK+tq2ybLeuJLWoOiwRGYCR+ByqFrmcTjcfr7rbTq4bjG3nnmcTob9+AtLGL+2edT32fyP0eejyE0Zvf18fmgTompZf3i1+J+GhpmALBr958wk/Ev+WoVaQj8usDdYz3UHZFS9iYkUUeEkDtI9dE7aN2xkLoXfkn5ym/if/lHdDTMYq8nWeRQQizmswOlA3RTRicXRe+y0+RFRkyPJg0+xqODV20GOt3p17HwGHsOL/2ARrf2VVy9GosyLB5CmnzaeJgyOjHNdH3jeKIbgLcbTPvaG4hz1UPraApan+XcsjqKuu6EjLR+VyKOlmrKGo/Yr22kJJBR/TmRxyKUSLbucOMkikmH23p/zbjarBICw2VcS25hyR5RnBMbpUmD4oj1eRbEIoxv2ztoe49UUcjU55cSQobhJBis5mWjMWu8jobLFIT1GGV0Ms98kwltexnX05iVDQZQEu+ihC77q2mmnV32GCF1hJGMEZIuErZFKD2XlIJdEZNNzVZhzEibi1ivJS5bN63mz5d/7rA1Xh2WRWju3Lk8++yzlJSUcMQRR+QNlk4xsMuzQqFQvFeklDQvv4bIhg0IjwcjGkkLod5eNI+HeNTgnWcbADjylDHEGxvpefIpfKbJ0QsdNL4tcGhlQIiA806ECNEU/jIB51aiWhB3EXgrLCEUbp9AQYkbf/HQGSy9vVbQqcPhoGN3BLNKG9SCFO7t4bUHrGKOx553IR5/AWPLdbaV1vGvOZdQWjgTp6YTlBHm7niC7eNOB6GjG15amsczavQ7QDNNzQ8zova8dK2ijGeiiUlQRvG7rAeeu28Eb7p32ft/In7Dj8bAu31XsK6/HSnaoQQ7i2ySUUN1uI5tyQdXUbKW0DLXYxzFa7TIGquDuQRjyzfQJ/0GgCfkGemO8gMRggafRlXUoDdpafHSj4akcsQ3+c3fXMx1dNkWj6C3gJN6a/Adsx50soVQ0jXWF/dTjoMYCVpEHENq1Lf3UxPwUu2v5oZZS1j+2pUcsfcUnp6/DCkECc16KPtiMRZtXsOLk+fYa3bHY3YvMMgfI2QIDSEFTnT+bawATsejCb4YiFHR9y3+XXAST4lT89+DPOyszG1R1aqVciJP8ywfBdJtLQoTpl1/qTAa4Tvr+/if6QV577kpdLqllUSQanlhylT2XvZYDQ1TGkSTPwiEkCBNXO4QAwthdblK+Ya8jc8bf+JExzN2sHRWHSHhICrDgIu1m5ZQonXAlLRFSEos11zcT0+JgxogHnbgdCTrQWkSKSXP/Pn3jJk995BbhoYlhM444ww7OPoTn/jEwVyPQqH4kNMcamZ3z27qiupwPfgUwYcfBk1j5O9/zyWbr2FyQwkaUNbUROtNN9Ny/OeJ9MUpKvcw8ahKWn9xA5gm/oULmbTAQ8kG60G3I9qHFON42vV5juurptvdRMLZx6yzijBpIdQP0a4J9HVG2bW2gzGzBv9jnHKLyYiTR25egxCw+MIpTDu2Nmfsqw/8nUioj/K6Mcw8cRkAlQVuflhdTaTLiwQqW9/muPoHcPX3UhcIEvjZb8AT4893rmVPwzQmjF/Frvpbqak+265V1PXQVlsMxUWCpsKdzAxY1gZX3wieq0z/ujaF4I7iShaE96QfihnPu616ExMjZfRgWZ2K6LHHlNGZ7l4ugInLAdA1H1etjfKz6e5B+2ONCpvE3Z10BDoA8GNZA1r33swx3k8Qj1vehYJYhIJohNnRyeyUljssUwilYoQccT9u6SAmEnQTRxcexpT77HFnTTyLxrdNdpuWpS+uO+xMMIdpMKVlF6O6WthVWsWLk49AH2CFieUTQpqOGwcCQchXDECN24no2oFmajzNx3KOGYp89yohnESShREn7djIN103UTSig7Wm1dJKSDguMZXJe+GE9hDvBnSumuMZ4FI08GFZKdMxQqlK4OQU+nRpXhLuZE0gIamq3cHEia8jxRfzrFnjTscXmM3bdmXpzBihqKsSPZk1FjV89ISt8+pSgoDNmxbS1jbeWmcy5so0NaSZnCu1TNOku7nxgymEli9fnve1QqH48NEUDLOzPcTYcj81gQNbDv+OtXfwm1W/QSKZswN+8ICBAKq+912MeTPYuyHMVBy4HA4Ke3rpeOAhVvctBmDustHIvl66H/wnAKWf/zy9z27HKcbQZ5isC5eyNnwN/D/2zjs8jup6/58723e1WvVqyZJlyb03bIOx6c0U00sIhC8J+ZEQCJCEJAQIkJAChJaEQBISQg+mmF4MGGzj3ntR73Ulbd+d+/tjtmrlBjYt+z6PH692Z+7cmdmdeeec97wH7Z6Q7iimy72DvTVbycvfBsDQEcfQU+1n1evVZA41RFt0DEx/tbdoN3YlpD0gSgkfPrWd0tFZCZGhjvpaNrz7JgDzvn01ik6Hp9/P23/bjHenEwQUjzdT9ci/EWGjWrFlNbZN75O+YAFCJ2htrqK4fCN462hte43CgrNjXkXtbppe3oypw8gxShGBjEXahp1DMPj9YI85Rg/xFtNnMuO0al3U4yMhUoDPb6UX7XxGIkKD3UBjnkSCsxsDbHQIXi1JjqAZBUy4YRodDUtwtYYJD5FeXipWax9OZ2K/NKfiQa9qRCiUkBrT5nP28Ap2dGiieL0I8ZtzxiZ8B51OJx3bN2MIR4EQAm+4Skwf9riz+b0UObXzF1T0zPWP5kPDVrYVDsU/UEwNdFntUaG0z6rdoAtNRsiqoIXCfQvK99UuY5D2JAjBGjQB/bC6nWRW9hAC8tr6mMVkHKoFG9r3Kt8nObEtiGuLj7vGmKLC6qt4lG60iJAu6ry979SdEAK/ohFegUpl5af7NcCOtMSI7G98Cs2mONCHxdIhRYcqIkRIBQFWay9Gowu/3xadkwyJOCIUJmSKQkZB8sPEkcZnlmr7/X7a2tpQB7h5lpaW7mONFFJI4euO51bVccvCTagSFAG/XTAuKlY9FMRHfdwBN+/Vvceb1W+yu2c3Wb2SMbWSq95RESoETzkJ6+wz+Xjn8ljH+dJSMs46k62runC7VGwZJkYeVUj3E39Hut2YqqrQDxlLz2taimBjfyuqakIoWvmwRHLU8VN4Y9EO2ttXkpsXxGQqYPJx09iyeDm1bTv50/1vINF80+bPn8/kyTGvnbpdWmd0RY0RAKlCe31flAj1drTz1p/vR6oqw8dUUlqSw5aaHp7773YsjR6yTTpOvGoMQ/KC7H4w7oYlJc2/ug3b0UeTlZlFZ0cnKzrtzM3tpqbmLxTkn4kQCnqHCb3DxPaZHYxdlEWVmskuRyMScDuLmNMyjfrMbbhMGonYkF9MQ/7J0a7sc3auZ1SLlj4TEtK8uTjDESF7lAiJQcudAUIhF01ZfSwaUjj45xLaTIKljuFsaJ0UPu6Eu9T34PYkmwc6VAtu1UCIwVNjY9Py2IvW480kgpw1MTHN1NWlRa4iN2UAT4QIhUXTSNCFPw8qCt0ZxXQqzSypmjgocWlLz6TTpMfl9+KyaUSjyGQARzFpIV+yoFyqnLB1NfpQkLfGzRx0zIhAXEiVnL4e2tOz8AotspXd3Yaq0wMhfL4+CvWOqPVDPM5uDPCfMgM1aTq+xyMcw0e8wEXavoaJUF5eDU5nAa0tlUnrSyTNprCfjxI6YIszIUPkEbMniE+NDRO5pIU0shhUlGjqLlI1Vjp0MyWlm9m18yjM/do2VVUQCcgJRSIUhROv/sGXIpg+ZCK0c+dOrrrqKpYtW5bwfsRo8ZviLJ1CCikkotnpiZIgAFXCzxduZk5V7iFFhhbuWsjty25HDlJzM2+DyvfeVCMu/rhGzibPvICOxzczAgvj0kcC0G/pJ/vHN1H7s48AGJHdjiKDdP/7SQDsp19Dx183AjqklOj96/H1b0JvPQG9aRwCQaEtG7vdjtW6CQCHYzLWdBOVszJZsu3D6JyklCxatIiKigocDgc+T5DaXS2gA10o0ZPmo6d3YrEbaa9ewTt/eygsfpYUtL3DA0/auGf0ucgpZsRkE3cU5FM+OgfXpyuSzf5UFX9tHbk5uXR2dLKjo5DZuf243btpb3+HvLxTootuNe6hx9rMrGAJ0tgPUlDiK6bbqCPdk4fL5EQqmTTkn5/Qlf2jqglk9fdQ0Ofk6OAorN4gveGIjT4QAAPRdgr7Cm7szmtDisGf4EPAlOVbtbMstPRRjRjOj+Sj3FrQRsiwE/zhlBuCbP1IbJjRhwYjQmFi1m2MVm/pCbG7rZ+xxbFoncFgiAax9KEgQZ0ejzHSKyuEKqEqWMUmtUY7zDod359mAzln3/3AhKDVauQZ/1Lq02YDGhEKOn3Yg4L/46/8XX4PVegQUmXOzg0M72jiw+ETBh9TCI7fshJLwI/D08/2wjLa02N92LK620BoRMYX9LCq522m5pyMIhSNmMeF6MpdKjVpOtxYw8c8sXxeiERH6XhstuymVxc7xoOe44Q3BRuZHP9h9JVVWrGGozshRYe5T/ND0imx6kYhoLJqBa3rNI2fVGMRodITz2bW5G99aVVjh0yErrzySvR6Pa+99hqFhYX7FU6nkEIK3xxUt7uiJCiCkJRRserBoMXVMigJml4wnXmm8Uy6569REiTMGeSP+BYiKs5UyPHm4FTcvNLxClO2HYfXlIXB34vjv3+k3VBDsL0dfWE5vupYp3MhBFNzTqbFU43H/R46QxkodjLzbYwYMQKP92UAMhxaz6XCsRbYnjhvKSVdXV2kyz4W/2MXvoBbI0JqzNXXZNPj6vHx4j0f4e15jNiNQvCmewKPjj43gYjc0drGGcNyyC0bqrWFiI+uKwrGoaVkOXsAMPocfNSrcEI61NT8mdzck6PX3preGlZltjDZrTkAG9z56KSJCRZJoPkizvK+yAxLO1cPGRBVEAovT5nHzY40Hnl+N52+XgibKQpfiPiWU8GgAb0+kHSj1Jf8GUU+mijejbt5DhZLkkLhrtYCzjc0kkkXm4L5HDNrFv71WppGHzTgA9SQNzxciGA4NSbbDRjDty0DIbY290aJ0Nq1a1m0aFGUJhgiREivkdWWPh0zM47l2FYdq/SJVVT77RAfqSwTsNemEYycVi8tj68kb4yRubzPONbh3HodO5yNpPm99BvNbC8qG2TvtfEKertI83kZFyqlwlnE6vBHdr8Ts9+DjCOg1f0bafFUk2bIYO2IXm7qvSY61BC3tlwbWo+u4CCGihFH6YFEaLulBnPQBjgRAnbtOoqqqk8HnzNhnZCM0w+pIlZ3rioo0dSYAkq4EewAh2shJAaLFqWVqohqmMx5BV8aCYLPQITWr1/PmjVrGDly5JGYTwoppPAVxeYmZ9J7OiESxKoHwn+2/mfQSNA147/H0Gc+oTPuIyUtPyEl4CeIU2hiUK8MsOENLUw/zL8Z4eyk86+PAgJd0byk8RWhkGbIwBPqIxTqJjh5CGmZZqqqKqmu0TQn6WEipBiS5yeEIKvpAzb+5X329l6Fmq3dpE+8bBLZtiIcZicGTy0fvG9n55oaBlKAbkd2kkg2BGzoc3NqQQGFv76D5l/dppEhRaHw13dgKCggq0ETP+eTzwd9Csc5DPT1b6Gz80NycrT9rO2tJV1nxG9vBLSKMW2fBRmkM6ovwJRQ3aA3egnc6+zne6dX8NjyGuzheeekZ6KoFlS0qiKPx0FHeynlw9YmDJEtOrlK/oW/832NDEmVo/Zu5dOKsUnHMB4q0CNCZAIdahp9qgGXVAEFXUC7LUUiQsFgX+x4tioYwhEhowixrVkjT06nk0WLFkVNMwEMwSAeI1En6QmduzhTVCCEoN80CHHfB5lL98T0VJH10j5tAQlKOCKYTRcje/w0+r2a1smatk9Sdc2Ofk7rHx3V/Wzvjy2X69NSrkIJ/0rCERNPqA9XqJc1GW1EdOwAxW6NPLeGiVB/2LQyvnFqvKN0BD4CqKhkdqlQDiDp7hoksjdgH1QRsxeQcUSozd0A4Yq1kKIQDDd9TXK4lgIRNANOZEigqtoAIfUwd7I9RBwyERo9ejQdHR1HYi5fKTzyyCM88sgjqVRfCimgpcUeWqwJb+Mrt//vmPKDjga9tvc1/rX1X0nvZ7oEmbc/SudHiel2JSex1UW70gsCjFLPRRt/TiA8i/RjZsHqp0DRY558JYYh05K2oUqV/kAPAsmajA6On5HHJ9195GT40esDhEJ6+vschIJdLPzvf7WV4m6GuVkZeN/4C8t679Lmpu8DdOR3Libfr4e3fw5S5WQp6GIuAxo9kOHsHpSI3LSjHqtOx7HnnYft6KPx19ZhHFqKoUC7sWVHrAJkNi5VsNabzlRTJzU1j5CdPZegDNLQ10CGMQ2/SbvZmfq1vlUq0IBKgeiiyN+OKeDDZ0wu8Q8Bfwm6kNNy6QoTib/KM5BKN8eorwCa2WJj4xh8PhujRn+csP5c8T76rXYaAuU4wqRhZdko1EE8eSIQUiXHpaXFguhwegJ0qSFAQRd2MIz4JkX0QTrFiuyTGMP9uIzEiFBXV1cCCQItIpRwDjCjCx//THdf0vmI6HbMfi+nblpOY2YeK4eNwRSMjeMyhQXLnrCfUpxGzCzNVIYK2aVvxuHuTzKKREqOWvsh47e1U5R3anij8G6eMTqXvWlVbBw5hQnKDgQyzrtHsnxcFz3WOIG7lAzxRIhQPh9yPB8zF4C3OIMS6jlWvk9ra3lSNGi9voajnWMZ1ueEKW+gKCGmz1jIhxyfeKIGHCNFhqJkSKqxh5Q+f0/YxR26rXbsJo3Y+DCGNWGRFKjEGBakS1XEyvu/ZCJ0yIaKv/vd7/jJT37Chx9+SGdnJ729vQn/vim49tpr2bp1K6tWrfqyp5JCCl8o+ru9NOzojraZkFLy84Wb6PMGmViSwcc/nccJo7TGpevqepJuQION817te/zyk18CMM80nrG1kOVUOXqr5JF/6gl8tAwMBtJOOEFLC1WegnlkojdLq+ih32jGm17G6FwTaeGr1+r14LMXY519I4Yh05BqEH/tJ1FjPVWqrO54G0+ojxMLd9E7LI1bejo5b/0ejtsS5EOOp7c3hyVLlvLEP/+JLxBA8bqw7t2MuXEvSElbZzcv9V2LioEy0zL84ZtB+tI74a2fEVF9burJp9kVuXHGtBVtJafG0i9oF948o57OQIgLN+zhtt2NqHl52GZMj5IggKyscOsEVxCbzsarHW4QBpy966ir/wc1nesIyiAui49gifaAauovBgFvFhtpR1IguujUO2IkaJDzFX0nmtIS/E29NNq6Yai7HKNOT29vbtLqUgoMvQrFzo5o5MSgDvIAGV4xoqOJtrWQClubeqkIy1V0Xo3o+No0UXSkYkyvaGk7c9jKRYsI9SGljB6neEQcpSPowkcoPIf6rPykuY1s0vpdGYNB8vud5PVp2w+F27ioKHjCxzDfq51vJU4jpqoKrj7NzyrN700wikSqzFnxLsesep/q/o3Ic+zkXD0O9ceT+PdwY0JrjXfmnEWbMaajQcnhv/Ma2VXSn1DB1+hXMYWPWRv5SS06/s736BJZ5OdXYzS6EvdXwFL9doZnzgovLukSWdoY+4AiQ1zO47FDFkeE9IbxdIRdwLcXlvHO5OEANIlSfsSjMYIlQDW5YvsW0RUNOFdfNA45InTCCScAcPzxicwxJZZOIYWvPyINSCMPgnMvG8lWs8oHO9ox6hX+eP54hmRaufPssSzZ9SEra7r4eFcHc6py9zkOApZUvEAoN8SPm8Zz1JPrQVXjKrM9mEaPoui392AeUUX3q1txLdPKm9NPKMU6rYBgh4c3V3Xyct54pBA8LiU/2+xl2G4vqgTHrOtRM0J4jesILX0bUb0XU/7TvN99NK5QL+6w2NaSrbBi1PTojV9Fu2Hc7HoA516tgSjBAJa6XSihIDq/j6AtnWBGDp02JwUBJ5MdT7KKizHgx2zPhT4tfdXoTuf9Fs0rZdqxR7NpwyhCwR7QZ/DxJC1ddUX9IuY7P6F8wX1kFI7mjt2N/Kupk0fr21na3c+fRw+lyhaL2tjtdvR6PcFgkNMKTuOFxhfolA6y6WD37t8Aghk2A72moXjYCkDmSRNQ08dzzyOfAJAvutlg16QMDncfjp53qCs8G8S+IzYQK5fOpgudLwN/MAghG7t2HUVl5QqEkEgpEnpY5eTk0NHRgUlARIY71GzghoIMPnrxeYI6QzRqFPmOBdDR3tzHt8JpHRGuPvLWdRJ0+ggENUJCSE/A1IUl3QbdYBIhnJ4AzU4vRRkOTjzxRN59993o/A0D7kXPnXIeXVv6ub5DDFohtq2oHIBei41tBUPJcGvC8ZCiozatmultYwAwqpLcQjuBuj50ahwRkuAMdQPabyFmFGkjt2YH2S010WV3bKkj48wh1Cm6JLNDqSi06grIp11jqIoBt0Xbl5N6Yo3Ni40Kan8InZQERaJoP+H8ia5BNUJSgEtokRghoIWiZCuAuGP0W24gh06e4HvhAWKLdZts1GVmDbpepEHvONaRLbsJ+DVyKEMggtp+ueq2Q3Ig9wvDIROhDz744EjMI4UUUviSUbOpgw+ejKmEpYQP/7OdJ3O0i+UNJ1QxPE9LvxQ6LFw2Yyj/WFrNve/s4JjKnKh4t7fTkzAOEo7efT6ji9M56sk3oqJgnykDjyWXgvlzKf/JDxEGA73v1cZI0ElDST9OK81fsaOTl/OGRlMNqhDcM9bMos4Q+T5JT/F6Wkc/oZV6z4ChK2FnvZl2bz1VM2bjbG+lde9uPhUjky72qtBR5x9GtPZIp0+4kJs6mgg6cgiYuunXl9Attad1R5oN8X/vwJ/G0uvX82rDKFQUqtI7OOaSy8iZoPDhU9vZWGKkw6EnDcEtum04ulbAwsvhux/yuxElHJedzg3b69jc7+Hk1Tu4fXgxlxdlI4RAURSysrJoa2vj6KyjeaflOTJlR1xkQHJhpp860YOqahGWLa3fxxC4BUk+RgLkiF7W2EcDkNfXg9K3hAyxgZ6CO/ZdKYUWAcgPJ/l8AXN0m60tlXR3FWGx9OHx2BNusBHZhCHOqLDMYuai8mL8edns3btXO+ZSI6F6JAEUylBQIkZ9ajh1ogQIdnho97+tzUE2sHfOjZi6vgXdkG4A/LCtuZeiDAszZ85MIEJigLULQvDumDRGbGpCirSkz+JfL6mawMmbVgBaRGinYzcl3VqqtsAvkb5wRCiOCEkpKHGMZ3tcYjTN78Xm85LW0ZiwuQ1v/Y2Nbz/GxO/+CEGifkyoKgWyJTqmDDZTWZ9Gd4GeHzVfEjdNQYmU5HqDtFgMSWX8kfM3mEYIwpYJqplIrKiApqQxIm1VVEWHCX+C8s1i7ou+brYa9vtdipCyzA47Ug0L3+Oqxvq2rfxSe40dMhE69thjj8Q8UkghhS8BUkoadnSz9q1aGrZ3D/I5ZPZL8iocXH1MecJn359bwTMr69jQ4OS9bW2cMCqP2s2dfPD0tqRxFBSKPz2VrVVZZPTsxG9IY++ws0AoiAaY92ETeV1ePOvaAEg/pQxlQi5717exbVkz77e1I49NvEiq4RYOWbTHSJC2MWpnSPbs1Z5Qx8w9gebdO2jdu5tQq2fQG4axUwcRYacQqEYzSlAjgErAj8HZQyAjg/60elrzT4WOEOl5JeAoJnDKvbz653/jDhnJNbk45arvIjKGMHo2FI3K5J+b90AoyA/LC3BMfxgenQNde+CVa+GCf3NyjoMPpo3kum11fNTdx093NvBBVy/3jigl26iPEiFbwMaM7HIUkXh8FQFl7I0/MgS6fku2+TYsPm0f3rfPBCC3r5txLXP50NhNz37M/hQZ4ir5KNlC03akeXNB9kbJkN9vS4owxEMXilWdOQxa5MliiWnJXveNZL5ZI8tBqaMBlRASHQIlSoT8hNKdNG16ITawkPiy/oPReDa2cAPWbc29HD8qH683pp+p7gKXMnjp+nZdI0IW7dMNG7QohiecggsqOvx6P93pWqQuz60SbAu3pohPjaHgiisXj9tkwvcpug0pWfe3B/j2/Ov5V2EGUuhQZIgTl7xK9lHaw4BUtWL5mZuy2G6zogxQs7wyxEiLWbuNS23QsMGiylU8SpbsTojYxTYOs4MjearwTU4Iv5VN16BWAGsqKulX0nBjjTqDA+ji/KXyXP59V92hEap8mkFnQehkdN9kJBymfDmO0hEcMhFasmTJfj+fM2fOZ55MCimkcOTQ3+2lp81DRp4Fq8PE3nXtrH27lva68JNdvAo6Dqd6DJRaHPhdQfTpsQt/rt3ElbPL+POHe3j81e243m6kcUcPQJLfCYDPBy0FM2gpmJHwfolBkPZeLZ7wRbQ/08S6TZ3UPbsrukx6Rg9CJj45K1JS4lYJWFuTTf+EQKap2PRZlE2YjDnNzqcvPktfh2B29zo+yZoSXXRCcB0WT6KhoeL3ctR532bdOyr+/kUYO+oIOBwETN3U6XOADtLT05FS8s6KTlq9aZitFs765d0YKiZEh3rD56Y+FCTLoOP/huSAXgcX/Av+cQpsexU+/TPMvJZ8k4FnJgzjsYZ27t7TzFsdvazt3c5Do4ZGBdOdnZ3MGXYeauOdxN/jB7//qPzi5HSeXbSbLtLZaR+qnbO+HgSC6a3F7Consb2mVLlB/hYrPnJlC6I7g7BEiDR/HieNn8g7m/Z//Y9AFydUztBrRKizJ1Z1qI+bbwAd7Uh+j5efYo5FhPJ0+PRNJH0phYrF0kfIp+nUtoYF05EmuH6pwyn0dGYO0AFBtHR9zs71fLQPA0XQhNMOt9aMVVUUnjr5KX77iuZZle+R0SlF5grQJvsIDqyS0jaJEufiPfDDylVvcnPmTgzHQFpLD13b02GWdmb05iD+XiMKAp+7BxU1SoZaTYLfhJ2ltcnEmvb+mHuYzBpWrFgwKGGdFxjLX5Ummo1ro0QIiFoBtMpCmtZOxO73sLMqj37ScGNDEl8oFTsvRp+LirYG9uSXxHY6PC9FSk6vXk12eRfSoaBzalWmmrN0bOpfhqN0BIdMhObOnZv0XryXUEojlEIKXz0kaHYAi92Ap097QtUbFEYdXcTE40to2NHNh09tR6papUqHIslTFRpWtfPkxi4mnlDCpBNL8XuC9LR5WDA0h0ZvA1U1IRrpISSCbCr4iD5TN7NrFmgJDwFTx/gJPPMY3enDac8Zj9umuRGbBUy06BKuIdYuL201/Qnzt4S6GdVUzdbiYdH3bt7mI98nCZCf1ApCquDrNTLx+HkoOh35FcPx6szIkJdqs6bXGWNoZ0sglyZDJYhGCOscjU01GIVgxllnkVnUweJ/G/A5n8HQ00EgM5fqFu1m4HA4WL1oIduXfoRQFM686VYcFeOjc/CrKvfVaCmOH5bmYwsTAoZMhZN/A2/eDO/+CoqnQOlRKELwvZI8Zmek8f2ttexy+7hwwx7OsWaTIxS6uro496QLuXnbA5yV3ositBTTW10WTsv2kkgYFE6bNI0pvm7+tPoK3CYLSJWcfo2M2Hwefppt4fedHkJoPFK3xclznWeRa+mgzZ3DSWUfcFLWHgCKvjuX4TlD2NSxi+bmuJq4wVpwkOjs7NDreG5VHTvr20kPBzTs4fJqVUIoPMDrBGhG5e4wufAaglgtZcmDo+Dx2JEhP1b8bGvWiHx9uxbR9Eo9FrN/UJJjqnZi9XkZ1VLL3pxC6rMLkpZBSmbs3Uy6T4v6BBWFZx57hvZho4CYUBpAqLFb6HLDboKBWMsVET4/BVWT8Gxfzb7Q07yddGMfw2jAJSwwQiXco5bKs2up/6iQrh0Z9Ci9PFD4NNc1X4wOHbVWkdx8Nfx3MZpubTASJCQUqA5cwR2kuZMpQKS33KhJW2hoGIWNqQC4sMV1nk90ll5v3cyQnjL25JeQ7+ykvLWPT6vKKG9r4eGtaWzIX6odE52Ko0z7bcenxqyVY79ePkLd3Ynh80AgwLp167j11lu5++67D9vEUkghhcOD/m5vAgkC8PQFMFp0jD+uhPFzh2Cxa5Ge0TkWSkZlcut/NvBeQyflJQ4eOWEUK16ppq2ml9Wv17DhvXoCvtiNrir8hFqT3sfSivvoM2vplJqsjTi8udxhOwH7vX8GVaX82JGYvzWbZx7QUjljLEqSKasiBDadwBuMTThg6EMX9imJYNTkQmioQ+/LQAnYUCOVMVLSsKSQgMvAmGO1592WvgA15lJyDd00WgswEOJa/0+4iYdpFQ6qzh3LTNtkmjeuZfnWVRSNnYDeaGT07CJKR5/G3jd6eO+tVwlkZBPpENmycztNi18HYN4V36VkzPiE+T3d3EWDN0C+Uc+3iwdc5KdfDfWfwuYX4YUr4HsfQ5omsh1rt/L21BFRIfVLfoXsSXNY0LQLi97CkOILuWPnkxyTN4KavW6qWmaws2B3VMAMCqNG3s3Wrc38brvKR5MXhDcq2J03hFEttQghuGJoAedXWaj2+Hho0TY+bXSjtxewo1s7zp6gObqesGnnqKSkJEqEsnPzMRhttDTGp+U06OIeiINuD7cs3MbFxlhqyC7CHkHoiGdSawnRFwqXx4f8mM2F6PXpBIPhimSpYDBcid/vBwKcZ9rA8u4y3P4gta3hKjMpqGrfwQdSjZ4rQGMldR42B8uZoK/GFBq8Uim/p5OJDXuirTlURYcqJR1mTWeT5419L/1xWigpRXRXJs84Cr2jkKqhBVQOyWeVPciSp/456PZAi44A9FrtlMxpji/+omROM70NVtozfdRalrHWtpXXjnmFmkc3IKR10BRfpKdbpL9XbENa81YbZlpNeoyWfVdrCQFDhmzDGk6HubERf67ie40ZTe4o+dWrarRiz+ZzkYWP0hFLE/YJQNGrBAx2oAd93pcXDYLPUD7vcDgS/uXk5HDiiSfy+9//np/85CdHYo4ppJDC50BPm2ewimlO/M4YZswfFiVBESyu6+Llpk58BsEfzh9P6ahszvvpFE6+eiz2LHMCCYrgzcwg/zW66DfHHpRcJicVDbtwhEmQ49wFFN51J1mjykibncsYs8IQ4yCVSwLccRbWEpWgoZ+WdC1FlBmOrKzNM1Dws+mYvkWMBAFKj6BzRyaFw0eQPUQL1X+ys50aayl7hlYBMFJuwkEvR6OlO55pa6ew0Ez7Lk1/Uzo2lt5KyzQzPq+WM/M2oHPHtrOjqQW/I4fiUWOYeNLpCbvgCan8qaYVgB8NzceqG6QaZ/6DkDMC+prhxe9AXBTFqlP43YgS/jWunEy9Qqc9g38On8Q/69s4r/I8nCGF9+sbqGqZgUDQ2lLJyhXnsGnjSYwf9zo228k88/a7idVRYRFwn9HM/PnzcTgcFJmNzM60R6ODbX2xlguFtpboGVi2fC5NTc+HCYiGjrZWmhuSSRCACMRIz9alS6hS2jCI2I0zPUyEAoPcgnap4e+Ez4OnszVKgorWXs+QHffzwQexsRUBM/U1rNvThMOgja+GJOmuXo5ZERNOIyW5G6oRPhUZyuci32x0xsRKxwjyXFrUTBcntg4JJWqmWBAXEXLHOSdHO70D61Z+SqElROUQLT037cxzmXHOBYNuD0ANauua9H6SCrcUmHTBcZw24RwAOgw91LR7MLklp69yoQz4ccd3oZ8+YyH5BbEUs1FVGBHSSEe3LIG09H3OCbSvjghHjdwDIkLxAcjpMxaSk65ZBwR0OkLhfRAySMDaGiboidCZQ6hKJIL25ZbPHzIR2hdyc3PZsWPH4RouhRRSOEzIyEs2PBQK5AxJS3q/rc/Lba9uAeC64yoZWRBuuSAEw6fkMfdbI5LWATh9UjEy6EDxa1qUrF7JxR+p/GBRCFSVjPPPo/DOOxGKQkOXi64trQw3aze891Q/oajoAqxnO5h0sRuDTYsshYwuAjpBh12r67o4V4tYfNjZi95hotugkZn09IkABO0CFMmYuVo06LlVdfxs4SbqLCXsGaqVkU8KNzU4jncAWMM0qntqqN+q9R0rHRcjQgA0riHX7idkiztmQuArHErDnt30d3UmLP5kUwct/gDFJgOXFmUPeswwpcEF/waDDaqXwAe/SVokIqQu7W4nqNNzy+4m7mpQqMw/CaGfiMsYO7d+v40G91A+ahdsb+ugx2JLihZIofCGcSKTJk1KeL/L5Uv4O9PUzdT89XHvqGzb/gu2bYuZXgqx70Kh+NSYKRhgur4u4XO70FJjQakj0xrT2Vx3/HCml2vkIeT3UP93zdDRSAH2jol4LelJvlWKgPfX7cHr0VJZLmFGRWD1uhOWc+s0bUoDKr0mMzscySXnQLQlhy5uH0KKLkqE8nyx7dtCseMvB9TBL1q0CKczpos6+qLLmXPplYNuUw2GK+biTBRj4ypMn/ttSu1aBaXN5+Cjp7R77aRqP//9sJ9JofWxOdEfc7ASWq+xiI+QX1FxoR37j24+h/LMkkHnE79Pap8WGXRhRYYpg5AJ6jKEgPLCjYAmLg+FW4QYFR0Gd37SsQEI9BtQw+/Lr5uP0MaNGxP+llLS3NzMPffcw4QJE/axVgoppPBlwWjRozcqBMP+HUKBuZeOjHZJj0BKya0vb6bHHWBMUTrXzK1IGiurwBbvCxgd7/xjy3l8z1Z8hjqO26ByzZtqVL9smTqVgjvuQCgK3S4fS/68lvPRngR/j4dXlQB/p49bZxZSPnwzO1rvAVQqTlcoyPg5Xb0j+Nf6TKRQyBIK/3huM8wpYGOfh3+v2Et54A0AyjPPYlPnOjBAWm6QEbOOiTaKlYDXZKE3bFY4Sa4FASXUUyW3sVOM4rkGHXaXC6PFSn758NgOqiFoWk8XGSQJYoQgpDcmVLy4giEerNWq335cVoBJ2c/zZt5IOPNBePEq+PiPUDIdqk5OWKTAbOSK1t2829nC6uHjeKujF0yXQamgtkQyqW4nRc4OqrML2VpUzn9aXSjA9LSMZCW1lLj7JL2eII44AtLtClfIhXUtedZ2kouuVMzmXny+A7dU0YfiiZA/aaxIamx4YSbb/Ra63dr2zxyWC8+6IQdUJYAvTdO6mD0aCcgpzEPUiQQypEpYtLGNPYYWynXQL8xsGX8W7xwV1yBUCFzj8jF1tdDuU7nfFtwni9udP4RiZwcjW2qj7/kNBrzh5q3xGiGzjBEhg8GD3x8jypH+dA5H1JiBaWeey8jZx/LpwmfZ+N5bsWXDIRS9Lkj93qGUVmjblqqgbdsVmI8vjBYfOLyJppaZHsnRwdWs000EII1EfV1CrzEBTsWDTTVT6LBEDSYHg5QC37bTqbLmswnCYukYYRt4+EyRdKdOT8RrUaBi8GWxZ+dMKqqWJawT8Ojxytbwtr5mEaGJE7WniYkTJ0Zfn3baafj9fv7+978fiTmmkEIKnwPbljUT9KvYs82cef1ELr97FqNnJ+fkX9vYzNtbWtErgj+ePwHDwHQOWppo7mUjo+H7CKnKybcydtQ6svtCfC+OBAF41q4l2NbGxvpuXvrdcub0a0Ls3+LhVQJU6tqZZ1rP6i3/pbblN8RqmVRaen5DS/tmWhxaVMXV0ENxVx2KOwCK4K/rPsbvb0cvzGT95zpCXdqz3dDRRsy2NN7e3BJtFKvmmJGKQnZXK1ndsQjOcbwHwItOTXxaMmYcSnx7iI5d4O8jS+9PbjItJbqgP6Hi5R+NHXQEgpRZjFxQkOx4nIRx58G0q7XXC78L3bVJi2RnZTGhcQ+3GCORm1i6a93QEbw+frYmJI/4LAErho0mr7crOoaCxL6zF+FTaeuLVTH5giH6fdqN6NYzRqMTgjZ3bvRpnbgRvN7EVIoqSWrEC4nRFFMwkBTlMAnt8zSrmW5XLN1WqMYMFaUugC9NS7cYezSBe1ZJDvPnz48uLyXsCWVzumkb5TotLevAi37CdORAAioEqlX7fix1eZNSSvHLLamagMtojmqdTrz0cgDMQUl6XBV8Y/bK6OvpM15KSEMJIQZ1vLZn53Di1T+goDIWXVXDGiGzzkvJsNrovjWvHkPXlqPYsqmNTR1atNJpbkeN0+f4pWRsMJaiVAhFHcG1ceJ8hCQ4VI28tbha2NSxedBDICVUV0/E2HwUhQHtgSmRCKlJ59Qow+lOnQ5VibiIa8evpaWCXTuPSlxBFdFIsF/t48vEIROh6upq9u7dS3V1NdXV1dTW1uJ2u1m2bFmqEWsKKXzFoIZUNryv3UwmnzyUkpFZSZEggC1NTm5ZqEV7f3DccEYV7ls7MHp2EZffPYuzb5gUJVU93h629r9LYbeMdo+PTULlnVdWseOR9Zzk16EiWTU6nbdEECt+ZulrUISksHDnoGXgDQ1raQ4ToVlNq1jQ8iqj6rSbgr1Au/jmNveghqCzWbvgZ5m38NN/vsnti7ZGRwrlavtdUbsDLNokq6pu5/oZd5Kp19GuGKgpqUzQBwHQuAYAR3El8+fPj/MylJhb6zj5yu9Fo0G9wRCP1GnRoBvLCjAM5mUzGE6+W6se8/bA85dDILHcOnJD9eyjjZHZ5056TwoFZziV99NsHatnjmFoeLF4LVBXmIjoFcEVs8r45Gfz+PPlp7K5/xpCUTKkCbBPPvmiKBlUJSwPlvFf3wR2BBPF4PGpMUMg0T8nHu3uEC29sX19p6kHRcYMFX32MBHq1IimPtfK5MmTmTlT80WqV9Op0HUmRJyG6JwUOeuSb25Sorg1wid8Kj/f4kPsgwxJoeC0pEUJXVu4jCvPF1PJOE3NyNKl0XXi01BCiKgOa19w5MbK+yOpMb0SShAVF07dgsHSzYePbKZupXbuXSYnS4Y9F21e7JeQHfTgkBoRbIxrazHQ+TstZMSG9jvYVbcZyeBEVggoL19PyNiNPaAtEE+ElHBEKHL4pBS01Gj3fy01Ft4v6UdFIoXWuDceakhg8GoPHP5QYhTri8Yhp8aGDh16JOaRQgopHAHsWddOX6cXc5qBEUcNUioMPLOyjlsWasRC6J24lR20uNIpsA2+PGiRoXhC9cyOZ/CGvIisUiQ1CQkk3dBjGL/XgRACCRgn5XHuhSOZ5RzBms072bRkKcMrV5CV1ZS0HSkFLk86rekaEchFouoNlDZVs2XkZHrtml4ov83H7v5s+vw2cuhBZoaoXbEZRYxmalkWq2q6UHO0+Y7r3oTeEgKpo7joAhTFxHl5IR5r6mTDqGmUDqj+onENTcZc9g45mWGjx3J9RQWNNdUofi/Fw4YnlP3+rb6dnmCISquJBfmJVW77hd4E5z+hmS02r4e3b4FjbtKMF7Mqol5Cxu4OlMLcBP8fIVUKWv9KbckNSU0+fXoTeb5Orht9DDq9kTy7id1t/QkRoQgRyrQZEUJQ6LBQ6LCwoeEcfrq4gOOG+/nRKfMoyi2nqAgqKir4/atreGmLEzeanqZT2iDBYyaGte4C5ukGF1VXN3cCMUJw01vbWXzKMAiFiVCa5shs6hkCikCfrZ3D4mItQmQXyWk3IUDsWs2vj5rIbXUdhECrTdvSg/CplGSa+f15Eyl5bCvGkORXEwbR0EkVh6cfXVgLU+3RiKOltwOJBYHAmVaXRNwjaahjjz2byZMnDxw2AbaM2PdDBnXRuSeMp0iMae0EPVnM2Xsh9RnbcJmc1GfETDX9KnTJHJxkxMYTCo+r1xBcV4CpP0YJrTKWDs3o0CMQg1mHRfdlm30jLrUMKAqXz0fGktH57tk9hY6OofSQBcPCqbFwyNgf8EajV4GAKWF8qQrSXNp8bIobr7cZs7lwf4fsiOGwiaVTSCGFrxaklKx/VxOpjju2GMMgFVor9nZGSZDBsQrb8Ht4pv7nnPzfk1m4a+FBbccT9PDMtmcA+L46OcGXUbVkYpl4WTSK0GYSfFrbRV17PwXpRsocHzBl6qtkZTWhqgodHUMShJUej51mYyFBnR5jwE+mux/VaKakuRqAWjGMPn86+nbB6s5i3G3aTa0/TUfB0GG8ff0cnv/eTP78g6PAqGDXKRyTHb4p96ehhKtWTvJpT9N7ho7Am5t4MX66z8DUo57jPP2xTF2+ldddQUZPmMjIaUclkKDuQJBH67Vo0M3lhdFO5weNjFJY8DggYPU/4P4x8K/58KexZHVqzZ/V9jb+OKIk6uEipMrE6iX8ZNbZnNNZlyhiDW+/3ZjFc21a6iHPru1vW29yRCjLmigerut00+3L5MUt+cy9byvPrdK+Sw6HA6cuI0qCAHwy8Zk6FJeWcgeNg1YtAuQIF1ZiqbGQlDQWhQmCkEidHxEyYHDno882I8Lp2pwc7bhnG5NTNBEcrwRYNXM0L06s4FfWDPSNWjisvttLXZcmHs7zx1aOCowjTWEDPuxhd+lt/ZoQW7rb2Fu+g5yrx5FTNHrQBrQej5333nsvQSg9GOKJkKomFy6ARhb8/Vp1m4KCw6u9dnhzo5ohswLNIj+JRUlFoUOXl/CeXsTOU+YbOn5q/H+E9nH8pBR4vHaq1RoA3FgTIkKRZTo6huL326K6MCkEgXBqWQn7kcHgRMiaoxHyDGUTS5fNoanp+cEnc4RxyBGhFFJI4euBpl09tNX2oTMojJs7JOGzkCr5+yd7+cNbWvWJ0DsxFS6MlrmqqNyx/A5mFc3ab2QI4OXdL+N1eRmtG0naO1p67cmRJ7E5exgTHWV8L3yBfrnYwG/GmFCFQNm8i+8bX2KW7yl0OnA689i18yhcHgeblSwM6V18e/SzWK29uEu1dEZBbxci7PrsCAbI8rfTZcxlY/NY9uwuImqN7QXMgvOOyaIyX0uVrfNqN/4TstPJLIN+wNkYu/wp2zdR4rdQXzyMZ1q6uLlcI0P1fb3cmH9htCWHCty8o565WXaKzInE4S91bfSFVEbbzJyRu++UyH5ReQLMuAZW/IUonZQqWUvvAq6ip6eHa/MczM0aw19efRXP7t0cf9QETq08lTHqDqwvvkRjRg4fjJwSvTFKIaJzzgu3iRgsNZZli+1Ps9PDM6tilV6qhJ8v3MycqlwKHRZ6vYniVh8DiVCMdCtmZYCFdQxCQLrixR3u2aUTgqE5GWyvjy1j6h+CQEGXEbuRRlKFatCPsGaCZ0B7mLA+x2E2InwhvvXyloSPP3lxB7Mxo4+b1xVF2ZyRl0FO0I+xPJOsrCze3d5Em9vHh91a6mbnsLG83bqTrNqVNC7ZgGHcAuSolwZpQJsslB6ItPgmpTJeZE007dS4fCZBT3g5IemzaNq2PksnCIkZQZ5eUBzoTe4TpmpRrXgY45uzSpizcTSbj49/dInNIbIvRpOW2oz3EdKq2xLTbvFNbv16LdITEh5C4ZMfDBPiCF/TGYNkVMRrg7TKxKysY77wyFCKCKWQwjcUkWjQyJmFCV5BO1v7uPm/G9lQ3xN9TzF2JHl9qFKlvq9+v0QoqAZ54+M3OLXxVASCV6dIxqPwcsUcFIOZ28IdxSPtACJOuCqCv/jOokp9h+7dw5gw4QdMmljI7l749zNbwV2BWe/n8tHP0ZGjjVHo7MTUUosSDKDoYax+M0uYx4bARGbxQXhGAmdrGo6h/WQYdgPzAHi3U9NXnJjjQLb7QIWuejPdzY1kFhZTt3kDE4I66ouH8WRTJxPtVrb0e/hnXXNSk9YQUO32JRChdn+Axxq01NBPhxWiHGo0KB5Vp4SJUAx2ejHodQSCIXp6esjPzMS6Zxc6v4/xI7RUXn5+Pml+L2l+b1J0IISW3olGhAYjQmmx/anucCVFO0JSUtPh1oiQJ1H34x0QEeq0xTRmtcdUsn2ni7LmesyoSekfV7jySicEv1kwlqKMdOJa9mLq00i8b1cPrlUt2KYVYDQaSU9Pp7e3F7e7H6tIdHP+NDCUUTudXDjNQXWHK0EHk4vgpnDVoiHugywpmJ0ZFhXnaqlIIQeka4XgifxKTE/dy1TdCMY2nElr+wSWOD7G402PkoJ9CaXjYY2LCAnFjho0oOgDuLwW0iwe6j8qoLd2FHoLIGDeZaM4d+Lz1PfVU2IvoWu0yqZndyCEIDfoHdAnTHLSkldIG2hWOqDqUUihtaEPE6Eajw539Sz6enOj+2IKa7zinaVDqmDlynMSzBoVJLqQJKQTMSKEL5oaU6QO4dGDVSPRemtwUE2gx1ObIkJfFTzyyCM88sgjqZYhKXyhaHZ6qO5wUZ5jo9CRrF04WHQ1u6jZ1AkCJh6veYUEQip/+XAPDy3eRSAksZv13Hr6aFQp+dXiVUljCARD0oYkvR+P17a8xrDGYbG+YkKwccpE8nxObkRQQDo9qNTZ9EntAFSho1XkE/IMZ+rUaRgMBpr3xHQmHzXMYmLeRnblaCLMqj3rufoXd+DpcyJZhbN1KUuYx970yjgiBO42M46h/Qi/djut9/rZ5vKiAHMcejaqWsmuu9XC3rWrGXd8Fi27d1ApwSqgzR/kW5uqo0dhsGZeTzZ3clRmWjT99XBdGx5VZaLdyknZ+zepOyByKsN9o+JaOQgdWZkOWtu76Orqwufz4fP5MJlMFBZqN42I67/D3Y+QMkEvpAPKLSY6oqmxmEaoe5DUWHmOLVpKHx1DCMpytNL5Xm8iEYpPjfUbzVFNV3jyLKmawJCuVna7bQzXdSYczgfPKMBcUElZjjX6nVcUE6qqkTVTf8zrpnvhLkxVmegdJrKzs+nt7cUqtLksCZTjwUivasaNMRrBGrgvQ1DQhb+vxriIkNGTfK0PyORQlioUutKzCPVrN/R831BcnTsIhpvLIiWjRo7cbzQIIC2OCCn6NGTIAPoA5nBlYMBlgLhUVunoLNJs5uiDScFsGDIkjd6/bECoBiJ9wj7aegEju7Pob67FV1SWsM2BTVulkDEPL6BNhaGBY+n0a5VrQsIcXxnPAh6sqIS1TFIZtH2HOSRxxREhKQOo4QcsBQW9SyEYdl/w9xqRUgx4AFOwWL54HfJn0gjt2bOHX/7yl1x88cW0tWk58bfeeostW7YcYM2vD6699lq2bt3KqlXJN4gUUjgSeG5VHbPvWcwlj61g9j2Lo5qMz4L172nrDpuQS0a+lc2NTs58eCn3vbuTQEhywqg83vvxsVwwrYQTx9nIG7oYIKFRqkTyj83/QB3kZgCaBumljS8lNVdFCP5iWMxkcoEQ3nHLkXOeZ2CIQZEhCkQT48cXYDBoF87ITQuggC5W7plNj8hELwOcOsdFTulQSrIV+rbdwyi0601LbjF+Q+wm7ukI36z7tDTde+Fo0DSHDZ1nCxIV4dFuNNVrltG4bQtqKIRuyFA8A6IgQkp+Vv0YuogmJ/zv5bYert1aS0CVNPv8/KsxHA0qL0gusT9UOIph/gOJ7SFKjyI7V7sBdnZ2Ul2tEbWysjKUsB4nKysLIQRpfi9zdq6P6oUUqfKHESUUmY3k2bXUWHtcRKi+S9POGHSxeRc6LPx2wbiYbgb4zYKxUaLSt5/UmNOalqxXEQq9ljQqdInGkwBL3nub0Tn6BOIf0W4BmPriTP8kBDs0vU5EQB5Bu7TToqZHtUvxEazfLhgXJa3N0XorMMSdb0taYqrz6aZOarzJFW9CqmT1dhEKmwC68BLUx50rIdi2Y8eBNUJxqTGdzoga7mSv12nnLRRQgAi5Ame443080oemk3luZbT5azZdFDvbsXt9+AqTCYWaqyf+pLYeG0ogQrqQieyO2LE/xzeDCT5NzC6Fghtr+BgMvk/GcJWdT6fNZ0goP5oa0yHQBezRZQMuAx3rJxDtUxuCof3nfCmC6UMmQh999BHjxo1jxYoVLFy4kP5+LQe5ceNGbrvttsM+wRRS+F9AxPgv6nkj4WcvbuLet3ewfE8nfYNckPcFl9PHjhVae4TR84r5/VvbOeuRpWxr7iXTauDBC4r4/fwADmM3UkruWH4HTn83VZlVvH7O6/zj5H9ww+QbEAie3fEsdyy/g1CkHNrZqLkgOxtZ3rScbd5tcbcVDUKq6AIXoOo8OEfcgTP/bzzprowJH8KYzRKyZDfjh8XK1SM3rYt0H7LUdB3j9dqT6TB2I43v0vvGVYQenU17epAcOiihH6noaCws07atKEw/9YcAuN17CQR6ea2tB4DpDhtO5zoA0sNtAxq2b2fPmhUA6MdOSaqgkUIwrXcLqwp7eHFiBWtmjubRMWXohUaGLtu4h5u21+NVJTMcNuZm2TksmHw5XL8ZjrtV+7t2KVlejdx2dXVFiVB5eXl0FYfDoZX3C8Gollou+/Rt7ll/P6s/vZBLWrSeaHnpiamx51bV8dJ6Lf3zz6U1CeT7wmml3H7maACGZFqYUxVrSTEwNaaiEAi3mIhEpOIRqcJSRHJlVMR4MB6KEqtIjI8IIUCfoxGmiGAaICQFLplIZOIjWBdOK+WTn83jmauP4qVb5pJ1bqU2VlzIyxqnkWry+rlpRz0DIVSVC3etZsGl3yYUNgF0Ku5k4jfIPg3ErpXLo699/atQg4nHTA0oIMIRGAUcgzjEA9imFZA2MRa5lVIQEuqghpGqTVDws+nkXD2Ogp9Nxz1Wl/Cdt4TSCBKLjGVgw6SCQdXOd3841b2vWjNV1SKNkYjQBDk2SoQUFPS60tg8QwLHwgbybzWQfb+e/FsNBH72OoGWluSBjzAOOTX2s5/9jLvuuosf//jH2O2xH/28efN44IEHDuvkUkjhfwUDdQygZe0f+mA3D32wG4BhuTbGFzsYNySD8UMcjC5Mx2ZK/gkvf7sGNSgxF1j4zmsb2dOuVcicMb6QH87cTkP1taxbrwIK3qzz+KD+A/SKnt8c/RtK0ksoSS9hWsE0cq25/HLpL1m4ayHeoJe7baPQv3ZDOGUj+Ed5JR6dF0EQMGA0urBY+jC4MwhmbqN6xFMETS4+4ViWi6PRyRB/9PyH1y35vCdOYScj6d01jNxPzoB5P4fSmdDfyoX9O7nA8BjekI6PbFoJ8ui+7WBX2aK+R3mOiZBewewNcbRzCc8UnEbmt/8fFwg3GQVF2LNzWLrsAbzeBh7du42lPdrN/5G6NvQ2L1OBXH0eDoMHZ8DCpsVaT6rJFeUogURdr06GKPc0UlQ6gSKrdr0702zEopRz5eZqPuqOiVGnpNs+fzQoHo5imHMTKDp473ay9r4MnEBbWxuNjVpZeTwRApg8eTIVeTa6Hj+XLH83Dn94fouuh4rjybNrVUT9vmBCxSBo37d4QTSAN6AdjfpuD7PvWcxvF4zjrInF+ILJUUI/Ogyo0YjUkqoJSKFEq7DS/F5UGY6qxR2mwfQ0kYiQUeSiC4aFxAIyF1Sid2ifxUeEcrKzuLqigsc/2YsqY3qj+ChTxBYAgGkWTFWZBFr6oCnsXh1Xh7/X4xtU333Ge88xTQkw7nvfpf3jHeABh2od1MHbtB8Pqb7ODt577OGE99Sgd8DfCgL9Pp3g46E3x9y+pVSwSP2gKV2LxYLeYYoeQ9nwMWlx4RCzqY+AiBEhBYEqJRbVT0Ax4AoToRCJEcEIIoJpv167LhmkiFaNKQj0cREhqQp0UqLrEeh6IvNU8dfWYSjYf4HG4cYhE6FNmzbx9NNPJ72fm5tLZ2dy2DOFFFI4MMpzkvPtAji2Kpddbf009njY2+5ib7uLl8NP8IqAitw0xg1xRAnSltoe2j5owILg2d4e9nhVcu0m7jxrLHOHS5Yuu5B452Zj5/M4dGauLD2dEbrEEt75w87A6OnhZ2v+yBvVbxBw/ZffSRUDsMVoYAVeMv12wEBB7g4qR6wERbv+toSva33ebJ40/h/o4MaaJ7i47t8ca3Ywe8pcWvWFdIZKQK6AxXcl7fvKjhLqJpVpc2lYibFCxW3Vs7VKm2dWl59Zvet4puA0VntV7pwa8/9JTx9Po9fN3U2xp3wVeMA1hz/xNFOtoyhPW836bgtquAv5lLHj+KNb5eYd9WHvGckfdv6RorR0sCbeqMekWZLKjh+tb+P/huQkVZN9bsy+Htq2kb3xEwBqa7UomdVqJS8vL2lxR6AVBwOiGTIEXXt5vT12k7vob58mPdfHp5OanR5+91ZMthypHBtTlKx9qdS1YyUWJRrVUktJVytOSxoOTz9pfi9CCOadeDIdfX62rvgAKeV+jAe1SIjVUUHBz6YT7PCgz7FEb+CQSISyMjP40emjuPLoMmo63Al6o31B7zCRZlGiRMgYV/I/zGJCIZEUK0Bxaz3BbO274A1XZNkwk9fRT1tOWjTqaWquJdQ/uPklQHdzU1LPNBkcoKELKEw/o4Jxx8/aLwkCUJTYd05KBRNGjp93LIs/+jhhO0rcPnq9zQRb/5nAlcotHrwjQrBH+7tPetjtMWAJBenV26IRoZAYXDurD/+WgjqNWuhUGRVL66SCEqcrkqpITrEpCsahpXzROGQilJGRQXNzc9KTyLp166ImVymkkMKhoaE7Mf8feaK9cJp2Uejo97Gp0cnmBicbG51sanDS0utlV1s/u9r6WbhWixBM8uk4QRrpVlR2G7Qaj6eumkFVgZ2W1tcZWMesCDjd4OPbix+CDx6Bqd8BswMa10LTOk729mC0WrgxL4d3bVZ8+bn8tLOLP2RlADA3WInR2M/wkSvjuz4gJRTW+3gg4wf0W2xMs5u5bu75UFtE3dqNjGmoZnXZKJ4tPYX/a/+vtmp6EWSWg8lO75YPWeoZTlemdqOf2bMNWX4r6xt/gwxrWZoKzVR4NWO5jf1u+oIh7OHO9On2cexu64z6nkSgoqNNlGDPmoZCov5v79pVXHLcSczNslPt8VG+4R8UtbwB45K7hu/1+JJJBFpl1mEnQuFO9VltZ0Nc1qC8vHzwCFRWBVErgegYOloNRfz8pcQI0EDEp5MGi1KGpGRHa2I7BCt+ZuprkjIx0Qo2tO/D6kAxI41FXHhKKc6ZE+jq6tJK3AeQoKam5/F6awDo6VlBm+sViiqSz0EkPQiabnXt2rVMnjz5kIoMjHGT1sfNv8hs5I8jSuJIMdzi0OFz9RK02+jr7KCjuQ7Ccpa0fjeunr2oRjOK34tODSW0XRmIzMIizWA0vmdaKFGpogYUcoZmHJAEQWIqUVUVdCjMnDiL8ZOm8MI//05Dj0bKtmzfTkX4OLk9NQz8FggBTl0dkR/zs+Zl2P2VWEJaGjJChMxqzJgRqS0uJNj8iXOtce1hBloUSEHQ37oXhoVXCwmap0xnyHvvR3aCwl/f8YVHg+AzEKFLLrmEn/70p7zwwgsIIVBVlaVLl3LTTTdx+eWXH4k5ppDCNxpSSu56XbuhnzWxiIumlSY90eakmZg3Io95I2IRgLY+L5sbnWxs0IjRmppupvZqF9PVpiARX8JOl5/e3o3s3HnHoNufnitZY8mgtNFD1pJ/EOzVY7QHMVhV0BmZlzGKh6y5/Mi1hSVWC0ssRSAERiEZrnORNWH9IA678Nf837PSPJo0ncLDY8rRW0ailh3Nqo0PMLZxL1uGlLMlrZL3s2ZwQvdquOo9LR0ELLvzB9TnaRfVKlcNWaf+Gu/QE6HxNwkb6R7mZahJR60vxEqni+PDFVsW+wRei3PajUCRISrSHLiMJazrTrxRvfvYw5RNmExRdjiq0xTutF48JWmcwSIGkcqsIwKDmbRL/onxvofxExaWl+2jusZRrJXg73xT+1voYP6f2ONzDNpOIYKB6aR9VY45LIaE9dIV7yANWhMhBEzWN3DXwrXh1Jtj0Koqr7eZbdt/EfeOHNRbxul08tprryWsu2jRIioqKg5YrRUPQ1yEZKAB5iVF2TFSbDGha6rjP0DQ76e7uSmqEQKQUtVsHYIBhKJw4tU/SDDbHAh7dg4nfveHvPvYw0hVSzVLmQPEeTcFBXrDwZFqRRf73kkp0EmFlt+tQn9qYZQERfDqq69SUVGB1VLGQMIsJdTWuIGIDQD0p+/CrGq/K3dEIxT/PQoPcaZvKssCid+NZruH3U5t+woKe4M9RBRfUhX0hqM/PpOVMW+//qWQIPgMROjuu+/miiuuoLi4GCklo0ePJhQKcckll/DLX/7ySMwxhRS+0XhtYzMb6nuwGnX84vRR0cqeAyHPbua4kWaOG6lVdaxaUs/Kp3fhFpLNRi10rROCLN0nrFn7c1TVi9GYh9/fAaioEkL9IUxWhf40PVtH2FEKwLZEh/UDHUO+dzUZV/wA9EZmA3cv/jk31b1KjkFydFqA6bYgVkUTH0eeCiPYLYfzolEre/9N1RCGhglCdXU1nd1O0vWCbzUv4m8l5/NA6eUcP/syRJgE1Wxcx5bNNTQcpXVhnzF0JIwbibs7Ji6NQTLV5qPWp2dpd3+UCD3Skc9eYUIvfajChKaIUrmKR6nIHE63My0pWiRVNdZFXkqoDzfUzEwmHINFDCKVWUcKIr2QrOwcWjq1aqTc6legqjjahiNCIgEIhd2aZ1wDs64DRzHlTk8SsYlAARb+v5lMKImVdEeE6/Ei/gumDqF/QMVYr2pGlRyQDCkCbMITTb0NBi1KMVCdk+wt09XVlZxaGqTb+4EQHxHqDiTrXorMxug57TRq/wf9fjILi1DjiVCYTJxx/U8pqhq1XxIUwbjjTqJswmR6WprYsLiPoLyXCBEKBbT6RJ3esN8xIoivsgOhlclL2PnWOhjkK7lr21amHjUTW9E19Df+Jfogs9OVlVwWL8Ac0sT1kYhQksmUgICi4jK0ArHUloJkp1GrLNehoAZiRps6SxAlpKVTvRbbl0aC4DMQIYPBwFNPPcWvf/1r1q1bh6qqTJo0icrKyiMxvxRS+Magv9tLT5uHjDxLNNztC4b4/duaDuN7cyoOmgQNhJSSmqVa3mSDKURQgE7A/aevo2HvPwDIzj6WESPv5f+9eyU9/Tspz5jEveteI6gX1GVbqMuzomZC3/wQfaeE6Fv1V0Y1zMBcMAy3u5q0gky+6/Ex2hK7UXk8aTQ3jWCqv5Kukc+DUPFi5RH5c1SdwkSPk/Pjem6tXKmRi4mTpzFl+lie2NTJKsdYPq0Yzkxg4/tv8+7fHgKgsUAjINNztSiY9gSbrNw4OiuLF7t6WdajaTZeb+/h0cYeAH7AA5wx8mqc5qk4d3wfk3s5jvQHsWaN1Nxx48iQUJRYOmPpA+AJV/08e4lWzj45MeI9MGJwJElQdI5GK6ARoSe26pi/9QIms1krK4rMUVWhcbW2woSLowQpSmxe3DQI1QC3P1kefOG0UuZU5XLOI0tp6fXxzKp6nl2VqD9yY2R5sIyZ+pr9kiFVauaJkdTbYNjXOR7oLROxCognQwdjYjgQz7XEdK03bK9Hldp5HQz6OCJkz87BkhX7XqtSxWAyM2LmMYe0fXt2DvbsHHau2UZvKE7PFgj36vLuw5J7AHTh1JiqhglU+Hu9rwBgV7OWSk/LOYU9NX+hILzpxpAOw4DqTiRYAtqDVaxqLBFCal3tQ0pr4rwi6ni01Jg1N9YcePRFe2jbq1UiBoyf7bp3uPCZDRUrKiqoqKg4nHNJIYVvLLYubeLD/2yPFnLMvWwko2cX8eTyWuq7POTZTVw9p/zAA+0Dzbtj7TTuvPkomj39GPrvobdLK5suKbmSyuG38Mj6v7C6cw8OUxZ/G3c94tPXMQQl2cslvtUGPJNVXMepBMok7lkh1uy9HOL6ZY62aDe0ZpeD7I3TWR3MJ1O1k+ufQUbbdAK2Vh4+6Wjauv3YfB4mrf2EnRUFjBgxgu7ubnbs0Fp6TJs2jdycXC4qEvy7qZP7dtXzw52fsurVFwEI6PS05GqkZIzUohtmcyGjRt4dTp1oMZ5RI+9mVNYQ2L2VjX1u1ve6+dE27an6AutWprlWYPVMZ1TeDD7yrEQCDsckzOYcThyj8u4WBYlITGc4G+G922M7LdVo1VVC1IXEiMGRhtPppLm5OTYtFBZxPBXU4JD9sTn6XeB1gt4C+WMSxrhwWikjC+yc/edlCfe6eG3QYGiN60022M11VyiXxpCDG+YUcsHsUQCsWLGCZcuWAxJVwopgOb9csH8Nz77O8UBvmYhVwKJFiw4gut43mrx+bt7RkLBf+2qfAqA3alGXYMCPlBK9JU6gjIrJuu/jdyAYjDpUVyz6EyFCb/x5JSdclcfo2fvWG0EsIiTDFgYR48R8mTFoRVtuhnacdEKHMU6aNDe9nfcr3sO4+3gUbVHsvZWYo+7SGhHSqxr5kWFt0NHBUdgwo+JKnJdUo9Fio9FFzpie6GdCgbyKTwllGPAbjlBK+SBxUEToxz/+8UEPeN99933myaSQwjcR/d3eKAkC7eLy4VPbySi389BirTT+xpOqsBo/u9H7une1p/SRRxVQlOunY+N19PauQwg9I6pup7j4YjbtXc+KTz8ix5DBT4/9Bbmr/glIQvmzaF/ci1C7sK7WYVmtEBgm6T9VwTvan/j4J6Fo1U8Y1TOaT/W7QF9HvqpdVA2+LD5x5PF8tx8B/FDnpScY4K233mLYsGGsXq1FKYaWlOBpbabD4+b4hp08KbL5uN9H+bJl0T7kLXlDUHV60ly9pHUKKNTC5kVFF5CVdQweTy0Wy9DoDbLcYqTa4+ecdbvwqJKjHDZuyA2xd5dmrNjbuxEpQ5hMBZjN2k1l3MThlPmepWfUlWSc+pNYOmPZQyTd7sNVVwOJ0BeJwXxpJApdZOCgPzbHnrDOpGgS6JJTKxNKMrlnwTh+vnAzISkHLTWPR3WHa5+RhXi4MWLNLoySkZNOOokZM2awu76FXtXM94fmHZSQeV/neCAmT55MRUXFPkXXB8JgJfL7E7xHIkJISSgYJBDwQtzhVfSf/fdrMCnI3riIUFBjJwHXq7z3dw+lo7+3X9F0jAhpP1YdCgjIPakc0zNPaeaKkYq2llqGj9e8tmSwi8y4XsyKgOMLm7gn8BquvddQ1p/HMZ40TAGtjCxChGTQy7gWwZDsCThUGza0uR3vnMA/4rOCUiVHWOnAjcnSy4BuNQgBwVxJfyDZLPKLxEGduXXr1iX8vWbNGkKhECNGjABg586d6HQ6pkxJFhWmkML/OnraPMldqlV48tUdOD0BRuTbOW9KyeArHwS6ml3UbOwAAVVHe1i96hy8vib0egfjxj5MVtYsuj+tw/Gyk99yHRJJmj0Nz5Ya1NDRdK6eCKYmjCPTkD4/ILBkVWJxp1Evfp+4MQEGIQgFe2mRDaAnSoS2pivcPla7IF9mgW8NHcE/N6yhu7ub9957L3odaVv+AS+881J0yJHHnce2qolsmHMGJ730OEgZTYsVt9SSOfPUhCmYzYVJN8c8o4Fqjx9PWMxyco6DLMd49gK9vZuiRoqO9EmxlXJGYDf4sRtbIEKCdr8PKx5NPshCB1nDDvaUHBEMmg5CJYue8B/hOW7WomoMmbrPsSIpr4MpNR9MNL0v/PLlzegVEa12dDgcTDlEggKDn+PB4NiH6PpgcKiC9ygRAoJ+H4GgL0qEzEoaPr130PUOBnqjDhmKux0LicEWIOAyEHC9R/OuM6icvu8MTLxGyGh0sdDxPg985yH0DhNnyEt46x9/RTWYUAI+TvnONVHSr/pbkgodFAHpFje1BjfFYZNKq6rNLZoak5Jq52qOyduAT70huq41lDjYmpwVnNw5iw7FTciTGQshhSFV0LcLenKbaHG1HLDB85HCQRGhDz74IPr6vvvuw263869//YvMTC1H2t3dzZVXXskxxxxafjSFFP4XkLEPR1jzeiezTXquOrEK3YGUpvvBhnA7jYqZ1eysuZ5QyIXVWs6E8Y9htZbjq3HS/3JNNFwuELjWuXBxCwBKLlhyBxnY3TXIhUvQuPxFcnY10rXgHEBHgczgpWIDd48xRZ86W956lX9tX03psSextb+fFStWhAeQqEriZef/sizcCGzMG8qPvns92x97gIZC7UZ6wrCyAwpPm7x+VjoTQ/J37Wli/lHDEcJAINBFa5tWYeRwxBOhKu3/jl3a/23b4YUrABWGTNMsBGQoWnX1ZUaDYLB0EMyX72vRIERsjg1hfdCQafsdL8Fg8ADL/XbBOH764qYDLitlsinjVxWHKnhXdPro9zvo95MnYg8vcwrOZVvos7djMph00RYbAJYsP6Mv2U39kkK6dmQg1e79rt8dLiTQ64NMn7GQ91uLop5L8aLsiOFoBGZzaVLmTJXQGdJx+dSprHtXq/hKD1OFSEQIJMvGdnIhQ4izj8I8wFzLINLJDKWD0gI+B/lbr6B19L9AqEgV6pYUoCoBvMb+AzZ4PpI45FjevffeyzvvvBMlQQCZmZncddddnHTSSdx4442HdYIppPB1h8GsR29UCIbFqEKAx6xg9qjM8hloenEVm116KsZPwGLZvxZgILraa6nd8wE5Y7ZjGPIGoZAkM3Mm48Y+At0GuhbtwLWuLbkfGCD8tQScXpAhTKNGYMjNBUWgegL4q3sx+LKSLlz1SwroMuoxj6tE1enQ+X2sytjEb8YcF7uaCsF7x8ynom4HNZ8shuHjEz7zFQ5F73KiBLUr6JzRozhZTePtjl7eKazk9of+zp83N4KEs6cdOMq8L0+fWq8kLW0EfX2b6e/XBOkJRCg3TIQ6d0NfCzx9Pvh6NYfry18BV4eWasoa9qWToAiS0kEdx8KTZ2sfls8BXz+0hXs+HoAIHQounFbKpkYn//n0wP3v4k0Zv+o4FMG7EAK90UjQ58Pf6WKcaXbcZwqjdNMIOn0Jpo8HC71RhxCJVWtCgZI5zfQ12imsLNvnul5vM3X1/4ibCxyX34TX2xyNqkVE2QNhMhfQGhQUhJuuqRJe6DZx/fQ7CPaUsDbcz88UdowOhZvA9lsC7Czpo7m9ncKYuTrmAT6LjQXf4cPuZkztmlg6o/FYbB3jWOF5mraOHgIuA91DJDrFS4n9s0fFPy8OmQj19vbS2trKmDGJIry2tjb6+vr2sVYKKfzvYt3btQT9Ko5cM8deMoLGUJCLnlxNlaLwnaFryRnzT1pdkpZlgqGFt1E5+lv7HEtKiZQBQiEvTc3PsXvX7yiZE6MBxUUXU+64id4XmvBs6gAZsfmQiWRIhuj78M9Idzc5P/wBudfOjX4UdPpouWclSKIXLp+lhSW736GvzwcCnFlaZY3wuljj6U/odA4gFYVuRzaWno7knkdCaMZzYc+VjIIirtNbeLujl/+2dnFabjn9EtJ0CqNsB76Z7i/F0Zs+nr6+zeHNGrHb465bjlLQmSDkg7+frGlrMsvhwqdAb9LIz1eEAMUjIR3kmAcVx8GexVpKb8RpWr4hfQikH97mlU73wfW7O5Dw+quGQxG8640mgj4fgXY3YoDgRQhFc8D+DETIYNKhGHxJ7wsFZl96+n6jooMZIyoCNmx4jxkz9n0tAdALPT2hGBF621vBr0/9NwW2At7a3Bwd1ewf8PsWKopQKNQl6ta8ygDVlVD416giLnGaUdza8dJ5M+hqUAiEwnlFIRBi+JcWDYLPQITOOeccrrzySu69916OOuooAD799FNuvvlmFixYcNgnmEIKX2e4nD42vK8JmWedW8mQkVn8+NHlIGD6LCO5mZpgGUAISW3z7dTXvUR6jgmJj1DIgxryElLdhEJeVNWDlHGPXQnXJ4F9zYm0b4qlMLbn1PHntKeZKiZwec2p2qZkCN/mfyPd3dhPPpmc738/Yc56h4nMBZV0L9wFUrtwbW1YQV9/+EItJVZhxA8MaW6l9PyLeG7AfgtVJdPZiS7oT9K1ICWK35tQqTUFmJ2RxtKe/mizy6npNvQHkTLcb4rDPp5GtJZANltlot/K+v9oJAigp0arsrrkebANXj79lcXMazUitPbfGoGD/eqDPguanR5e29h8wOUUwX6F1193RHRCqlUipZpAhiRqtCHsocJg1KG3DJb+Uhgz69z9rjuY5YCUgsWLNzFypHO/GiqdokvQfumNOVFCsqa2m6KwaFvXk1zGf9vM2yh48+6o2XkLXTyRvxGYn7CcKgROSxo6t0DKEKs73sYTiguaSAn6sv3u45HGIROhv/71r9x0001cdtllBMIldXq9nquuuoo//OEPh32CKaTwdcaq16oJBlQKhjkon5DD21taqW/bzoLKtZyYt45gINniXho34Nx3m6L9QOJq2IFVjKKzzMut3Eu1WfMLOXbcSRRcNJ3AC3fR/OSb+FtdmEaOpOi3v0EoStJItmkFmKoyCba7eeYPP6OrvzE8P8GYhnbWVGh9vY469XRemzQTamK9H4SqctKSV0j39HPi1T8gkJGTUOZ84nHHUXLBuUl6hR8NzWdpTz9tfi1FMCrt4L1F9pXi8PpiN+/+/q00NT1PUdEFWon8oh8lDhLygTG559tXHhXHQ+5IaN8Oyx/R3juMaTE4+MqxBy+axBkTDi29+3WCIUyEQqYQqzvfYUr2SShCQZUqe01bKHEc+5nGlfp20oo2DngThg//yQFF42ZzITk5N9Defh9CSKQU7No1A5/PekCDSUUoqHFPU4qiRWmanR6eW1LNdwPab9A0IFhVlj6UBZVjCb0dK6boxUO/YZBIoFRxePpZbm8gsPoJ8huDyJJcLUItJeMa2lk/cvJ+9/FI45CJkNVq5c9//jN/+MMf2LNnD1JKhg8fjs32NbyApJDCEURPq5utS7Ub8ZT5ZvbW/JXOmue4+2gt4hEcNNMgcO6+iP72NGTQREZeJhOOG052UTaKMKOoRkTAiHPdHjaLyxK97qVC+rAxyGklXP7R/GizQ4B/bP4HF+TOIPT8f/C1mtFlpFPyyMMo+/E+0TtM1OxaT1dPI0aLhdOv+wm5Q8vpa21j+fPPY9DrGX7JJbywaicAdw0vYlSahVyvC0vBuWQU/DBKdA6mzPmYzDRKTEbqfZpv0F/r26m0mvdpcDcQA1McXm8z1dUPxS0R166ha4+WQoqHVL/0EvnPBCG0qNCrP4w5SmeWHdZNHEzlmE4IppRl7nuBbwAiLS8CXi97+zbQ7N5LmiGD/kAPeWOGf+ZxpWhMyiAjID193EGtXzb0Ut54vQazuRePx47fbzsog0m90Cc0D1bCGqDqDheOoBJNp5v8iQ9LxnDBg9t9VPS9dFMxXvuIATsmGdb0EWl+LzvTu3BLJyVdKkvGeVkxwsjFH/oo6QqyVD94E9cvCp/Z+MBmszF+/PgDL5hCCv+j+PS1VWRUvEvuiHXsadbIQoEVVFWHvXMs9pYZqDo3baOeBqGCVChx/oDxynH0KR68Li+6vRCqVunWdaAMuAnlF8eEzEiF4bm/pOD42SxrXJZAgkBzvm295xZMdWZQYMjDj2A4iCbJqxctBGDiSaczbLIWZdhZUwtA8ZAhrO33Uuv1Y9MpXFyUjU2nA+xR358IDqbMudkXoCFMguDABncHwn7bNWRVaAKMeDL0FSiR/8wYdwG8dQv4w8rV5781qBv2Z0Wkcizee+jsSUW8vK7poLyIvimImCr6XNpx9oT6omke3efwEbJYy5FSIBKaeCU7au8LDoeD3IlV1K2oQ0FBRaV0eukBf3M6RZfo4R2OCJXn2HDqVVQkCoL1BTkJxozOYJCg04ez65zoug023aB6wAx3K2BGFSp+vbZ/OhmiJduHza8RILfxs1sPHA4c8pmbN2/e4B2Pw1i8ePHnmlAKKXyd4fd30db+Fg21r6AvW0N+9MKmsLOnkk2Nk/lJ62z0wbToOmntkwlYWzG48zH4svDRgxEwxutjBmlyGBEyR9bNv1wLy79V81bCnLJ6JXM3SUxLtPRVwfVXYZ16YA1J864dNG7fgqLTM+mUWN6/rk6rHCopKeGFVk3XcEZuRpgEfXYc7o7u+23XYC7UiMKi679SJfKfGe5OzVE6gv24YX9WDOY9dNPJIw7Ki+ibgohGyOd2JX2m6D47EbLZimhZ/S0KpvwHoahIVSHb/rOD8lICaHG18FD7Q5hKTNgCNlwGF752H2e4ztivCFknEjVCiqLtX6HDwi/OG8fTz2xjujTyyriiBJLT5AtS39KHgVikqMSlIqRMLJyQKjavkwgRirQaMwS1jZrC0gCf8uXq8g75zE2cODHh70AgwPr169m8eTPf/va3D9e8Ukjha4NgsI/29ndobX2Nru6lUTGzEIB3FFXjL+D5zcN5aGU3F9tt6IOJhMHgyyJ91AgMBTYUkw5h1EX/FyYd7c0uVr1dS0eLm6AEo4CT0vUIITD4sjD4skCAPsfCWzVv8dJuzaxQIJi7IcT33lSj0SRrmY3M7950UPu1+jVtnFFHzyUtK3ahqq/XUnv5JSW82toDwPkFnz8lcrg7uh+wXcPkyzWi8BUrkf9M6NrDF+GGPdB76GC9iL4p0EWJkDv5s8/lLK3DWX0MrtYx+OytmHrzybv86INev663DlWqePQePPqwS7PkgN48SREhEduHCPFduLMN2Z8s5K63KQwL/64A8n2S7+728mhl7PtQ1PoOVr92rFShErn0GcKZMFt4qgb/wTWXPVI45DN3//33D/r+7bffTn9//6CfpZDC1x1ebzNuTw1WSxlmcyGhkJuOjsW0tr5GR+dHSBlL6Zj0I6lfMxZX03QuvOV0ehXJo598CMAVVhv0DQgDC3CcWr7PstshQ9MpmpbPmrdqWbmoGq+E9Z4QEyw6FCFAQOaCSupp4raltwFw5dgruSjzRHruuTBBRuSu8xBoaTlgp+ee1hZ2rVgGwNQzzo6+39/fH231sMOagTPYRLHJwKyMtMGGOSQciY7uB2zX8BUtkT9kfNNSfV9RRDRCkdRYPD5Piw29SWMIQXcWfk8mOin47tNruSE4NurSvT+UppdGRdvR+QjlgN48OqFLoM86JfG3VuiwcM64Qu5e3p2UZB7e8jbp+hfpCf4A0CFRaQ4tBC6NLpPn3IISphl6nZ5A+BAZgjBvg0pxuPr+Ox8+Qc9/h5Fx3nkH3Ncjgc9+5gbgsssuY/r06fzxj388XEOmkMJXAk1Nz8dFFQTp6eNxuXYRCsWeCq3W4RTkn0Fe7uks+lMHXQ39TDihBHuWmV89tx5/UOW6nAxsrV7tAUoSbUaYuaDygN4jik6hcHhG9O86v6QtEMSmExz9nTGIcWn8+I3v4w66mZo/lesmXYd3xSqcA3t7qCr+2roDEqG1b7yClCplE6eQU1oWfT8SDcrLy+OVLu1mcG5+pkbIDgOOREf3g23X8LWGo/ibler7imJ/qbHPFRGK63xqCfcL83HwLt0FtgJum3kbdyy/A1VqHj+3zbztgN48OpEYnY5ohOJRZDby+8p8btrZEk2PlRoVCl//fwi9ilm3lqBahCKa+cReQjwRyvI4sIWfEW1GGwGdlte3eSTfe1NG69UUJM2/ug3b0Ucf8Np0JHDYiNDy5csxmw++3DWFFL4O8Hqb40gQgKS3dwMAFnMp+fmnk58/H5utCiEEO1e20NnQj9GsY+opZWxudPLSukbyEJwfLol3nFKOZUKuZr6WY0FPB1Qv0Z7q93PjysizRBz+tblJ8KkSR5mdOz/9Nbt7dpNjyeEPx/4BvaLH9cnS5EEUBePQ/T9hevr72PTBOwBMPeOchM8i+iBH6VAWd2k7dF7B/itTDhVfZEf3bxS+Sam+rygiYmmv6/ASIUWnIHQCGVfCFRCSkOSgXboXVC5gVtEs6vvqKbGXHJRBoRAiwWhVrwz+QHZpcR4P7PyUesoByBV+RDj6pBed6HWd2oIy8YGjxF2EJaAxoWx/djQ1lu4hqfjjYB/SjgQO+cwNNE2UUtLc3Mzq1au59dZbD9vEUkjhq4DBK4+gqurXDCm+JKFwIBRUWfHqXgAmnTwUk03PXU9vBeBeewaiL4Sx1E7a0cUIRWhRoLX/1vxspKqlNvZT6ZOWaWbuZSP58Knt0cXnXjqSN9sX8dre19AJHX+Y8wdyLDm4Pl1B1z//GV4zHHpSFAp/fccBLzQb332ToM9HbtkwSsdOSPgsEhHanVtM0A0T7VaqbKkHoK8Mvimpvq8ojpRYWhtbIeCJlZEHOHSX7gJbwSE7NMdHcwemxiIQQoeZmJmQ0JuRQomSIYCQFAgSy+BFXETa0mHBa7AALvJlEFXoE8nQQTykHSkc8plLT09PuPgrisKIESP49a9/zUknnXRYJ/dl4pFHHuGRRx4hFPpy/Q1S+HJhMg2WUlHIzTkuqXpyy8eN9HZ4sTqMTDiuhMXb2/h0bxfnKEbK+0KgV8g8vwoRqQaLmPpFLiYHUekzenYRpaOzcLZ5cORZqA3t4Z437wHgR5N/xNSCqQTa2mi86SZQVRzlbnLH9eI/9iGMU044IAkKBgKsffNVAKadcU7CPgYCAZqamgD4WBoB/2ERSaeQwtcFA4mQwWQm4NM0f8rnrJo0mfUJREhVvhhLAoUDEyEAPbFeaIrehJj/APLV6xBhldFfg/NB7khYJ54ICQSqMQ1wkaWG+PcZdr69yIMAQggennAu8+r9XPgldNo4ZCL0xBNPHIFpfPVw7bXXcu2119Lb23tAL4YUvrlobXl5wDsDKo/C8HuDrH6jBoBpp5cj9ILfvrmdfAQ/FBZA4jhpKIbcuKe7QU39QlqU6NifgDL4hTUt00xappkebw8/fu3HBNQAx5UcxxVjrkAGgzT9+EZCHR2YCmwUTGlGGX0yhtMvO6j93fbJB7idPaRl51A185iEz5qamlBVFW9OPps9fvQCzs5LEaEU/ncQ8xHSiJDRao0SocEc2g9t7LjfuwJLbpn3hVTkxT/s6HX7rt7SxRMhgMmXIzr3wlKtgOotdTqCrQnrKHFESCLxC01XuMVg4sNxClcs0j77wdwbqMko4t2D1EQdbhzymRs2bBidnZ1J7/f09DBsWKpCIYVvDtzuampqHwVgxIg7mTzpKWbPWqK1aBiA9e/W4ekLkJFvZdTsQp5bXc/utn5+qVgxhiTGoemkHT0gyqPbh0D6o3vgkRmw7ikIDd7oUpUqP//k5zS5miixl3Dn0XcihKD9gQdxr16NYrVQPKUGRS/h2J8e1P5KVWX1Iq1kfsqpZyZpHiL6oIYyrWv78dnpZBsPm8wwhRS+8tAbNKIQiQgZLbEHGzmwMOEQYTDFiJDRpP/CyEACERKD/56bmp7HEEeEAv527UVGrCothKI9yMWPHXdM9tr34lO0enl9SBAIxKpn22zaA1VISmo6kq0JjjQOmQjV1NQMmi7y+Xw0NjYelkmlkMKXDSklO3bchpR+srPmUFx0MZmZRw1afeTu9bPuPU07M+PMYXiCKve/u4szMTBJ1WkpsfMqYykxAFWFxXcmDiR0UHUKmB3QuQte+X/w4CRY+RgEPAmLPr7pcT5u/BiTzsT9c+8n3ZhO3wcf0PnYYwAUnlWGyR7Qxis+uD4+1RvW0NVYj9FiZdzxpyR9Xl9fjwqsC1+0zs8/vCLpFFL4qiMaEYoQIXMcWTmMREhRoL/7i3FbVhLE0slEKFIwEp8ac7v34vU2a9eqMILoYKBGKK44v8XaEvMRCkp0cYsGw9s9VE3U4cJBP869+uqr0ddvv/12QrooFArx/vvvU1ZWdlgnl0IKXxba2l6nq3spimKkquq2/bqpr36jhqAvRF5ZOhWTc7n/3Z3o+v38EM1bx3FyWWJKDGDV41DzMRiscNmLoIZilT6+Plj9D1j2MDjr4Y2b4KPfaf2kpl7Fp91beWS91lzzFzN+wYisEfgbGmn62S0AZJ53Buk8rm3nIKNBQDQaNP6EUzAN6EGmqir19fU0ZeTQiYJDr+PEnPSDHjuFFL4JiGiEgj5NOGwwxUd1Px8Rik+NeV1B/v3zZcy9bCSjZx/ZJrbiAEQoUjCSoBGKtKoxZ0TfC6Jj4FUyITWW4CwNurgKuaBQvtQ2LQdNhM4++2xAC6MNdJA2GAyUlZVx7733HtbJpZDCl4FgsI+du+4GYOjQ/4fVWrbPZZ3tbrYs0SKhM8+poLXXx9+W7OW3WLAgtJTYwAtZ5x5491fa6xN/DUNnJX5ussPsH8H078K6/8DSB8FZB+/dTuuyP/HTwlxUqbKgcgHnVJ6D6vfTeP31qE4n5gnjyR/fDVtVqDr1oKNBrXt3U79lI4pOl9BOIzrlzk48Hg+7h44G4Ky8DEyfUxORQgpfN0SIUAQ6Q0xT8zkDQgP6jGnjffjUdkpHZ5GWeeQqM+OrxgYjQpFWNXpiaXoB4VY1DdH3tIhQIuKJ0eVDjued5qcAzVk6KxDTRz713VmU5dq+NJfyg76SqaqKqqqUlpbS1tYW/VtVVXw+Hzt27OCMM844knNNIYUvBHv23o/f34bFUkbZ0O/ud9kVr1ajqpLSMVkMGZHJfe/u4OSgjmnoEYYBVWKgRX5eugaCHiifA1Ov2vfgBgtMvxquWwtn/5VATiU3OUx0qT5G+oPc4vRCbxNt9/wO7+bN6BwOhtz6I8S2F7X15x58NGhVuLnqiFlzSM/JTfq8rq6OgKJjb65G6i44zN5BKaTwdUAkNRZBgo7uczIhNZS8vlTB2eYZZOnDh/hot2EQQ8VIqxpdXNorzVahyQTiUmOZsne/25k3/BR+G3alL/cGyQzvr9TrmDk850tt1XLIj3TV1dXk5OQcibmkkMKXjt6+zTQ0PAnAiBF3oOzDYAygva6PXataATjq7Aq2Nffy8epGrkV7eks/uQxDzoAf9/KHoWElGO1w1iOaGOBA0BloqZzHT0Yfw3qzCbsU3NfainnFX+m9YRrdTz8NQNHtP8Ww9nfa1bPqVCiadFD77GxrZeennwDJBooR1NfXU51TiF/RUW4xMiX9i8/jp5DCl439RoQ+Z2rMOoi7vFDAkXeky+dj16DBIkKgtaopLYhFii3mcI37rnei7y003b7f7XiUNHKOuQ4AXQj0EV6l/3y2A4cDB5Uae/DBB/nud7+L2WzmwQcf3O+y11133WGZWAopfNGQUmXHjl8BKnl5p5Odtf+mh8tf3gNA1fR8ckvs3Pj3FfwUC1YExrJ00mYNSIm1bYPFd2mvT/ktZBycedjCXQu5fdnt0QvtGSMvpOToGfhe+i3NK7TQdPboftJWXBFbqWDsQY0NsPbNV5GqSum4ieSVDV75WVdXx87ikQCcl5+1X81UCil8U5FEhPRxEZTPmRqz2hPHjhimHsm0GCQ0ld8nEQKwGNIALTolQPNBezdmoqwT+z8AzzzzDKdMnUo6oJqyMKQ7gHr4nP5LhwMHRYTuv/9+Lr30Usxm8z6broIWYksRoRS+rmhsepbe3g3odGlUVf5iv8vWb++ifmsXik4w48xhfLSzncxdTqZiQeoFWecNSImFAvDS9yDkh8qTYNLB+fo09zcnkCCA53Y+z5XDL8GzxIoaVLCWmskd25S44sf3wZQrBzVmbPL62evxMcxiIivoZ9P7bwOageJg6O/vp67fTUOmljI7L2WimML/KCJNVyOIjwj5PJ+v7FsfVzU2c0EFVdPyjzgJggFi6X2UzwMY4q5nPcEQTW17KRrog7YfSCl5a/VqzrBYSFMFBrRjJ78uEaHq6upBX6eQwjcFfn8He/b8AYCKYTdgMuXvc1mpSpYv1KJBY+cUY8sy87cnVnFbOCWWcUo5+oEpsY/vg+YNYM6A+Q8mPobtA3W9ddzw4Q1JIXdVDdF+590Ydu1Gl5tD8W9/glh0GX0BI91+C5lGD3aDX+s5NYAIPdXUwc07GlDR8uL/z9OKxeclp2QoQycMLqyur69nd94QEIKjHDaGWvbfIDaFFL6pGBgRcra1RF9v/eh9howcw7jjPluHBb0hlqIqrHB8ISQIEsXSrkBy65AIDHHLre11M7XXzh8Lz+CS5tcOeltSSvrtadj6+jGo2v5+bYhQCil807F79+8IBntJSxtNcfH+ozW717bRXteHwaRjyqllvLimnvM7QljRo5Tak1NiTethye+116ffC+n774SuSpVntj/Dn9b8CW8o2Uvk+I1geHspKArF996Lfngxm3oKeLd5OBKBQHJi4R7GZWlprjqPj6U9/bzb0csbHc7YdoCHzXmcWT6ak045eZ/prrr6enYUaGm881Mi6RT+hzFQLN2wdXPC3+8+9jBlEyZjzz50HW3jju7o65f+uPYLKZ0HCKqxarAHV92BTtGxoHJB0nI+NTH6owI3V93EMZ0rKfG3EZQH1jsKIUjr60f6fDic4XL8ryMRCoVCPPHEE7z//vvR6rF4LF68+LBNLoUUvgh0d6+kuWUhIBg54k6U/eTJQyGVFa+EG6ueVApmhc2v7+a7GAgpgoILRiSmxII+rUpMDcKoM2HsufudS11vHbcuvZW1bWsBmFEwg5lFM3lw3YOoUmViteTqt7UIUe711+McP5FlrW0s7ZuCHa1qo9fm4P6082h/dSnVeSV0GPcjthSCV0++hJVqkEuqm7mgIIuyARGfFS3tdJeMwQDMz8vY7/xTSOGbjIERoYGQqkpPS9MhE6H+bi91W7ti43xBpfMtrhYsMpbS+1Whhxc2/JJZRbOSmrf2hZLTYCEEPyn5M3LbemrUfOAP+91esd6H1aPpjC56ogYA+XXRCMXjRz/6EU888QSnn346Y8eOTYkmU/haQ1X97NipefoUF12EwzFxv8tv+6QJZ7sHi93AhONLePKdXVzm1X5G6aeWJafEPvwttG8Daw6ccf8+U2KqVHlq21M8uPZBvCEvVr2VG6feyPlV5yOE4PRhp9P8xN8xP/uf6DovFZRy2/KtqACX3syQ5hr6bXZ6HIkXYUUNkdfTQY67j83FFYlzkBJDwEeL0cx9Na3cV9PKDIeNCwqymJ+XgUWqfKDXjCFn282kfwWe3lJI4cvCgYiQUBQyCg49itMzSIl8pHT+SBKhms51lBhjqXdFwPmZPmq71lNgS3SXzzcml9brAFV1sELV/MXs+9mWRGXCi2/FtiUj739OlflhwCEToWeffZbnn3+e00477UjMJ4UUvlDU1/8Tl2sXBkMWFRU37XdZvzfIytdrAK2xarc/QP7SVqzo6Ms1Uzx7gDC5fiUsfUB7Pf8BsA3+lFjbW8uvlv4qFgUqnMEds+6gOC02nn1LPd0PxUhQe0YWv7Llxi4hQtBQVK69VFXyO5sZbVBwNNeT1dGKQdVqVbNdfSypmoAUCkKqzNm5gcqmaursDtqmHMMGYWSF08UKp4tf7GpgmF5hWzgt9lGfl6ebOrmkKPtAhzWFFL6RGJgaGz71KPasXYlUVYSicOLVP/hMabGMPAtCJFoRfRGl87l6FdeAZzNFQI4+OfqTM6CvoA74w4gS3qnZc8DtqKi0ijWkuQfxRFK/hkTIaDQyfPjwIzGXFFL4QuHxNLK3+iEAKof/DIMhY7/Lb1xcj6fXT3qOmdFHF/Hs42s5VurwCRj+7TGJKTG/W0uJSRXGXwijks1GQ2pIiwKtexBfyIdVb+WmaTdxXuV5CZHWvg8+oPGGH0f/bsgt4HeXfw85iAfRrPUfM7qtHr0p5vNjNpsZOXIcGzZsYFRLLSVdrTgtaTg8faR5XKDTM8zVx7AlbzAtPYOOkeNZa8+mNijZ6g9FI0gSuHlHPXOz7BSZ9/9knEIK30QMjAgVjRjFcd+5hp6WJjIKij4TCQJIyzQz97KRfPjUdqT6xZXOF2dNZq/UyE8EEkFxZrIHmTFuobNyM7hteBFFZiPvib373cZWx1Zq0muwuN2oIhYJikA177vj/ReFQyZCN954Iw888AAPP/xwKi2WwtcaO3f9GlX1kJExnYKCZHFgPDz9fta+o3VfP+qsClZvbGF6jRsQeGYVYBjYKPD9X0PXHrAXwqm/Sxqv2lnNr5b+ivXt6wGYWTiT22fdTlFaLKwuVZXORx+l/cGHQEqctjT+fdoCXjn2REI6vfb4GPcbFFIyzOOKkqCysjImT57MqFGjMBgMlJaWsmjRItL8Xux+L6aWWvTdHahpDvKmzaKttx93bw/5K5dwCrC9eBgfDR+fMO8QUO3xpYhQCv+T0On1CEVBhrWxOoMBe3bOZyZA8Rg9u4jS0Vk42zw48ixfSNWY2VzIGjmaKWwNkyGF0SPvHrS5tDHuWlNpM0evAUksIMLkwug19uLRe/CkC6qvOYWKv2jpMRleV34FriWHTIQ++eQTPvjgA958803GjBmDwZDI5hYuXHjYJpdCCkcK7R3v09HxHkLoGVF1xwFJ/Zo3agl4Q+SUpLFG9WB7sZpi9KwliCffSIJ9YfUSWPEX7fWZD4NF891pcbVQ46xhdetqntjyBL6QD5vBxk1Tb+LcynMT5hDqd9F8yy30vfsufr2ed274KY+VjaJX0TQ6k7dvwhJwsWzs9IQ0V6HJwMQZ05g0aRJZWYkVXpMnT6aiooKuri6ysrJQgoGEJ9lQKERdXR1bt25ly5YtlLQ3ISrGIePmpQDlqfL5FP6HoTcYCfi0as4EQ8XDgLRM8xdWNh9BHSW81ryXXL3k4ZNfoChzzKDLxfsIWXWH1pRCILhhyg2cMfZKdr+2hUB9PVuPKmDMpy2ohzjWkcAhE6GMjAzOOWdw47UUUvg6IBTysHPnHQCUlnyHtLSq/S7f2+Fh0xLNwXnESSU8+eImbsCCG8k9eGh9aQtzRuRpvXJ8ffDKtdqKk78NlScAye7QoEWB7ph1B4VpiU9f/tpa6q+9Ft/uPXw8dSaPX3EN9TrtqSm738nMPZsZ0t0GQlD+6Tv0WtOYXFTAvHmzGD58OLr9VGE4HA4cjlh/oPgnWZ1OR3l5OeXl5YwcOZInn3ySOTvXJ2iKfppjS0WDUvifht4YR4QMX35a5/MiJEM4QwrOEDhsQ/e5XHxEKJ4IJT9DJua+vj/h+8yYNCNahabLyCBQX48hGO419nUkQv/85z+PxDxSSOELQ3XNI3i9jZhNRZSX//CAy69cVI0alAwZmUmTx8s1YePEv+KlCQkSajrcGhF655fQU6e1zzj5bgKhAC/vfplff/rrhDEFYlAS1P/xxzTeeBNbMnP568/uYuPQCgBy9Apjt6ymqqVO6wwUVlbOGzuKOfOOw27fX73GoSMnJwchRIKmKMPr4or/d81h3U4KKXzdEC+Y/iYQoUCcj5BZv+9olCFOk5gYERrIhBKJUKGxMKEUX0mzadtyadv1Kyorm1dSml6aVLL/RSFlqJjC/xRcrt3U1T0OQFXVr9Dp9t88tKOhnx0rNffYsaeUsPc/W7CgsJYgL6H9kHVCUJZjhV3vwZonANhx/C28vOHPvLb3NXp8PUnjSiQN/Q1RIiSlpPOxx9n0ryd5/Nxv8f50rc+ZRRFcbDdi++BNAn0DujsLwehx4w87CQItcjR//vyYpijgY/78+QnRpBRS+F9EvGBaf5hTY18G/CF/9PX+eo2Z4iNCSnJESOgjZq2JROiNN95Ar9czebLmXK9L0+w4LC7NUHFPfy2/f+cqFKFw28zbBjVzPNI4ZCI0adKkQfUUQgjMZjPDhw/niiuuYN68eYdlgimkcLggpWT7jl8hZYCc7OPIyTnhgOt8+vIekDBsci5vLNrBOUEFD5Lf4UGikaDfLBhLodGLc9EPeN2exssFZWxb+9voGNnmbLq8XQlpMUUolNhLaPL62dPtRD7yMC8HFF647Y8EDEYEcF5eBrPqd7Ljg2UEkqeGECJJB3Q4MVBTlCJBKaSQSIS+aRGh/WFfGqHIu4qxQ/tbSuQAirBo0SIqKipwOBwoNo0ImVwaAQuFM/mqVLlj+R2DmjkeaRwyETrllFP4y1/+jNFxwAAAV6xJREFUwrhx45g+fTpSSlavXs3GjRu54oor2Lp1KyeccAILFy7krLPOOhJzTiGFz4SW1lfo6VmBopipqrrtgALpxp3d1G7uRFEE64WP8zpDgEDMG8J/jyqkpsNNSbaJGtd6blr4SxZn6QmILAj2olf0zCuZxznDz2Fm0Uxe3fMqdyy/A1Wq0SefxU4DN+0IGyIed3b00Wp2Rhr/L13PtjdfY0eHdnGZOH48tZ8uodviACEQQnwhEZqBmqIUUvhfRwIR+oZFhPaHfWmEIr3KVH8Oihhc7yOlpKurSyNC4YiQqV8jYMG4VVSpUt9X/9UnQh0dHdx4443ceuutCe/fdddd1NbW8s4773Dbbbdx5513pohQCl8ZBAJOdu36DQDlZT/AYhmy3+WllCx/STMKU8utjN3ThwU97gIrlSeWU99fzyrny9y69lVa3a3aSkIwMq2Es0dfymnlp5FpjnVpX1C5gFlFs6jvq6fEXoKqy2LK8i3IyPOUEIDkj5XFFO7czCevfoyUkrS0NM4880y2vfoCwdrd5OcWMO/711M4pCRFUFJI4UtAgkZI//VXl3zuiFD4bRl0cNvM27i2LtmMMT56HdEImXyayWsojghFIuVfNA75LD7//POsWbMm6f2LLrqIKVOm8Nhjj3HxxRdz3333HZYJppDC4cCevfcSCHRitVZQWnrVfpft7/aybWkzrdW9CL2gob2XsxQzQR3smtPA3W8/GHWBBnCoktP7+jl7+FmMOu2BfY5bYCsguw+cG2u4qXkDMnNgh3vBpvffY3f1TgDGjh3Laaedxpb332L3quUoOj1nX38zhcNHfObjkEIKKXw+6OPSYd+I1Fjo4IiQcR9i6fjA+oLKBdzUtAF3nFv0wOh1RCMUQSQ1FomUfxmC6UMmQmazmWXLliW5Sy9btgyzWVOcq6qKyZTyGknhq4He3o00Nj4NwIgRd6Ao+y7/Xr54G2tfaI7q/TqUEN8JL/94/n95ZZPWVFgRCrOKZnF2WwPzdi7BmDsKTvr9fufR89//8uaTz/LHS66iMS/ZsExIlVBjHRaLJdrLr2HrZj5++gkA5l3x3RQJSiGFLxnf5Kqx/SE+NdYXCEVfiwFVY7pwdBvg29/+dpK+UElLLO6IpMYWnb2I0vTSQ5n6YcMhE6Ef/vCHXHPNNaxZs4Zp06YhhGDlypU8/vjj/PznPwfg7bffZtKkZIvuFFL4oiFliO07bgUkBflnk5U5c5/LvrD2JVqet6NoBeqYBZxrMmBGYZ11O6+kL6bUXso5lecwf9h88vd+DJ9cBYoezvkr6Acn/4HmZhrfX8yd1a28fv0vAcjp7uTo9at4dd4pqBA1RJw0tIQzzzwTu91Of3cXrz3wO6SqMuqYeUw48dTDfXhSSCGFQ8Q3TiOkHpxGaFF7T/T16Wt38ccRJVrfwQFSy/gIUXl5edI4ysCIUJgIxfdW/KJxyETol7/8JeXl5Tz88MM8+eSTAIwYMYLHHnuMSy65BIBrrrmG73//+4d3pimk8BnQ0PgUfX2b0evtDK+8ZZ/LtbhaePrDlzmBbwNQahRMtOgQQiCR+IYq/OuUfzEpL1w12dcCr9+orTznZiiaCGjaokBjI+6Vq3Cv0v69n1PIAxddSdfRmmPrmR+9y/+9/AytQ4rItOnoMFrICfk5/7i50arMUDDIa3/6Ha6ebnJKhnLi/12bammTQgpfAXzTqsbixdItrpZBU1NNXj/37G2O/q0S6zs4EAMjRAPh2bA+4e+iLokiFHTKvo1gjzQ+k9Lr0ksv5dJLL93n5xbLke2Ym0IKBwOfr509e+4FoGLYTZiMg/cDklKydskejtl9AaBFgiIkCLQf9lHbqyg4e4z2npT0PfcDujslGcUTMJWci/v553GvWo179WqCzdoFozM9gwcuuoKPJ80AoKS1iZv+8zeGN9axasY0WgoLMXlczM7P5ayzziIzMyau/uTZf9O4fQtGi4X5P/45BvMXa7ufQgopDI7E1NjXXyztC/mir09+8eRBvXz2enwMlEBH+g4OpD3784kOtLTQ/Z+nEt4bWwN5/V8eCYKUoWIK32Ds2v0bQqF+7PZxFBdfPOgyLqePD5/aQc1GHyYsOE0dlKg5ydEXCcEOD7p0I+vvv5nFKzzAeKiVjHv+Ukq6+mKL6vW8e94lPDz7BPr0BvQC/i/fwRm1G2hQJG+degoBoxGdEJx48slMnz4dJU6IuGvFMlYv0nr2nfL9G8gq+vJCximkkEIivkmpsRZXC6qMUZx9efkMs5hQIIEM6dD6DioDrpXxfzZ5/Qktefw1tVqz6DgowKmrQnyZOGQiFAqFuP/++3n++eepq6vD70/ML3Z1dR22yaWQwmdFV9dSWltfBQQjR9yJEMlPHLvXtPHh09vxuYKERJBVJW/Sm7mDBXXXgxxwgZMqHQ/8ls41H7O4ODPOTlWwaUgu7SVFZBcPwTt6PH8bPpm1UvtpTbBbuFq62PL8v3lNSpisaecKc3NZcMEF5ObmRjfR19lB3eaNvP+PPwMwdf4CKmfMOuzHJoUUUvjsSHCW/pqnxup665LeG8zLp8hs5I8jSrh5Rz0hNBL0hxElFJmNSb3GfHEVY1OXb41piQBj2dBoe6B4nPypn0BLC4aCr0mLjTvuuIPHH3+cH//4x9x666384he/oKamhpdffplf/epXR2KOKaRwSFBVHzt23g7AkOLLSE8fl/C51xVgyTM72LW6DYAOawMfDH8aa89c/lB7FmnoUPvbEdZshKIg1RC+Df8hULuUrnRrcpdBIWgSKq/kDmHZ0EkEpR59wM/Rq95j2p6NbC4ZkbTO0VMmoQ8FCPr96I1GNi1+h3f/9hAyfIHILCzimIu/fWQOUAoppPCZoTN8czRCpemlKCiocbGefXn5XFKUzdwsO9UeH+UWUzTSE39la/L6cYXiIkzEtERFZiOGggIyLr6YnqefThhbkeCvrfv6EKGnnnqKxx57jNNPP5077riDiy++mIqKCsaPH8+nn37KdddddyTmmUIKB43ausdxu/di/P/t3Xd0FdXax/HvnJp+0huptFBCDUhRpCgICoqIV7021IuX+6KC6FUUpSio1w4aC6ggWEBRFBBFkCoq0kIHKQkJqaTXk9Pm/SMQiQkxlOQk5PmsxVqcOXNmnkmG5Mfee/Y2+NOy5aQq7yXtzWb9okOUFlpwKA52ha5hX+QmgpLv5aXi9pjQoPVRKFo5E0VnROMRiKM4C9Wcj6l/LIfLUqC86vmy/EPZdNN9JOorxsa1yUlj2JbvMKadwOwfUtPyzKx8/y10pRXdaS6eXpj/so5YfmYGJfl5VVaHF0I4n/6sFiGNtmmPLgl2D2Za32nVZr0/11w+oS6GKl1dQJVhBMfLyv/6kcqxRGc+5zdmTLUg5FDAEOmcR+fhAoJQRkYGnTpV/A/bw8ODgoKKhdaGDx9ebbZpIRpaWVkySUnxALRpPQW93gsAS5mNn5ce4eCWioHMhW6nWNtyIWqgmehjjzCtuAVeKOjDPXGJyiLfVoZqK8Nuzq84sEaD2XU3x/LbAhX/+C0aLb/2vIZtXa7CoSh467RMbx3KbcFdUEcNZf26dWz++ecaqlTx9fGh1G7BVl5eLQQBqA4H+RlpEoSEaGTODJbW6nSXxZOcf531/nwnNDz7K1DbWKIzDBHhoNOBrWLRVQfw/jANA4p+YVRwwy+4ChcQhMLCwkhPTyciIoLWrVvz448/0r17d7Zt2yaTKAqnUlWVw3/MwOEox8enD0FBIwA4eTiPdR8fpCjXDKjsC93Mr2HLae3fGt+j45lY4IEXCpoWHviMasGJ2yZUHPBMX7ZGQ0B/I99mV/yA6DL4ehh8I0+dyCbl9Bi/EQHezGrTgkCjHovFwrJlyzh48CAALVu2JDExEVVVT8+yeiPdu3dHVVXMxUWkHzvCspemV+k3VzQavINDG+pLJ4SoozNjhJp6t9jZgt2DL3xG57OSUG1jiap8xMsTNTcPgI+GKKzvomGjkxZchQsIQjfffDM//fQTvXr1YsKECdxxxx18+OGHJCcn8+ijj9ZHjULUyansH8nJ2YCi6IlpOwOb1cFvy46xZ/3Jih08LXwb/h7ppmMMDB+E6cit3JHlwAsFNcSdoAc6kvrIeGxZWRhatiTs7bewbfwIw8F3WGXvxB6XDvip5XzXeyifH69YDDXYoOeltmEMDaiYOTU/P5/FixeTkZGBRqNh+PDhdO/enYKCgmqruCuKgqunFy27xjHkwYdZM+9tVIcDRaNh8NiHpDVIiEboz8HSCkU52c3+3+nZ8walF5SdcyzR2exuBjSnn6vK8Tq9aKuTFlyFCwhCL730UuXfR48eTVhYWOWSGzfeeOMlLU6IurLZSvjjj+cAiIwYS1GmP2sX/E5BVhkAxa1PssR3DlZtOWM6jsH92LVcnViAFwqWIFei/t2Z3AUfUPLzzyhGAy3u7ITxx/swZiTwfvhIpneYgKrRVLTanKroyron1I9nWoXipat4Ii05OZklS5ZQUlKCm5sbt912G5GRkcDfr+LeadAQorp0Jz8jDe/g0Gb/w1WIxurkwf0AWMpKmTf+PgY/+DCdBg1xclXOk5RTUvn3K19ax4ujOnFbz4gaA9AZOhe3yu6z8tMNa85acBUuwTxCvXv3pnfv3peilnqRkpLC3XffTVZWFjqdjmeffZZbb73V2WWJSywxaQ7l5Rm4uISRuWcYCT/uQFXB1aQnof0q1isr0SpapvaeiktiV1rtSscLhWI/I23vCqHsq/9xavYnAAR3ycTlyLsAnDQE/BmCoHKV+HkdoxkR6F15/l27drFixQocDgdBQUHccccdeHt7cz48/fwlAAnRiBXlZLNr9crK16qqsmbe20R16d4s/+2mF5Sx80Re5WuHCk9/vY+r2wYQYqp5YuX8pUtxHE2sfB2bqLIvCqctuAoXGIRSU1PZsmULWVlZOBxV55tsbE+N6XQ63nzzTbp27UpWVhbdu3fn+uuvx93d3dmliUukuPgwKSnzAcjceQfp+zIACO3mznzvl0g0H8VT78lrA15DlxyG+/rjeKHB4pJNW9c5OF5LIPWHAFC1mKJK8W5vgJaDoUUca9dv/jMEVVLw1Ve0AtntdtasWcNvv/0GQPv27Rk5cqSMlxPiMpSXnlZtDpzm/GBDYnYJ6l+22VWVpOzSGoOQNSOD9KnTqmwbuVUl7bqO1WazbkjnHYTmz5/PuHHjMBgM+Pn5VRk1ryhKowtCISEhhIRUrPQdGBiIr68vubm5EoQuE6rq4NChqaiqnaLUbqTva4urp57QYVqey3yUInMRLTxaEB83GY+t+zBvLUKLKwblAKHqNJSsMlJ/9cNu1mIIMRE8+z1ofSVodRTlZLP328SKH3xn3ednnoIoKytj6dKlHDt2DID+/fvTv3//KrNECyEuHz4hoRXrD8qDDQBE+7ujUSpags7QKgpR/m417m9JOgF/aTzRqBCa59yn7877J/bUqVOZOnUqBQUFJCUlkZiYWPnn+PHj513Apk2bGDFiBKGhFTfYN998U22fd955h+joaFxcXIiLi2Pz5s3nfR6A7du343A4CA93Tj+kuPSOHvqcgsLtOKxGMnfeTsuuAXjdlcNTJ/+PIksRXXQmPj2ZSvhHj2HbGlYZgvx84tF0vZls7qI004ji6krYvE/QxPSH03ODrJ3/Huu7X1PZHQZ/PgVhKC7kgw8+4NixY+h0Om699VYGDhwoIUiIy5innz+DH3wY5fS/8+b+YEOIyZUXR3VCe/o/ilpF4YVRsefsFjNERcJffkbaFSgJ8qr3Wmtz3i1CpaWl3H777ZfsB35JSQldunThvvvu45Zbbqn2/pIlS5g4cSLvvPMOV155Je+//z7Dhg3jwIEDRERUTMAUFxdHeXn1iZx+/PFHQkMrknpOTg733HMPH3zwwSWpWziX6lDZtW4f2daX0Roh78iN9L8ujB/ML/Jhwl4AhhWX8Hx2Moq9NacsM1HxIFeTTfg9cWhjdlOydSvZT98PQPC0qRhbt648/pGtv/ClWSEtOAJ3Bb7s1pYyh4NoVyOlJ5P5YOFSzGYzXl5e3H777ZX3mRDi8iYPNlR1W88Irm4bQFJ2KVH+bucMQQD64GBCnptB+rNTK7sYv7wSyv08GqrcGimqqv61i69WTzzxBL6+vkyePPnSF6MoLFu2jJEjR1Zu69WrF927d+fdd9+t3HZmHMaLL75Yp+OWl5czePBgxo4dy9133/23+54dqgoLCwkPD6egoAAvL+emVlGh4FQZ6xYexOE1B+9Wm3EUB9L1gJUXvfJY417RJDsur4D/U3ywBN1K2p4r0Tt07FfstBrfjdZhJmynTnH85lHYs7Mx3TKK0FmzKo9fXlrCa89O5t3r7saqN/By2zDuaeGPqqps3bqV1atXo6oqYWFh3HbbbXh6ejrrSyGEEE1O6hNPUrh8OVAxq/Svd3flX09/fsnPU1hYiMlk+tvf3+fdIvTiiy8yfPhwfvjhBzp16oT+L5NKvf766+df7TlYLBZ27NhRLXQNGTKEX375pU7HUFWVMWPGMGjQoL8NQVBxfTNmzLigekU9KUiF3GOopgj2b0pmy5pydN5JRHWr6CJtc/woj/j6sM/ohh6FGaHXMmL4eCxlgaS+txuDQ2UPNrzv7kDrMBOq3U7q4//Fnp2NsU0bgp95psrpNn32Md90HYBVb6CvyY27Qv2w2Wx899137Nq1C4AuXbowYsQIdLqmPcW+EEI0JGtGBoUrVlS+1qjQ55PdWO9vQouuvvDCC6xevZqYmBiAaoOlL6Xs7GzsdjtBQUFVtgcFBZGRkVGnY2zZsoUlS5bQuXPnyvFHixYtqlwm5K+eeuopJk36c32qMy1Cwkl2LoTlj1Bs92V9wf+RbOkOCkTHVXRxuhW48KC/H+n2MryNJt4cOJu4oDgsKUWkzd2DwaayGxulI6K5vkPFfZQdH0/p1q0obm60mP0mGtc/m3JTDx1g0ckskvv3xQi83j6S0pISvvjiC5KTk1EUhcGDB9OnT5/LYnp9IYRoSJakE9WevNM41Ka16Orrr7/ORx99xJgxY+qhnJr99RfOmaUK6uKqq66q9oh/bYxGozz63AgU55nJT0zBtGwGaZar2VQ4FovqjhYLXXovpNQ7GzRuTDG7cspeRpRXFPHXxBPhFYElpYiMeXvQWR3sxsaBvoH898qoiuP+vIXsd98DIGTGdIwtW1ae026zsnTRfDb0qxir9nTrUFwK8pj3+ecUFBRgNBoZPXo0bdq0afCvhxBCXA4qB0yf9XvZoVGa1qKrRqORK6+8sj5qqcbf3x+tVlut9ScrK6taK5G4fBzYksaGTw6d/k/DXM4sZhOoP0K/gNkcilBAhS9y7JyylNEzuCdvDHgDk9GEJaWIzA/2orFUhKBV7Tx4a3gHAKyZWaQ98QSoKt633oppxIgq59367VKWtInDYnShm7uRK4uy+XDZMqxWK76+vtxxxx0EBAQ07BdDCCEuI2cGTKdNnYbicGBXYP99V9LRSa1BcAGPz0+YMIG33nqrPmqpxmAwEBcXx5o1a6psX7NmDX379m2QGkTDKs4znxWCoCIEqXR1+5pbfCeTFVOEXTWTVK7h12INI1uP5P1r368MQVkf7EUpt5OAjfkhOl75Zzc0GgXVZiPtscew5+ZibNeOoClPVzlvbtpJ5u8+wPHIduhRuas0m6VffIHVaqVly5b861//khAkhBCXgPfo0ZQufp3p/9Qw/v+0ZFwT69R6zrtF6Pfff2fdunWsXLmSjh07Vhss/fXXX5/X8YqLizl69Gjl68TERBISEvD19SUiIoJJkyZx991306NHD/r06cPcuXNJTk5m3Lhx51u6aALys8r+2n0MKEQZd5LuZ+BUgAGHCl/mGXik+0QeiH0ARVGwpBRx6oO9cDoEveblYPF9PXEzVNzip956m9Lt29G4udHijdfRuLhUHl11OPh6/jzW9BkGwLVFpziys2Iw/hVXXMF1112HVqttiMsXQohmQRccxIHIirYYvUb/N3vXcy3n+wFvb29Gjbp0U2Fv376dgQMHVr4+M1D53nvvZcGCBdx2223k5OTw3HPPkZ6eTmxsLKtWrapczLK+xMfHEx8fj91ur9fziKpslupfb0WBspGPsbvwGbyxsKXEyKS+bzAkqmKhw/LkQrI/3IdabmcXNqbry1k0pi9BXhVhp3jzZnLefx+AkJnPY4yOrnL8fRvW8nGLjphd3PAryids168oisLw4cOJi4ur5ysWQojm5+zw4+wgdN7zCDU3dZ2HQFy8nLRivnltF+YSKxUzOSsoCrQd6cVm64Nc7VZAoV1Ly06L6BLcC6gegiZTyltjejCoXcUYMmtGBokjb8aen4/3HbcTMq1inRur1UpqaipH/zjMp3sP8X33/iiqg1t2bsS/uABFUZg4cWKtK8YLIYS4MIdzDzN6xWgAHot7jDGxYy75OeptHiEh6kPBqVKWz07AXGIlUH+Ea7zmUHbde+z1tfDq3geZGFgAQPu2U2lXQwhKwMZ/KeXJER0qQ5BqtZI66THs+fnQKZb8W25h35o1JCcnk5qaisPhoFynZ2OPawDomnwE/+KK86iqSm5urgQhIYSoBzrNn/FDr20CXWPdu3fnp59+wsfHp04Hveqqq1iyZAktWrS4qOJE81CUa+bbNxIoLbDga0hlhPfzGPvczXLfY7y+4zX+7W9Gr4DJuw8xEXcCVUPQbsXO42op/+gTyZi+UQDk5+ez7733SFQUsq8fRoGXFyxdWuW8rkYjG0JbUWp0wbukiLgThyvfUxQFX1/fBvsaCCFEc1IlCDWFMUIJCQns3r27zr8YEhISalz7S4i/Ki20sHx2AkW5ZkxuRfTxmsbOQH++cbXw/Y7X6Opqo52rA0Ux0KHdTBRFqRKC9mkdPGYv5qooF24ILuWrr74iOTmZwsLCihO0+XP9MH9/fyIiIoiIiCAkOIhX5r7PoYi2KKrKs4HuHEWt6JBTFEaMGCGtQUIIUU8a0xihOneNXXPNNdR1OJHMuCvqwlxiZfmcBPIzS/HwVDH6PsNNIW44FDuc+BGjonJ3oBEcFqIix+HmFkX5iUIyP9pDliWPvYZ8flGzGa0vQZdh4/vv/zy24nDgk5dHmL8/7f/xDyIiInB3d698//tFH7K86wAA7g/15c52kRTEtCI3NxdfX18JQUIIUY/ObhE6++/OUKezJyYmnveBw8LCzvszovmwmG2sfHs3OSeLcfXU0dfvae7yd8HP7EmxvpgyXRnXm6xoHWW4uERgtV7D6q9Xkbj7D05RiN1YMSvpmc5XvV5PeHg44S1a4PLpZ3hs3YpH+/ZEvvYaGoOhyrkzjh1hToGNwjBvWmhUnm5Tca+aTCYJQEII0QCaXBCq70fVGyN5fL7+2Cx2Vr27h8zEQozuOm7q+C0/ndQy9OT1KCjoDcXYQ/bQx+MYANu2tSE/7/T4ntONjWZVR4bDk7Yto7h3SA+Cg4PRarVk/u9lcn/5BY2nJy3efKNaCLLbbLz35Rfs6nUDALM7t8Zd5ggSQogGdXb4UXBuL5I8NXYO48ePZ/z48ZWP34lLw25z8MO8faQezkdv1DJieAG6jYs5wAMoKAQFH6FNm98407taVOhLfl4oXqobQXYT5aon7ziMZKtGQGHzHwr3jfJFq9VStG4dufPnAxDywiwMNbRK/rJqOYvbVTx1drufB1f5eDbUpQshhDhNpzSe+NF4KhGXPYdDZe38A5zYm4NWr+GGByIJ+uEavtcHgFWDwVBSJQQBeHrlc5u+E55FgaR56bi7MJezh+HbVZWk7FL8inJJm/wUAD733I3X4MHVzp+fmcGrSRnkd2qJP3ae6xBdbR8hhBD1r8oAaScPKz7vtcaEuBCqQ2X9J4c4uiMLjVZh2LhOtDg0jXxzLh97VYz0cXUtovo4ewdaXQ55AS7VQhCAVlGINOlJnTQJR2EhLp07E/T449XPr6p8uPgzfu/YG4A3OrXGSyddYkII4QyNqWtMgpCod6qq8vOXRzj0SzqKAkMe6EikuhH1wDdM8w0jKq8TAOXlbtXXGVM1mN0iuPVUFuXA1W380Z7+N6NVFF4YFYt2XjzmPXvQeHnR4vXXUf4yLghgz+YNzA/riKrRMMLLyGB/6e4UQghnOfvp8oLyAidWIl1jogFsXX6cPetPAjDonva0agvEP8YSDw+sxf3wcbjg7e/NgKuKycnlzOoaoGrwPf4At6XaMQP39olk+o0dySg0k5RdSpS/G+6//0zqxwsBCH3pRQxh1SfxLCsqZNauA2R3vhKTw8ZLnZy70rEQQjR3Xx/5c4H2mb/NRKfRMarNpVvH9HycVxD67rvvWLZsGb6+vtx///20a9eu8r28vDxuueUW1q1bd8mLFE3XztUn2PH9CQCuvr0t7XoHw5K7OGIrYpmxP+2LA1G0CjdcF0hyylugKgTv+Td6iwl9aRCach88KGbkFWFMG9ERRVEIMbkSYnLFkpJC4tNTAPC97z48Bw2qsYZPvljMz6e7xP7XIQo/g+R/IYRwloySDGb8OqPytYrKjF9n0De0L8HuwQ1eT527xj777DNuuukmMjIy+PXXX+nWrRuffvpp5fsWi4WNGzfWS5Giadq74SS/Lqt4BL7Pza3oNCAM9i7FfPg7pnvHEJPfEYBhQ3uQmvY/AHwTh2PK7I1bXnv05b5oUfhnmyBmjeyERvNnU6rDYiF14qM4iopw7dKFwEmP1ljDsT0JvO3RAodWy0AXDTcF+9XzVQshhKhNcmEyDtVRZZtDdZBSlOKUeuochF599VXeeOMNVq5cyebNm1m0aBHjxo3jww8/rM/6nCY+Pp4OHTrQs2dPZ5fSJB36LZ1Ni/8AIG5YJN2vi4TCdFj1OK96BxKa1xcNGtq0jwbmYbcX41oYg/+xkVWO4wDGj+pYJQQBZL38Cub9+9GaTLR443UUffUp2q2Wcp7b+AuZgS1ws9t4s3t7mfVcCCGcLMIrAo1SNX5oFA3hnuFOqafOQeiPP/5g+PDhla9Hjx7NihUrePTRR3nvvffqpThnGj9+PAcOHGDbtm3OLqXJObYzi3UfHwSg88Awet3YElQVVk5knVJOoqUf7jZ3XD1d6d45iaKivWhtHoTsehC7qxE7FSOmHYD3za0x+LhUOX7hD6vJ++QTAEL+9xL60NAa6/jqm2WsbVcRZJ9r04Igo3PXsxFCCAHB7sFM6zOtMgxpFA3T+kxzSrcYnMcYIS8vLzIzM4mO/nPulQEDBrBixQqGDx/OyZMn66VA0bSc2J/Djx/uR1WhXd8Qrrq1TUUrTMJnZB5bw3umPnTIDQcFbrohitS0pwEI3vsAijGYu8oKKMfOiHBf/nt7F1z9XKsc35KcTPozzwDg968H8BwwoMY6MpOO86rdFbtOzxVaO3dGOOcfmBBCiOpGtRlF39C+pBSlEO4Z7rQQBOcRhK644gq+//57evfuXWV7//79K8OQaN7SjuTx/Xt7cdhVWnUPZOBd7VA0ChSkYv9+MtN8ommb0w2AAQO6kJk1EwCfE4NxLe3J/dZCTlpt9GvjzxP39MBFX3WeH0d5OScnTsRRXIxr9+4ETJhQYx0Oh53p36/hZLteGO023unTWbrEhBCikQl2D3ZqADqjzl1jjz76KC4uLjW+N2DAAFauXMk999xzyQoTTUtmUiEr4/dgtzqI7OTH4Ps7VIzrUVVY8QgfGTW4FPZDp+oIiQjCw/1zbLZ8jAVR+Cbfwf9ZizhmsdK7pS9z764eggAyX3qJ8gMH0fr40OL112ocFwSw+sfVrGzVFYCnIvwJc6k+r5AQQggB59Ei1L9/f/r373/O9wcMGMCAc3RTiMtbTmoxK+YkYDXbaRHjzdCxsWh1pzP2rkXsTd7EZuN1tLR4ozPq6Ncrn/TMHWhsLoQcHM/jqoX9FitxkT58eG9PXA3VQ1DhqlXkf74YgNCX/4c+uOb/RRScymJGjhlrqJFOajkPtml+CwYLIYSoO5lZWlyU/MxSvp2dQHmpjaBoL67/T2d0Z4JMfgrFq5/mRc/OtCxqDcCN17chPaPiScPAQ/cx3erFdrOFLmEm5t/XE3dj9WxuSUoi/ZlnAfB78EE8+vWrsRZVVZm54juSQluit9t474rOaKRLTAghRC3OOwi9+uqr9VGHaIKKcs18++Yuygot+LXwYPhDXTC4nA4yqgrLH2KWuzeReVcAEHdFW/JOvQKKildqf2bndWNzWTkdQrxYeH8vvFyqd3U5zGZOTnwUR2kpbj16EPDIw+es59fffuWL0IpJPh8J9KSVh+s59xVCCCHgPIPQ5MmTmTdvXn3VIpqQkoJyvn1jF8V55XgHuXHjhK64uJ8VZHbMZ3nGNgpL+2N0GDEFeBGkW4ZNk4ehuAWLT/yDH8vMtA3y4JN/9cLkVsM8QBkZnJz4KOWHDqH19SX0tddQdDX35pYVF/HU0TTKja60tpbyaGzb+rp0IYQQl5E6jRFSVZUHH3yQtWvXsmnTpvquSTRy5hIrK+YkUHCqDE9fF26c0BU3r7MGJOclkbJ2KkuMfWlTEICiUxjYqpAc3U4Uu4HNfzzIV8V2Wga48+m/euPrXn0wc/7SpaQ/O5Uzq7B63TgCfVDgOWt6ZcV3HA5rh9Zh572eseg00iUmhBDi79WpRWj06NGsWrWKNWvWEB7unJkfG5rMLF0zS5mNFXMSyEktwc1k4KZHu+Lpe9bThA4H1m/HM909ktYFFUtoXNshlBxdxXIsx47/k7nZPkT6ufHZv3oT4Gmsdg5rRgbpU6dx9lL0eQsXYc3IqLGmvfv3Md9UcV8+4Kkj1sfrUl2uEEKIy1ydgtCyZcuYMWMGrVu3ru96Gg2ZWbo6q8XOd+/sIetEES7uem6c0BVTgFvVnbZ9wFt5h/HLvxIFhbahQdiM80DjIP9UL2YldqOFtyufje1NsKnm6RjKk5LAUXUdGhwOLCeSq+1rs1p5fOdBylzdCS8v5pkenS7R1QohhGgO6hSEJk6cyGOPPSahoBmz2xz88P4+0o7kY3DRMuKRLviFelTdKecYv22ayUHrQNzsbri5GIn2+gmbaw7WskCm7BlFsJcrn43tRQvvcw9kLj90uPpGjQZDZES1zfGrfmB3WBsUh4P4bu0waORBSCGEEHVXpzFCr7/+On5+fgwbNoz169fTqZP8r7s5cdgdrPlwP8n7c9DpNdzwUBcCI//S/eRwkPftON4ydKZNXgtQoJ8pn9LAXagOLS8m3IOHqzefju1FpJ/7Oc9lzcoi+513Kl4opydk1GgIeW5GtbmDEk+c4C2tNwC3661cEeR/KS9bCCFEM1DnCRWnTJlCQEAAQ4cOJTU1tT5rEo2I6lBZv+gQx3adQqNTGPafToS29q6+32/vMqM4l5b51wPQ28WDsjafA7Dkj5socrRm8YO9aBXgUe2zlcdQVTJmPIejsBCXjh1pMftNrKlpGCIjqoUg1eHg0S3bKQ5pRWBZMbOG9Ll0Fy2EEKLZqHMQAnjwwQfx8/Orr1pEI6OqKpuX/MGh3zJQNArX/SuWiA41fP+zj/LZ1tfRlN6EVtUSrvHErcNSrBobCVmx/H7qWj4f24u2QZ61nq/o++8p/ukn0OsJeeEFDGFhGMLCqu+Xk82Cn37it5AYUFXe7BCJm676bNRCCCHE3znvARW33HJLfdQhGqHfvjnO3o2poMA197anZdeA6js57Pzxzb9Ya78SL6sXrqqeDi13YHXPIrfMhy+O3MuiB3rRIbT2J7lsublkPF+xCKv/v/+NS0zN8wDtXfcj8RP+zdvGikfpB+UkMyiqeTzJKIQQ4tKTkaWiRtu/T2Ln6hMA9L8jhpheNa/tZd7yJi+U6ogsbgkqXOlfSmnoVuwODQsO3sc79wykc5j3354vc+Ys7Hl5GNu2xf/BsTXuU5STzZq5b7Hpimsp8PLBqyiPzssXUZSTfcHXKYQQonmrcxCKiIggJyen8vXbb79NYWFhvRQlnGvP+hS2fnscgL6jWhN7dYuad8w6xCs7PyYsrxcA3YzulLf7GoDvkm7g2VG3Exfp87fnK1q7lsJVqyoGRc+ahWKoebX43LQUUoLC2Rlbcb7rNn6D3mImPyPtfC9RCCGEAM4jCJ08eRK73V75+umnnyY7W/4nfrk5+Esam5ccAaDHDVF0G1L9kXUA7DbWfvsAOSUD0al6gnDDFLsCtBYO5sYw+uqn6d3y78eT2QsKSJ8xAwC/B+7HtVNsjfupqsquDetZ3f9mUDR0OriDqJPHUDQavINDL+xihRBCNHsX3DWmnjXrr7g8HN2RxfpFhwDock04VwyPPue+GZte4PPCSLwtPuhVLR1bHcDukUqhxZMO7V+jX9tzL4dxtsyX/of9VDaG6Gj8x48/534/L17IfIuWXJ8A3EsKGfDr9ygaDYPHPoSnnzw2L4QQ4sKc11NjzUl8fDzx8fFVWsEuZ0l7s1nz4X5UFTpcGcKVo1ujKDWv12XP2MusPesJL+oPwBV+FqwtNuNQFdwDn2dQx/Z1Omfx5s0ULFsGikLIrJloXGqeaXr7iq9Z/vMWfh/1bwBe7RBNj8lT8Q4OlRAkhBDiopxXEPrggw/w8KiYB8Zms7FgwQL8/av+InrkkUcuXXVONH78eMaPH09hYSEmk8nZ5dSr1MN5/DB3Hw6HSpueQfS/s905QxB2Kx9+838E5Q3GhoMYgyuWmIW4AHa3uxjafUSdzmkvLq5YTwzwufsu3Lp3r3G/fRvWsu6zj/lh1DhUjZaRgd7c3DIckCfFhBBCXDxFrWMfV1RU1Ll/OZ45mKJw/PjxS1JYY3EmCBUUFODldfkt5pmRWMDyNxOwltuJ6uzP0H/HotWeu8c0YdUTrN1moli14o2RwC4/EmRKxqbtxOB+S9Fo6pat06dPJ3/xEvRhYbRc/i0aN7dq+xzd9hvLX3+BLV2vZssV1+Kr17Lpivb4G6QhUwghRO3q+vu7zr9RkpKSLkVdohHJPlnMyrd2Yy23E9bOh+vGdqw1BBWe+JVft3tRrFrRqApK1GGCTMk48KR/r3fqHIJKtv5O/uIlAITMfL7GEJSyfw8rZ/+PUyZ/fus5CIBZbcIkBAkhhLikZB6hZiovo4Tls3dRXmojuKWJYeM6odOfe3Zmh8XMD5+u5dTpMVOKyUxs5GYAunZ6FReXuj255SgtJf2ZZwDwvu023Hv3rrZP5vGjfPPK81htNtYPvxu7omGInxcjA73P8yqFEEKI2tU5CG3dupXvv/++yraFCxcSHR1NYGAgDz74IOXl5Ze8QHHpFWaXsXx2AmVFVvzDPRj+UGcMLrW3tGx9fx6pZiMORUWjt9Opw3cAhIXdS0DAtXU+96nZc7CmpKALDibwv49Xez83LZWvXpyGpayMY9feQpK7D146Df+LCfvbrlkhhBDifNU5CE2fPp09e/ZUvt67dy8PPPAA1157LZMnT2bFihW8+OKL9VKkuHRKCsr5dnYCxXnl+AS7ceMjXTG66Wv9TPLXazmeaaBAU4qCg8B2v+FpKMHTsyNtWj9Z53OX7tpF7sKFAIQ8NwOtR9UFWItyslk66xnKCgvQduzG9627ATC9VQtCjDVPsiiEEEJcjDoHoYSEBK655prK14sXL6ZXr17MmzePSZMmMWfOHL744ot6KVJcGmXFFpbPTqDwVBle/i7cOKEbrp61B4yCLUkc357HEV06oGIJS6WNz3G0Wg9iO85BozHW6dyO8nLSpzwDqopp5Eg8rr66am1FhSyd9SxF2acwhbRg/bA7KVdVrvbx4I4Q3wu9ZCGEEKJWdQ5CeXl5BAUFVb7euHEjQ4cOrXzds2dPUlJSLm114pIpL7OxYs5uctNKcDcZuGliNzx8ag8xpQlZnFx5iC36ikkWC70KGBi9EYB2Mc/j5hZV5/Nnx7+D5fhxtAH+BE2u2opkKSvl65emk5uagoefP7b/TGZrsRlXjYZXYsKlS0wIIUS9qXMQCgoKIjExEQCLxcLOnTvp06dP5ftFRUXo9bV3sQjnsFrsfBe/m1PJRbh46LlxYje8/F1r/UzZwRyylhxgg/4AVsWO1VDA4M4/oygqoSH/IDj4xjqfv2zffnI+/BCAkGnT0Hp7V75ns1r59rUXyDj6By6eXrR5dCovZVSsYTelVQiRrnVrcRJCCCEuRJ2D0NChQ5k8eTKbN2/mqaeews3NjX79+lW+v2fPHlq1alUvRYoLZ7c6+P69vaQfLcDgquPGR7riG+Je62fMx/LJ+eQgO7THOaUpxK5Y6NI9CYMmD3f3NrRtO7XO51ctFtKnTAG7Ha/rh+F57Z8Dqx0OO9+/9SrJexPQG11QHprCP08WYnZUTG3lopGWICGEEPWrzkFo5syZaLVa+vfvz7x585g3bx6Gs1YJ/+ijjxgyZEi9FCkujMPu4McP95NyIBedUcvwh7oQEOFZ62fKkwvJ+fgAJ9Vs9uqSAQhvn4+fYQ8ajZHYjrPRamtvTTpb9rx5lB8+jNbHh6DTj81DxVp1az94hz+2bkGr09H10Wd4Ls9a5bNPHj5JmtlyHlcshBBCnJ86z04XEBDA5s2bKSgowMPDA6226pwzX375ZeXyG8L5VIfKTwsPcjzhFBqdwvX/6URIq9qXCrFmlJA9fz8lllLWulQ8IWgKPEUr/7UAtG07DQ+PmDrXYD78B9nvvQ9A0JQp6Hz/HPT88+KF7P1pNYqiodVDTzKhVA9UDUJ2ILGsnFAXeWJMCCFE/TjvaXrPte6Wr6882dNYqKrKxsV/8MfWTBSNwtCxsYS3q/37Y8su49QHe3GUWVnlsgMbDnSGPHp03o3FYicocDihIf+oew02W0WXmNWKx6BBeN1wfeV721d8ze/ffImKQv7Y//J/5W5YVWu1Y2iBaBkjJIQQoh7JzNLnEB8fT4cOHejZs6ezSzkvqqry69fH2L8pFRS49r72RHcJqPUztvzyihBUbGWTYS/5lKFiZfCgXCyWdFxdI2jXbuZ5Pb2Vu2AB5n370Hh6EjxtWuVn961fw8ZPPqLY1YOf/vUU8zSeWFWVGwJMPN86lDPtjFrglZhwaQ0SQghRr+q86Gpz1dQWXd32XSK/r6h4um/gXe3ocFXtS1/Yiy2cen8PtlNlHNSdZLPuIBo0XN0rA9W4BkXR0yPuC7y8Ote5hvLjiSSOHIlqsRAyaxbet4wC4Mi2X1nx2oscC2/NmuvuoFCrx1Wj8HybMO4M8UVRFNLMFhLLyol2NUoIEkIIccEu+aKrovHb/VNKZQi6cnTrvw1BjjIb2R/uw3aqjDRNMWuNCbjaXYkISgGXLaBC69ZPnlcIUu120qdMQbVYcL/ySkyjbgYqFlH99u3XWN9nKDs69wWgo4cL73aIoq27S+XnQ10MEoCEEEI0GAlCl4kDW9L4+csjAFwxIpqu10bUur/DYid7wX6s6SXkKjY+8VqJrzkAnT6fmM7HMJdb8fe/lvCwMedVR96nn1G2axcaNzdCnpuBoihkHj/KB3Pf4evhD3DKPwSAsWH+TGkZikstq90LIYQQ9U2C0GXgyLZM1n9SMftz18ER9Lg+qtb9VZuDnEUHsJwopBiVt3yW0KI0GBU7g69Mpbg8BaMxmA7tXzqvcUGWlBSy3ngDgMD/Po6+RQtyUlN4+ouvWH3D/dj0Bvz0Wma3j+Rav8bfzSiEEOLyJ0GoiUvck83a+QdAhY5Xt6DvqFa1hhfVrpLz+SHKj1QMiZ7huY5WZf4A9Gp7kGLHLhRFS2zH2ej1PnWuQ1VV0p+dilpWhlvPnnjfdhvJGZmMWbeVA72uA+Bqkxtvd4wm0CgzkAshhGgcJAg1YSmHclk9dx8Oh0rbK4Lof3vb2kOQQyVv6R+Y9+dgReVpw3FaKHnoVB/8vY7jGnoQhwOioyfg7d3jvGrJ//JLSn/7DcXFhZCZz7MhPYtxu49SENYarcPOkxEBPNQ6HI2sGyaEEKIRkQEaTVTG8QJWvbsXu81BdBd/rrm3PUotS1Koqkr+imOU7srCjsqzSgFuXhvxtvigaEvoHpeEw2HGx6cvUZHjzqsWa3o6Wf97GQCfiRN4tVzhn4fTKHD1wK8ojy/bBvFImwgJQUIIIRodCUJN0KmUIla8tRtbuZ3w9j5c969YNH8z6LjwxxOU/JqOA5WZlJEfvILo4kgAru26izJ7Knq9Hx07vI6iaGs91tlUVSV9+nQcJSXkXXU193fsw5zUXFRFQ5dje/i+ayv6hodd1PUKIYQQ9UWCUBOTl1HCijkJWMpshLQ2MWxcZ7T62r+NRRtTKFqfAsBrmDkUvINOBX4AdAzZitk9EVDo2PF1jMbaJ1/8q8LlyynZuIl1vfpx713/YUdxGcbyMkZu+JqPB/QmIqL2p9eEEEIIZ5IxQk1IYXYZ376ZQFmRlYAIT24Y3wW9sfbWm+Lf0in4PgmAdzHzm082Pc1pGB3+eLgmE9A2BbsKkZHj8PO96rzqsZ06xfFXX2f23f/mh74DQIXQjBPcuGEZ9z00keDWbS/wSoUQQoiGIUGoiSjOK+fbN3dRkl+OT4g7Ix7pgtG19m9faUIW+d8cBWAh5az2tBPntQn/zGBUjZm+3fZSppZhMnWnZfTE86pHVVXWzXmHpx6aTGpgCIqq0nvHeq7cuYmbJj5BZKeuF3ilQgghRMORINQElBVZWD57F4XZZrwCXLlpYldcPWqffblsfw65Sw4D8BUWvnJTGdZpB5qdgQD0b72OMl0uOp2J2I6z0Wjqfis4VJU3127m9atvwKbT4W8rZ/DKhYRlnGDwgw/TplffC79YIYQQogFJEGrkykutLJ+TQF5GKR4+Rm6a0BV3U+0rspuP5pHz2UFQ4QcsLHC18/gNdhK+z8cdd6L8f0cNzgGgQ/uXcHGpfSmOs2WWW3l47zE26SomROyTmUyP7xbiYjHT759j6HzNdRd+sUIIIUQDkyDUSBXnmclJLWbr8kSyU4px9dRz44SuePm71vq58hOFZC88AHaVjVh5y2jj7bva8vk3rxBoC8Tgkkl0+0RsQFjYPQQEDKlzTWuyC5hwKJlcqx2XcjNjNv+A57GdoDroMWIUV9w0+iKvWgghhGhYEoQaoQNb0tjwySFUteK1zqDhxgld8Ql2r/VzlrRisufvA4uD37Hxit7CR/dfwaebZxJYFIiKjf4dNmJWLHh6dqRN68l1qsdsd/D8sTQ+TM0GoHVKEg8vnke6tx676iB24BCuvvO+i7pmIYQQwhkkCDUyxXnmKiEIwG514OJe+7IU1lOlnPpwH6rZzh5sPK8rZ+79V5CQtgz3YxUBqnfUD5g9ytFq3U+PC6q9iw3gcImZ/+xP4kCJGYBbf1nPP5Z9wq42YdjtNlr37MPgsePPa00yIYQQorGQeYTOIT4+ng4dOtCzZ88GPW9+VlmVEASgqlCQVXbOz9jyzZz6YC9qiZU/sPOM1kz8mB64uaexf/1+tKqWIN89GMLzAGgXMxM3t+ha61BVlYWp2QzdfpgDJWb89Tre2rWZe76YT0J0MFaHnYjYztzwyH/RaOs+AaMQQgjRmEgQOofx48dz4MABtm3b1qDn9Q505a+NK4oGTIE1jw2yF1k4NW8vjgILJ7AzWWPm9Xvi6BrpxrtL3sXL4oXWmEfHmL2gQEjIrQQH31hrDXlWGw/sS+KJP05S5lAZ6OvJSrWAVvPf5/eWoVg0CkEt23DT48+gM9T+9JoQQgjRmEkQamQ8fFwYcFc7lNPfGUUDA+5sh4ePS7V9HaVWTn24F3uOmQwcPK6UMeuubgyICeTllS8TkBMAqPRv8z1WvQM3t9bEtJ1a6/m35BUxaNthVmUXoFcUprcKZWHLIIqnP8vvLUMxG3T4hIYx6qnpGFzd6uErIIQQQjQcGSPUCHW4MpSIDr4UZJVhCnStOQSV2zk1fz+2jFJycDCJUp79Z1cGdwhixb4VWPZaMGCgc8QarL52NBojnWLnoNXWHF6sDpXXkjKYfSITFWjlauTdjpF09nQjZdo0fnPTUOJiwMPHj9FTnsPNy1TPXwUhhBCi/kkQaqQ8fFxqDEAAqtVBzsL9WFOKKERlEqU8dntnru8UQmphKmu/W4uPwweT6QjekRXBpm2bqXh4xNR4vBNl5fzfgRPsKCwF4J8hvjzfugXuOi1Fv/7KT9s2U+DphouLK6OfnYmXf2B9XbYQQgjRoCQINTGq3UHOZwcpP1ZAKSqPU8q/R8dyU9cW2B12Xl7yMj5lPij6EuJifsWqKAQG3kBo6G01Hu/rzDyePJxCkd2Bl07DKzHh3BToA4CtpJgVLz9PjqcbOkXDLc/Owq9FeENerhBCCFGvJAg1IapDJe/LPzAfzKUclScp5Z8j2/OPHhXh5O31b+Od6g2oXNVmJVYXBVeXCNq3m1Xt8fZim52njpzky4yKJ8muMLkT3yGScJeKwc+qqvLdfyeSbtCgUVVufOwpWURVCCHEZUeCUBOhqir53x6lNOEUNlSmUsaI4THc1TsSgK3JW0n7JQ1XXGkdtgn8LSiKntjY2eh0nlWOtauwlP87kERimQUNMCkqmImRQeg0f4al9W++zNGcDFBVrhl6E9FX9GnIyxVCCCEahAShJqLwhyRKtmbgQGUmZfQb1pr7r6qYC6igvIBPv/wUX7svbl7JhEYloaLQutUTeHl1rjyGQ1V5JzmLlxLTsanQwqjnnQ6R9PL2qHKu35d9wa7fNgNwhX8LOt//YMNdqBBCCNGAJAg1AYXrUyjaeBKAVzHT6dqWjOvfCqhoKXpp6Uv4FvmCxkzvthuxahT8/QYRHv7nshcZ5VYePniCzXnFAIwI8OaVmDC89VVvgb3rf2Tz4oUAtC8oo+/7LzfEJQohhBBOIUGokSv+NY3C1UkAxGMmbEAEj1zTuvL9z7d9ju5oxbexV9tVWN3AaAymQ4eXK8cF/ZhdwMTTi6W6ajTMatOCO0J8q40bOvL7L6x5/y0AWmblcdVT09B6e9f/RQohhBBOIkGoESvZmUn+t8cAWEA57le14L/XxVQGmD+y/2Dnmp14qB60CPkdQ2AJoKFjxzfR630w2x08dyyNj04vlhrr4cq7HSJp4179sfzkfXv4bvbLqKpKWE4hPTr3xGtI3VemF0IIIZoiCUKNVNm+bHK//AMF+JJyynsF8dwN7StDkMVuIf7zeHysPhjdM2nd8hAOFFpGT8DHuyeHSsr4z/4THDy9WOq/wwN4umUIRk31ycQzjh3hm1eex26zEZRfTJdiKyHPPtOQlyuEEEI4hQShRsh8JI9Tnx1Co8J3WMiIC+DFm2KrdGW9tvI1fHJ8UDRW+rRbi12r4OPdm8jIcXycms20o6mYHSr+eh1z2kcwyM+rxnPlpKbw9YvTsJrL8Csx0zU5i5CXX0bn799QlyuEEEI4jQShRqY8qYCsj/ejcaisx8ofXX15+ZbOaM56tH3NwTUUJxSjR0+X1quxuzvQa02Etn2NB/an8H12AQADfT2Z0z6CAIO+xnMVZp/iq1lTKSsqxFvVEHc8DdOAAXgNv6FBrlUIIYRwNglCjYgltZiMj/ahtalsxca2WBOv39oF7VkhKLM4k1XfrsKkmvAP2otncMWEiMVRbzFsTw7p5Vb0isIzrUIYGxaA5q9L2Z9WWljAV7OepSjnFF5uHvT4fS8GN3eCp0+rNohaCCGEuFxJEGok0o7nUrbgIEaLgz3Y+KmdB7Pv6IZO++eYHofq4JXFr2Aym9C75NGx1S4s6Fjr9SKfHHdHxUprNyPvdoikk+e5V4a3lJXy9YvTyU07iYfJhx47DmCwOwia/CT6oKCGuFwhhBCiUZAg5GS2gnI2rj2O77YsAtBwGDtzAzQsuas7em3Vgc3z1s/D/aQ7imKnd/sfyNAF857mKQ4XVSyxcWeIL8+1aYG7Vnvu81mtfPvqTDKPH8HV04s+xTb0JaW49+2D6ZZb6vVahRBCiMZGgpATlWzLIO/rI8SoABqycfAYpRRmq+SWWAgxuVbuuytlF4lbEnHBhZhW69ji2Yf5PEiZ6oZJp+XVmHBGBHrXej6Hw86qOa+QvG8PehdXru3aG/vb76K4uRH83PPSJSaEEKLZkSDkJLaCcvK+PgLqn9t8UNADDhWSsksrg1CJpYSFXyzEZDfhEnSCpaG38rMyAIBepxdLDTu9WOq5qKrKmrnxHPn9F7Q6HTeM+TfWx54EIPCxSRjCWtTDVQohhBCNmwQhJ7Fll1UJQQBaFMLQkKs4iPL/c4zPy1+9jKnIRLavgc0x/yBTCUGDg8eiQpnwl8VSz2Xz5x+zb/2PKIqG6x95Av37H2ApLcW1Rxw+d9xxqS9PCCGEaBIkCDmJzt8VFKqEITsq6ai8MCq2sjXoq+1f4Tissie8Dduj22FXdASQw7wu3ent61Onc21b/hXbvl0KwOB/P0RgShrpv/yKYjQSOnMmSg2TLAohhBDNgQQhJ9GZjPiMavNn95gCBVeFsuyqFpUhKCk3iXXrdvNb536k+gQA0NvxC+/1GEqwqW4haO+6H9n06XwArr7zPtp16MLxG4YDEDBhAoaoqEt+bUIIIURTIUHIidx7BmNs64MtuwydvythJmPlezaHjSeXL2Nr1xsw640YVTP38CH/F9WbYFPLOh3/yO+/sGbu2wD0vGk0PUaM4uR//g9HcTEunTvje+899XJdQgghRFMhQcjJdCYjurMCEECZ3cGotT+wK3IgAFGOY4xX3iROH0hIqwfqdNzkfbtPL6LqIHbgEPrdcS+FK1dSvGEDil5P6KyZKLU8Zi+EEEI0BxKEziE+Pp74+HjsdnuDnvdgcRn37DpAiiEMgCHlq/mn4SNMVi0xV/9Qp2NULKI6E7vNRpsr+jJ47HjsOTlkzpwFgP/4/8PYpk29XYMQQgjRVCiqqqp/v1vzVVhYiMlkoqCgAC+vmhcuvRRUVWVBWg7Tj6RSrqq4WsyMzl/C0IDlaBwqPWMX4BF89d8eJyc1hSXTnqSsqJCI2C7cPHk6Or2ekxMmUrR6Ncb27Yn+YgmKvub1x4QQQojLQV1/f0uLUCOQY7Hx2OFkfsguBCA8J4Mb0r+nX/sVgELb8o51CkGF2VmVi6gGtWzDTY9PQafXU/jDaopWrwadjtAXZkkIEkIIIU6TIOREaWYLy7PyiU/O4pTVhhYHvY7up2vGQfrGrQKNQmBWOaGHt8BVqWA696SHpYUFLJ01laKcU/iGhjHqqekYXN2w5eWR8fzzAPiN/Rcu7ds31OUJIYQQjZ4EISdZlJrNE3+crJxGyFcLV21fQ2hhKR07rkZxceBaZqf9kWIUVYXc4+cMQmcWUc1LO4mnXwC3THkeNy8TAJkvvog9JwdD61b4/+c/DXR1QgghRNMgM+k5QZrZUiUEAeTZHHiZVQIjDuDtl4/iUIk9WITOroKiBd+aH5m3WSxVFlG9ZcpzePlXzDlUtH49hctXgEZD6KxZaAy1L8MhhBBCNDcShJzgeFn5X1fXQFU0lPnaiYneC0DrpDK8im0VIWjEmzW2Bjnsdr47axHVUU/NwK9FxUr09sJCMqZNB8B3zBhcu3SpxysSQgghmibpGnOClq5GNIDjrG2K6uCqNptAteLnN5Dw7tMhL7GiJaiGEKSqKmvmxXN0269odTpG/vdZglv9+Uh81iuvYMvKQh8ZQcDDD9X3JQkhhBBNkrQIOUGoi4FXY8Irv/iK6uAfZV8RqBzFaAymQ/uXUbzDILrfOccFbf5sQeUiqjdMfJKI2M6V7xVv2UL+lxVri4XOnInG1bW+L0kIIYRokqRFyFkyVzNk1y7cDIFE+2yne8h2QEPHjm9iMPjW+tHfv13KtuVfATDk3w/TpmefyvccJSVkPDsVAJ9//hO3nj3r7RKEEEKIpk6CkBOs/3U9h1cfpldwGm3afI2iVGz38LkSH+/ag8vedT+y+bMFAFx91/3EDhxc5f2s19/AmpaGPjSUgEmT6qN8IYQQ4rIhQaiBFRQUsHH1RoyGUtq0+a0yBAEU523BbE7HxSWk2ueKcrLZt+5Hfln6OVCxiGrPEaOq7FO6fTt5n34KQPDzz6H1cK+/CxFCCCEuAxKEGlhubi4Arq5FVUJQBQdlZSeqBaG9637kx7lvwenVUFq060C/O+6t+kmzmfQpzwBgGn0LHldeWS/1CyGEEJcTGSzdwHx9fVEUhbIyT6qv8qbB1TWyypainGzWnBWCANL+OERxbk6V/U699RaWEyfQBQYS9MQT9VS9EEIIcXmRINTATCYTI0aMwGr14MiR3qjqmWYhDe3bzarWGpSXnsZf18VVHQ7yM9IqX5ft2UPu/AUABM+YjrYeF4cVQgghLifSNeYE3bt3p1WrVuTm5uLhYUWny8XVNbLGsUE+IaEoilIlDCkaDd7BoQA4LBbSp0wBhwOvESPwHDiwwa5DCCGEaOqkRchJTCYT0dHRBAS0xcend40hCMDTz5/BDz6Moqn4VikaDYPHPoSnnz8AOe+9R/mRo2j9/Ah6+qkGq18IIYS4HEiLUBPQadAQorp0Jz8jDe/g0MoQZD54kOy58wAIfvZZdD4+zixTCCGEaHIkCDURnn7+lQEIQLVaSZsyBWw2PIcMwWvodU6sTgghhGiapGusicr58CPKDxxEazIR/Owzzi5HCCGEaJIkCDVB5UePkh0fD0DQlKfRBQQ4uSIhhBCiaZIg1MSodjtpU6agWq149O+P14gRzi5JCCGEaLIkCDUxuQsXYd69B42HB8EzpqNUn55aCCGEEHUkQagJsSQlcerNNwEIfPIJ9MHBzi1ICCGEaOIkCDURqsNB+jPPopaX49anN96jRzu7JCGEEKLJkyDUROQtXkzp9u0orq6EPP+8dIkJIYQQl4AEoSbAcjKVrFdfAyBw0iQMYWFOrkgIIYS4PEgQauRUVSVj6lTU0lJcu3fH585/OrskIYQQ4rIhQaiRK/j6a0p++QXFaCRk1szKNceEEEIIcfHkt2ojZs3MIvOl/wEQ8MjDGKOjnVyREEIIcXmRINRIWdLTOTlhAo6iIlw6dcL33nudXZIQQghx2ZEg1AjlL13KsUHXYE5IAMCjf38UnayPK4QQQlxqEoQaGWtGBulTp4GqVm7LfucdrBkZTqxKCCGEuDxJEGpkLEknwOGoutHhwHIi2TkFCSGEEJcxCUKNjCEqEv76ZJhGgyEywjkFCSGEEJcxCUKNjD44mJDnZvwZhjQaQp6bIeuKCSGEEPVARuA2Qt6jR+N+1VVYTiRjiIyQECSEEELUk8s+CBUVFTFo0CCsVit2u51HHnmEsWPHOrusv6UPDpYAJIQQQtSzyz4Iubm5sXHjRtzc3CgtLSU2NpZRo0bh5+fn7NKEEEII4WSX/RghrVaLm5sbAGazGbvdjnrWo+lCCCGEaL6cHoQ2bdrEiBEjCA0NRVEUvvnmm2r7vPPOO0RHR+Pi4kJcXBybN28+r3Pk5+fTpUsXwsLCeOKJJ/D3979E1QshhBCiKXN6ECopKaFLly68/fbbNb6/ZMkSJk6cyJQpU9i1axf9+vVj2LBhJCf/Oa9OXFwcsbGx1f6kpaUB4O3tze7du0lMTOSzzz4jMzPznPWUl5dTWFhY5Y8QQgghLk+K2oj6iRRFYdmyZYwcObJyW69evejevTvvvvtu5bb27dszcuRIXnzxxfM+x3/+8x8GDRrErbfeWuP706dPZ8aMGdW2FxQU4OXldd7nE0IIIUTDKywsxGQy/e3vb6e3CNXGYrGwY8cOhgwZUmX7kCFD+OWXX+p0jMzMzMpWncLCQjZt2kRMTMw593/qqacoKCio/JOSknLhFyCEEEKIRq1RPzWWnZ2N3W4nKCioyvagoCAy6rj21smTJ3nggQdQVRVVVXnooYfo3LnzOfc3Go0YjcaLqlsIIYQQTUOjDkJnKIpS5bWqqtW2nUtcXBwJp1dxF0IIIYQ4W6PuGvP390er1VZr/cnKyqrWSiSEEEIIcb4adRAyGAzExcWxZs2aKtvXrFlD3759nVSVEEIIIS4XTu8aKy4u5ujRo5WvExMTSUhIwNfXl4iICCZNmsTdd99Njx496NOnD3PnziU5OZlx48Y5sWohhBBCXA6cHoS2b9/OwIEDK19PmjQJgHvvvZcFCxZw2223kZOTw3PPPUd6ejqxsbGsWrWKyMjIeq0rPj6e+Ph4bDYbgMwnJIQQQjQhZ35v/90sQY1qHqHG6OTJk4SHhzu7DCGEEEJcgJSUFMLCws75vgShv+FwOEhLS8PT07POT6qdS8+ePdm2bdtF13Qxxznfz9Z1/7/b70LfLywsJDw8nJSUlCY1oeWl+l431Hnknmr85J66+P3lnqrqcr+nVFWlqKiI0NBQNJpzD4l2etdYY6fRaGpNkudDq9Vekn8kF3Oc8/1sXff/u/0u9n0vL68m9QPmUn2vG+o8ck81fnJPXfz+ck9V1RzuKZPJ9Lf7NOqnxi4348ePd/pxzvezdd3/7/a72Pebmoa6HrmnLv48TYXcUxe/v9xTVTWne6o20jUmGrW6rhUjRF3JPSUuNbmnmjZpERKNmtFoZNq0abLsibhk5J4Sl5rcU02btAgJIYQQotmSFiEhhBBCNFsShIQQQgjRbEkQEkIIIUSzJUFICCGEEM2WBCEhhBBCNFsShESTdfPNN+Pj48Po0aOdXYq4DKSkpDBgwAA6dOhA586d+fLLL51dkmjiioqK6NmzJ127dqVTp07MmzfP2SWJGsjj86LJWr9+PcXFxXz88ccsXbrU2eWIJi49PZ3MzEy6du1KVlYW3bt35/Dhw7i7uzu7NNFE2e12ysvLcXNzo7S0lNjYWLZt24afn5+zSxNnkRYh0WQNHDgQT09PZ5chLhMhISF07doVgMDAQHx9fcnNzXVuUaJJ02q1uLm5AWA2m7Hb7UjbQ+MjQUg4xaZNmxgxYgShoaEoisI333xTbZ933nmH6OhoXFxciIuLY/PmzQ1fqGgyLuU9tX37dhwOB+Hh4fVctWjMLsU9lZ+fT5cuXQgLC+OJJ57A39+/gaoXdSVBSDhFSUkJXbp04e23367x/SVLljBx4kSmTJnCrl276NevH8OGDSM5ObmBKxVNxaW6p3JycrjnnnuYO3duQ5QtGrFLcU95e3uze/duEhMT+eyzz8jMzGyo8kVdqUI4GaAuW7asyrYrrrhCHTduXJVt7dq1UydPnlxl2/r169VbbrmlvksUTcyF3lNms1nt16+funDhwoYoUzQhF/Nz6oxx48apX3zxRX2VKC6QtAiJRsdisbBjxw6GDBlSZfuQIUP45ZdfnFSVaMrqck+pqsqYMWMYNGgQd999tzPKFE1IXe6pzMxMCgsLgYoV6jdt2kRMTEyD1ypqp3N2AUL8VXZ2Nna7naCgoCrbg4KCyMjIqHx93XXXsXPnTkpKSggLC2PZsmX07NmzocsVTUBd7qktW7awZMkSOnfuXDkWZNGiRXTq1KmhyxVNQF3uqZMnT/LAAw+gqiqqqvLQQw/RuXNnZ5QraiFBSDRaiqJUea2qapVtq1evbuiSRBNX2z111VVX4XA4nFGWaMJqu6fi4uJISEhwQlXifEjXmGh0/P390Wq1VVp/ALKysqr970uIupB7Slxqck9dPiQIiUbHYDAQFxfHmjVrqmxfs2YNffv2dVJVoimTe0pcanJPXT6ka0w4RXFxMUePHq18nZiYSEJCAr6+vkRERDBp0iTuvvtuevToQZ8+fZg7dy7JycmMGzfOiVWLxkzuKXGpyT3VTDjzkTXRfK1fv14Fqv259957K/eJj49XIyMjVYPBoHbv3l3duHGj8woWjZ7cU+JSk3uqeZC1xoQQQgjRbMkYISGEEEI0WxKEhBBCCNFsSRASQgghRLMlQUgIIYQQzZYEISGEEEI0WxKEhBBCCNFsSRASQgghRLMlQUgIIYQQzZYEISGEEEI0WxKEhBAXbfr06XTt2rXBz7thwwYURUFRFEaOHFmnffPz8xuktkutrl/jM18Pb2/veq9JiMuBBCEhRK3O/GI9158xY8bw+OOP89NPPzmtxsOHD7NgwYLK1wMGDGDixIlV9unbty/p6emYTKaGLe4S+evXeMyYMTWGv/T0dN58882GK0yIJk5WnxdC1Co9Pb3y70uWLGHq1KkcPny4cpurqyseHh54eHg4ozwAAgMD/7YFxGAwEBwc3DAF1YO6fo2Dg4ObbNgTwhmkRUgIUavg4ODKPyaTCUVRqm37a7fNmdaKF154gaCgILy9vZkxYwY2m43//ve/+Pr6EhYWxkcffVTlXKmpqdx22234+Pjg5+fHTTfdRFJS0nnVO2bMGDZu3Mjs2bMrW62SkpKqdY0tWLAAb29vVq5cSUxMDG5ubowePZqSkhI+/vhjoqKi8PHx4eGHH8Zut1ce32Kx8MQTT9CiRQvc3d3p1asXGzZsOGc9SUlJKIpCQkJC5bb8/HwURan83JnafvrpJ3r06IGbmxt9+/atEjjP/hpPnz6djz/+mG+//bbyGmurQQhxbhKEhBD1Yt26daSlpbFp0yZef/11pk+fzvDhw/Hx8WHr1q2MGzeOcePGkZKSAkBpaSkDBw7Ew8ODTZs28fPPP+Ph4cHQoUOxWCx1Pu/s2bPp06cPY8eOJT09nfT0dMLDw2vct7S0lDlz5rB48WJ++OEHNmzYwKhRo1i1ahWrVq1i0aJFzJ07l6VLl1Z+5r777mPLli0sXryYPXv2cOuttzJ06FCOHDlycV8wYMqUKbz22mts374dnU7H/fffX+N+jz/+OP/4xz8YOnRo5TX27dv3os8vRHMkXWNCiHrh6+vLnDlz0Gg0xMTE8PLLL1NaWsrTTz8NwFNPPcVLL73Eli1buP3221m8eDEajYYPPvgARVEAmD9/Pt7e3mzYsIEhQ4bU6bwmkwmDwYCbm9vfdoVZrVbeffddWrVqBcDo0aNZtGgRmZmZeHh40KFDBwYOHMj69eu57bbbOHbsGJ9//jknT54kNDQUqAglP/zwA/Pnz+eFF1640C8XALNmzaJ///4ATJ48mRtuuAGz2YyLi0uV/Tw8PHB1daW8vLxJd/cJ0RhIEBJC1IuOHTui0fzZ6BwUFERsbGzla61Wi5+fH1lZWQDs2LGDo0eP4unpWeU4ZrOZY8eO1UuNbm5ulSHoTI1RUVFVxuIEBQVV1rhz505UVaVt27ZVjlNeXo6fn99F19O5c+fKv4eEhACQlZVFRETERR9bCFEzCUJCiHqh1+urvFYUpcZtDocDAIfDQVxcHJ9++mm1YwUEBDSaGrVaLTt27ECr1VbZ71wDmc+EQVVVK7dZrda/redMq9iZcwsh6ocEISFEo9C9e3eWLFlCYGAgXl5eF3Usg8FQZYDzpdKtWzfsdjtZWVn069evTp85E+LS09Pp1q0bQJWB0xeqvq5RiOZGBksLIRqFO++8E39/f2666SY2b95MYmIiGzduZMKECZw8efK8jhUVFcXWrVtJSkoiOzv7krWqtG3bljvvvJN77rmHr7/+msTERLZt28b//vc/Vq1aVeNnXF1d6d27Ny+99BIHDhxg06ZNPPPMMxddS1RUFHv27OHw4cNkZ2efs5VJCFE7CUJCiEbBzc2NTZs2ERERwahRo2jfvj33338/ZWVl591C9Pjjj6PVaunQoQMBAQEkJydfsjrnz5/PPffcw2OPPUZMTAw33ngjW7duPeeTaQAfffQRVquVHj16MGHCBGbOnHnRdYwdO5aYmBh69OhBQEAAW7ZsuehjCtEcKerZHddCCNGEbNiwgYEDB5KXlydLSpxlwYIFTJw4sckuJyJEQ5IxQkKIJi8sLIwRI0bw+eefO7sUp/Pw8MBms1V75F4IUTNpERJCNFllZWWkpqYCFQFA5tSBo0ePAhXTE0RHRzu5GiEaPwlCQgghhGi2ZLC0EEIIIZotCUJCCCGEaLYkCAkhhBCi2ZIgJIQQQohmS4KQEEIIIZotCUJCCCGEaLYkCAkhhBCi2ZIgJIQQQohm6/8BXdVVr1QWp10AAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# We show here structure function for the same 10 lightcurves\n", "plt.figure()\n", @@ -198,7 +166,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -218,7 +186,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -241,30 +209,9 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/astro/users/ncaplar/miniconda3/envs/tiny_lsst/lib/python3.10/site-packages/distributed/node.py:182: UserWarning: Port 8787 is already in use.\n", - "Perhaps you already have a cluster running?\n", - "Hosting the HTTP server on port 36509 instead\n", - " warnings.warn(\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# First, we create all the columns that we will want to fill\n", "# In addition to time, measurement and errors, this includes \n", @@ -304,192 +251,30 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "The `ensemble` has an `object` table, capturing the information about the global properties of each \n", - "lightcurve (such as a number of observations), while the actual observations are stored in the `source` table. \n", - "More information is available in the `Working with the TAPE Ensemble object` tutorial." + "lightcurve, while the actual observations are stored in the `source` table. In this case, our object table\n", + "is empty, as no such information is provided. More information is available in the \n", + "`Working with the TAPE Ensemble object` tutorial." ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
filter_ensnobs_rnobs_total
id_ens
0200200
1200200
2200200
3200200
4200200
\n", - "
" - ], - "text/plain": [ - "filter_ens nobs_r nobs_total\n", - "id_ens \n", - "0 200 200\n", - "1 200 200\n", - "2 200 200\n", - "3 200 200\n", - "4 200 200" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ens.head(\"object\", 5) \n" ] }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
t_ensy_ensyerr_ensfilter_ens
id_ens
091.989199-0.0640030.018550r
092.354235-0.0635810.018565r
098.559856-0.0057130.019927r
0101.115112-0.0758780.018284r
0104.400440-0.1129910.017365r
\n", - "
" - ], - "text/plain": [ - " t_ens y_ens yerr_ens filter_ens\n", - "id_ens \n", - "0 91.989199 -0.064003 0.018550 r\n", - "0 92.354235 -0.063581 0.018565 r\n", - "0 98.559856 -0.005713 0.019927 r\n", - "0 101.115112 -0.075878 0.018284 r\n", - "0 104.400440 -0.112991 0.017365 r" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ens.head(\"source\", 5) " ] @@ -514,7 +299,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -542,7 +327,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -553,212 +338,27 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
lc_idbanddtsf21_sigma
0combinedr32.8314720.0196180.000105
1combinedr102.0113230.0496990.000318
2combinedr210.2199680.0711410.000461
3combinedr302.6871320.0729130.000297
4combinedr385.9676290.0756910.000317
\n", - "
" - ], - "text/plain": [ - " lc_id band dt sf2 1_sigma\n", - "0 combined r 32.831472 0.019618 0.000105\n", - "1 combined r 102.011323 0.049699 0.000318\n", - "2 combined r 210.219968 0.071141 0.000461\n", - "3 combined r 302.687132 0.072913 0.000297\n", - "4 combined r 385.967629 0.075691 0.000317" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "res_sf2.head(5)" ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
lc_idbanddtsf21_sigma
00033.4325370.0177110.000863
100129.3680120.0644810.003192
200283.9965480.0866550.002505
300365.7708200.0832440.003209
400444.5902320.0629190.002613
\n", - "
" - ], - "text/plain": [ - " lc_id band dt sf2 1_sigma\n", - "0 0 0 33.432537 0.017711 0.000863\n", - "1 0 0 129.368012 0.064481 0.003192\n", - "2 0 0 283.996548 0.086655 0.002505\n", - "3 0 0 365.770820 0.083244 0.003209\n", - "4 0 0 444.590232 0.062919 0.002613" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "res_one.head(5)" ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "plt.figure()\n", "plt.errorbar(res_sf2['dt'], res_sf2['sf2'], yerr=res_sf2['1_sigma'],\n", @@ -789,7 +389,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -805,20 +405,9 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAHJCAYAAABpOFaGAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy88F64QAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOydeXxU1d3/3zNZJ3sCCQn7vsku4IIKuCO2iloVd61aq611e9yqBVxq66NW+2irffwJ1se61L1aFUQQF8qmgOxrWEMSsu/LzP398T1nzsxkEhIIJJDzfr3mNcmdO/eee+6dez73ux2X4zgOFovFYrFYLB0Qd1s3wGKxWCwWi6WtsELIYrFYLBZLh8UKIYvFYrFYLB0WK4QsFovFYrF0WKwQslgsFovF0mGxQshisVgsFkuHxQohi8VisVgsHRYrhCwWi8VisXRYrBCyWCwWi8XSYbFCyNImzJkzB5fL1ehr4cKFbd3EQ2LhwoW4XC7eeeedA647c+ZMXC7XEWhV63PdddfRu3fvVtmWy+Vi5syZrbKtxtDn5UheX+vWrWPmzJlkZ2cfsX2GsnfvXmbOnMnKlSvbrA1NkZ2dzdSpU0lLS8PlcnHHHXe0WVsqKyuZOXNm2GtE37fa8lxaWp/Itm6ApWMze/ZsBg8e3GD50KFD26A1lrZk8eLFdO/eva2b0eqsW7eOWbNmMWnSpFYTjS1l7969zJo1i969ezNq1Kg2aUNT3HnnnSxZsoRXXnmFzMxMsrKy2qwtlZWVzJo1C4BJkyYFfTZ16lQWL17cpu2ztD5WCFnalGHDhjF27Ni2boalHXDiiSe2dRPaBZWVlcTFxbV1M5qF1+ulvr6emJiYQ9rOmjVrGD9+PBdeeGHrNOwwkZ6eTnp6els3w9LKWNeYpd3jcrn41a9+xWuvvcaQIUOIi4tj5MiRfPzxx0Hr5efnc/PNN9OjRw9iYmJIT09nwoQJfPHFF/515s2bxwUXXED37t2JjY2lf//+/OIXv2D//v1B29LuqtWrV/Ozn/2M5ORk0tLSuOuuu6ivr2fjxo2ce+65JCYm0rt3b5588smwba+uruauu+4iMzMTj8fDxIkT+eGHH5p13G+99RYnnXQS8fHxJCQkcM455zTru/n5+dx6660MHTqUhIQEMjIyOP300/n666+D1svOzsblcvHUU0/xzDPP0KdPHxISEjjppJP4z3/+02C7c+bMYdCgQcTExDBkyBD+/ve/N+s4AL788ksmTZpEp06d8Hg89OzZk4svvpjKykr/OqGuMe2GWLBgAb/85S/p3LkznTp14qKLLmLv3r1B26+pqeHuu+8mMzOTuLg4TjvtNFasWEHv3r257rrrDti+5cuX89Of/pS0tDRiY2MZPXo0b7/9drOO7a9//SsjR44kISGBxMREBg8ezIMPPug/hp/97GcATJ482e/6nTNnDiAWh2HDhrFo0SJOPvlk4uLiuOGGG8L2hybcMe3Zs8d/7UdHR9O1a1cuueQScnNzWbhwIePGjQPg+uuv97dBb3vSpEkNLB/Q0O2pr5cnn3ySxx57jD59+hATE8OCBQsOug+1q3LLli18+umn/rZlZ2c36oYK597U/bhs2TJOPfVU4uLi6Nu3L3/4wx/w+XxB3y8uLubuu++mb9++xMTEkJGRwXnnnceGDRvIzs72C51Zs2b526P7u7E2vfLKK4wcOZLY2FjS0tKYNm0a69evb9CfCQkJbNmyhfPOO4+EhAR69OjB3XffTU1NTZP9ZDm8WCFkaVP0E2Xgy+v1Nljvk08+4fnnn+eRRx7h3Xff9d9stm3b5l/n6quv5oMPPuB3v/sdc+fO5eWXX+bMM8+koKDAv87WrVs56aST+Otf/8rcuXP53e9+x5IlSzjllFOoq6trsN9LL72UkSNH8u6773LTTTfxpz/9iTvvvJMLL7yQqVOn8v7773P66adz33338d577zX4/oMPPsi2bdt4+eWXefnll9m7dy+TJk0Kanc4fv/73zN9+nSGDh3K22+/zWuvvUZZWRmnnnoq69ata/K7hYWFAMyYMYNPPvmE2bNn07dvXyZNmhQ27uGFF15g3rx5PPvss7z++utUVFRw3nnnUVJS4l9nzpw5XH/99QwZMoR3332Xhx56iEcffZQvv/yyybaAif+Ijo7mlVde4bPPPuMPf/gD8fHx1NbWHvD7N954I1FRUfzjH//gySefZOHChVx11VVB61x//fU8++yzXH/99Xz44YdcfPHFTJs2jeLi4gNuf8GCBUyYMIHi4mJefPFFPvzwQ0aNGsVll13mFyyN8eabb3LrrbcyceJE3n//fT744APuvPNOKioqAHGl/P73vweknxcvXszixYuZOnWqfxs5OTlcddVVXHHFFfz73//m1ltvPWCbA9mzZw/jxo3j/fff56677uLTTz/l2WefJTk5maKiIsaMGcPs2bMBeOihh/xtuPHGG1u0H82f//xnvvzyS5566ik+/fRTBg8efNB9OGbMGBYvXkxmZiYTJkzwt+1gXE/79u3jyiuv5KqrruKjjz5iypQpPPDAA/zf//2ff52ysjJOOeUUXnrpJa6//nr+9a9/8eKLLzJw4EBycnLIysris88+A+DnP/+5vz0PP/xwo/t94okn+PnPf85xxx3He++9x3PPPcfq1as56aST2Lx5c9C6dXV1/PSnP+WMM87gww8/5IYbbuBPf/oTf/zjH1t8vJZWxLFY2oDZs2c7QNhXRERE0LqA06VLF6e0tNS/bN++fY7b7XaeeOIJ/7KEhATnjjvuaHYbfD6fU1dX5+zYscMBnA8//ND/2YwZMxzAefrpp4O+M2rUKAdw3nvvPf+yuro6Jz093bnooov8yxYsWOAAzpgxYxyfz+dfnp2d7URFRTk33nhjg31pdu7c6URGRjq//vWvg/ZdVlbmZGZmOpdeemmzj9FxHKe+vt6pq6tzzjjjDGfatGn+5du3b3cAZ/jw4U59fb1/+dKlSx3AeeONNxzHcRyv1+t07dq10WPp1atXk/t/5513HMBZuXJlk+sBzowZM/z/62vk1ltvDVrvySefdAAnJyfHcRzHWbt2rQM49913X9B6b7zxhgM41157rX+ZPi8LFizwLxs8eLAzevRop66uLuj7559/vpOVleV4vd5G2/yrX/3KSUlJafK4/vnPfzbYp2bixIkO4MyfP7/BZ6H9oenVq1fQMd1www1OVFSUs27dukbbsGzZMgdwZs+eHbYNEydObLD82muvDTq3+nrp16+fU1tbG7TuofShPqapU6cGLdPnf/v27UHLw51D3Y9LliwJWnfo0KHOOeec4///kUcecQBn3rx5jbYlPz+/0b4PbVNRUZHj8Xic8847L2i9nTt3OjExMc4VV1zhX3bttdc6gPP2228HrXveeec5gwYNarQ9lsOPtQhZ2pS///3vLFu2LOi1ZMmSButNnjyZxMRE//9dunQhIyODHTt2+JeNHz+eOXPm8Nhjj/Gf//wnrIUnLy+PW265hR49ehAZGUlUVBS9evUCaGDKBjj//POD/h8yZAgul4spU6b4l0VGRtK/f/+gtmiuuOKKoIywXr16cfLJJ/vd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yBLRmVz5+flB+7z22mud+Pj4Bm2ZOHGic9xxx/n/15ktr732mnP77bc76enpTkxMjHPqqac6y5cvD/puaNaY5oMPPnAmT57sJCUlOTExMU6vXr2cSy65xPniiy+a7Nuamhrnnnvucbp16+bExsY6Y8aMcT744INGs4D++7//u8E2QvvDcRzn5ZdfdgYMGOBER0c7AwcOdF555ZUG2wzH4sWLnWnTpjm9evVyYmJinE6dOjkTJ050Pvrooyb3qa+RZcuWBa0XLmuourraueuuu5yMjAwnNjbWOfHEE53Fixc7ycnJzp133tnkdx3HcVatWuVceumlTkZGhhMVFeVkZmY6p59+uvPiiy82eWyvvvqqM3nyZKdLly5OdHS007VrV+fSSy91Vq9eHbTes88+6/Tp08eJiIgIyt4KvW4Cqampce69916nR48ejsfjcSZOnOisXLmyQdaY4zjOrl27nBtuuMHJzMx0oqKi/O3Izc31r/PGG284gwcPdqKiohr09auvvuoMGTLEiY2NdYYOHeq89dZbLbpeDqUPHSd81pjjOM6mTZucs88+20lKSnLS09OdX//6184nn3wSNmssXD+Guz6Lioqc3/zmN07Pnj2dqKgoJyMjw5k6daqzYcMG/zpffPGFM3r0aCcmJiYo87CxTLaXX37ZGTFihBMdHe0kJyc7F1xwgbN27doGbQl372js9285crgcJ8DWbbFYLMcI3333HRMmTOD111/niiuuaOvmWCyWdooVQhaL5ahn3rx5LF68mOOPPx6Px8OqVav4wx/+QHJyMqtXr26QqWOxWCwaGyNksViOepKSkpg7dy7PPvssZWVldO7cmSlTpvDEE09YEWSxWJrEWoQsFovFYrF0WGz6vMVisVgslg6LFUIWi8VisVg6LFYIWSwWi8Vi6bDYYOkm8Pl87N27l8TExEbL4FssFovFYmlfOI5DWVkZXbt2PWBRVSuEmmDv3r306NGjrZthsVgsFovlINi1a9cBJ9q1QqgJ9Pw4u3btalG5eYvFYrFYLG1HaWkpPXr0aNY8d1YINYF2hyUlJVkhZLFYLBbLUUZzwlpssLTFYrFYLJYOixVCFovFYrFYOizWNWaxWCztAK/XS11dXVs3w2I5aoiKiiIiIuKQt2OFkMVisbQhjuOwb98+iouL27opFstRR0pKCpmZmYdU4sYKIYvF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vjigZWXdunWcd955JCQk0KVLF66++mr279/f7GMItTjU19dz++23k5KSQqdOnbjvvvu49tprG7j5fD5fo8fSu3dvAKZNm4bL5fL/D/DRRx8xduxYYmNj6dy5MxdddJH/s3DHmpKS4resadfg22+/zaRJk4iNjeUvf/kLHo+Hzz77LOh77733HvHx8ZSXlwOwZ88eLrvsMlJTU+nUqRMXXHAB2dnZjfbLwoULcblcfP7554wdO5aYmBi+/vprHMfhySefpG/fvng8HkaOHMk777zj/15RURFXXnkl6enpeDweBgwYwOzZs4O2WVxc7F9/5cqVuFyusG2ZM2cOs2bNYtWqVbhcLlwu12G1MtpgaYvFYmkveCvh7YRD3IgPlt8mr5ZwaTlExh9wtZycHKZPn86TTz7JtGnTKCsr8w+UjfHqq69y1113sWTJEhYvXsx1113HhAkTOOuss/D5fFx44YX07NmTJUuWUFZWxt13333ANkycOJGbbrqJZ555hqqqKu677z4uvfRSvvzyy5Ydt+KPf/wjr7/+OrNnz2bIkCE899xzfPDBB0yePLnZx7Js2TIyMjKYPXs25557LhEREQB88sknXHTRRfz2t7/ltddeo7a2lk8++aTFbbzvvvt4+umnmT17tl+gvP7665x77rn+df7xj39wwQUXkJCQQGVlJZMnT+bUU09l0aJFREZG8thjj3HuueeyevVqoqOjG93Xvffey1NPPUXfvn1JSUnhoYce4r333uOvf/0rAwYMYNGiRVx11VWkp6czceJEHn74YdatW8enn35K586d2bJlC1VVVS0+RoDLLruMNWvW8Nlnn/HFF18AYmk8XFghZLFYLJZmk5OTQ319PRdddBG9evUCYPjw4U1+Z8SIEcyYMQOAAQMG8PzzzzN//nzOOuss5s6dy9atW1m4cCGZmZkAPP7445x11lmNbu+vf/0rY8aM4fe//71/2SuvvEKPHj3YtGkTAwcObPFx/c///A8PPPAA06ZNA+D555/n3//+d4uOJT09HRBrjj4WfTyXX345s2aZMgcjR45scRvvuOOOIEvSlVdeyTXXXENlZSVxcXGUlpbyySef8O677wLw5ptv4na7efnll3G5XADMnj2blJQUFi5cyNlnn93ovh555BH/OaioqOCZZ57hyy+/5KSTTgKgb9++fPPNN7z00ktMnDiRnTt3Mnr0aMaOHQsQZA1rKR6Ph4SEBCIjI4P68XBhhZDFYrG0FyLixDLTEir3iDsMn1nmioCp6yCuW8v23QxGjhzJGWecwfDhwznnnHM4++yzueSSS0hNTW30OyNGjAj6Pysri7y8PAA2btxIjx49gga88ePHN9mGFStWsGDBAhISGlrPtm7d2mIhVFJSQm5ubtB+IyIiOP744/H5fEHrNnUsjbFy5UpuuummFrUpHFpkaKZOnUpkZCQfffQRl19+Oe+++y6JiYl+gbNixQq2bNlCYmJi0Peqq6vZunVrs/e1bt06qqurG4jT2tpaRo8eDcAvf/lLLr74Yr7//nvOPvtsLrzwQk4++eSDPtYjiRVCFovF0l5wuZrlngoiaSCM/R/jCnNFwPiXZPlhICIignnz5vHdd98xd+5c/ud//off/va3LFmyhD59+oT9TlRUVND/LpfLLzAcx/FbK5qLz+fjJz/5CX/84x8bfJaVdfBlA0LbEc7d19SxNIbH4zngfkP3FS4YOj4++NqIjo7mkksu4R//+AeXX345//jHP7jsssuIjJSh3efzcfzxx/P666832Ja2XjVG4L708X3yySd06xYsrmNiYgCYMmUKO3bs4JNPPuGLL77gjDPO4LbbbuOpp57C7ZZw5MBjPFLB3s3BBktbLBbL0U7fa83fU9dBv58f1t25XC4mTJjArFmz+OGHH4iOjub9998/qG0NHjyYnTt3kpub6192oAmvx4wZw9q1a+nduzf9+/cPeoWKheaQnJxMly5dWLp0qX+Z1+vlhx9+aPG2oqKi8Hq9QctGjBjRZKp+eno6OTlmmpTNmzdTWVnZrP1deeWVfPbZZ6xdu5YFCxZw5ZVX+j8bM2YMmzdvJiMjo0E/tSTmZujQocTExLBz584G2+nRo0fQcVx33XX83//9H88++yx/+9vf/MuBoGM8UE2o6OjoBv14uLBCyGKxWI4lWuIOOwiWLFnC73//e5YvX87OnTt57733yM/PZ8iQIQe1vbPOOot+/fpx7bXXsnr1ar799lt++9vfAg0tNJrbbruNwsJCpk+fztKlS9m2bRtz587lhhtuOOjB89e//jVPPPEEH374IRs3buQ3v/kNRUVFLbZW9e7dm/nz57Nv3z6KiooAmDFjBm+88QYzZsxg/fr1/Pjjjzz55JP+75x++uk8//zzfP/99yxfvpxbbrmlgeWpMSZOnEiXLl248sor6d27NyeeeKL/syuvvJLOnTtzwQUX8PXXX7N9+3a++uorfvOb37B79+5mH1NiYiL33HMPd955J6+++ipbt27lhx9+4IUXXuDVV18F4He/+x0ffvghW7ZsYe3atXz88cf+a0ILppkzZ7Jp0yY++eQTnn766QP24/bt21m5ciX79++npqam2e1tKVYIWSwWi6XZJCUlsWjRIs477zwGDhzIQw89xNNPP82UKVMOansRERF88MEHlJeXM27cOG688UYeeughAGJjY8N+p2vXrnz77bd4vV7OOecchg0bxm9+8xuSk5P9bpiWct999zF9+nSuueYaTjrpJBISEjjnnHMabUNjPP3008ybN48ePXr442cmTZrEP//5Tz766CNGjRrF6aefzpIlS4K+06NHD0477TSuuOIK7rnnHuLimhez5XK5mD59OqtWrQqyBgHExcWxaNEievbsyUUXXcSQIUO44YYbqKqqIikpqUXH9eijj/K73/2OJ554giFDhnDOOefwr3/9y+8OjY6O5oEHHmDEiBGcdtppRERE8OabbwJiJXvjjTfYsGEDI0eO5I9//COPPfZYk/u7+OKLOffcc5k8eTLp6em88cYbLWpvS3A5TeU8dnBKS0tJTk6mpKSkxReNxWKxHIjq6mq2b99Onz59Wjzg+qnKgfJt4ecaO0rnG/v222855ZRT2LJlC/369WuTNvh8PoYMGcKll17Ko48+2iZtsByYxn5DLRm/bbC0xWKxHM2Ezj6vBdFRNPv8+++/T0JCAgMGDGDLli385je/YcKECUdUBO3YsYO5c+cyceJEampqeP7559m+fTtXXHHFEWuDpW2wQshisViOZgb8Arr/tOHyo8gaVFZWxr333suuXbvo3LkzZ5555gFjSFobt9vNnDlzuOeee3Ach2HDhvHFF18cdOyT5ejBusaawLrGLBbL4aRVXGMWSwemNVxjNljaYrFYLBZLh8UKIYvFYmljrGHeYjk4WuO3Y4WQxWKxtBG6Vkxzi+dZLJZg9G+nuXWXwmGDpS0Wi6WNiIiIICUlxT9XVVxcXIsL+FksHRHHcaisrCQvL4+UlBQiIiIOeltWCFksFksboicbPdDEnRaLpSEpKSmHPEO9FUIWi8XShrhcLrKyssjIyGhXE1FaLO2dqKioQ7IEaawQslgs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c9hoTdrdIrRnzx5WrFgBwNixY7n99tuZMWMGubm5FBYWMm/ePC688EL++te/MnnyZO6++27uuecevvjiCwYMGNCeQzcMwzAMo5PT7kJo4cKFzJgxo972iy66iAceeACQgoq//e1v2bRpE4cddhj/8z//w1FHHdXGIzUMwzAMo6vR7kLIMAzDMAyjvejQMUKGYRiGYRitiQkhwzAMwzC6LSaEDMMwDMPotpgQOgDOOOMMcnJymD17dnsPxTCazbp165g+fTrDhw9n1KhRPPbYY+09JMNoFiUlJUyYMIExY8YwcuRI7rnnnvYektEJsWDpA+D1119nz549PPjggzz++OPtPRzDaBabNm1iy5YtjBkzhq1btzJu3DiWLVtGWlpaew/NMJpEbW0tlZWVpKamUlZWxmGHHcaHH35IXl5eew/N6ESYRegAmDFjBhkZGe09DMPYL/r06cOYMWMA6NmzJ7m5uezcubN9B2UYzSAYDJKamgpARUUFtbW12LW90Vy6rRBatGgRp556Kn379iUQCEQaskbz5z//mUGDBpGcnMz48eN58803236ghtEILXkOf/TRR4RCIfr379/KozYMT0ucw0VFRYwePZqCggKuvfZa8vPz22j0Rleh2wqh0tJSRo8ezZ133tng4/PmzePqq6/mZz/7Gf/617+YNm0aM2fOZO3atW08UsNomJY6h3fs2MGcOXO4++6722LYhhGhJc7h7OxsPv30U1atWsXDDz/Mli1b2mr4RlchbISB8FNPPRWzbeLEieErrrgiZtshhxwSvu6662K2vf766+GzzjqrtYdoGHtlf8/hioqK8LRp08J/+9vf2mKYhtEoBzIPK1dccUX4H//4R2sN0eiidFuL0N6oqqri448/5vjjj4/Zfvzxx/POO++006gMo+k05RwOh8NcfPHFHHPMMVx44YXtMUzDaJSmnMNbtmxh9+7dgHSoX7RoEcOGDWvzsRqdm/j2HkBHZPv27dTW1tKrV6+Y7b169WLz5s2R+yeccAKffPIJpaWlFBQU8NRTTzFhwoS2Hq5h1KMp5/Dbb7/NvHnzGDVqVCQ2Y+7cuYwcObKth2sY9WjKObx+/Xouu+wywuEw4XCY//iP/2DUqFHtMVyjE2NCaC8EAoGY++FwOGbbSy+91NZDMoxmsbdz+MgjjyQUCrXHsAyjyeztHB4/fjyLFy9uh1EZXQlzjTVAfn4+wWAwxvoDsHXr1npXJ4bREbFz2Ojs2DlstBUmhBogMTGR8ePHs2DBgpjtCxYsYMqUKe00KsNoOnYOG50dO4eNtqLbusb27NnDihUrIvdXrVrF4sWLyc3NpbCwkB/96EdceOGFHH744UyePJm7776btWvXcsUVV7TjqA3DY+ew0dmxc9joELRnylp78vrrr4eBereLLrooss+f/vSn8IABA8KJiYnhcePGhd944432G7Bh1MHOYaOzY+ew0RGwXmOGYRiGYXRbLEbIMAzDMIxuiwkhwzAMwzC6LSaEDMMwDMPotpgQMgzDMAyj22JCyDAMwzCMbosJIcMwDMMwui0mhAzDMAzD6LaYEDIMwzAMo9tiQsgwDMMwjG6LCSHDMA6Im2++mTFjxrT56y5cuJBAIEAgEOD0009v0r5FRUVtMraWpqmfsX4e2dnZrT4mw+gqmBAyDKNRdGFt7HbxxRdzzTXX8Oqrr7bbGJctW8YDDzwQuT99+nSuvvrqmH2mTJnCpk2byMrKatvBtRB1P+OLL764QfG3adMm/vCHP7TdwAyjC9Btu88bhrFvNm3aFPl73rx53HTTTSxbtiyyLSUlhfT0dNLT09tjeAD07NlznxaQxMREevfu3TYDagWa+hn37t2704o9w2gvzCJkGEaj9O7dO3LLysoiEAjU21bXbaPWil//+tf06tWL7OxsfvnLX1JTU8NPfvITcnNzKSgo4L777ot5rQ0bNnDuueeSk5NDXl4ep512GqtXr27WeC+++GLeeOMN/vjHP0asVqtXr67nGnvggQfIzs7m2WefZdiwYaSmpjJ79mxKS0t58MEHGThwIDk5Ofznf/4ntbW1keNXVVVx7bXX0q9fP9LS0jjiiCNYuHBho+NZvXo1gUCAxYsXR7YVFRURCAQiz9Oxvfrqqxx++OGkpqYyZcqUGMEZ/RnffPPNPPjgg/zzn/+MvMe9jcEwjL1jQsgwjBbntddeY+PGjSxatIjbb7+dm2++mVNOOYWcnBzef/99rrjiCq644grWrVsHQFlZGTNmzCA9PZ1Fixbx1ltvkZ6ezoknnkhVVVWTX/ePf/wjkydP5nvf+x6bNm1i06ZN9O/fv8F9y8rKuOOOO3j00Ud58cUXWbhwIWeeeSbPP/88zz//PHPnzuXuu+/m8ccfjzznkksu4e233+bRRx9lyZIlnH322Zx44oksX778wD4w4Gc/+xm///3v+eijj4iPj+fSSy9tcL9rrrmGc845hxNPPDHyHqdMmXLAr28Y3RVzjRmG0eLk5uZyxx13EBcXx7Bhw/jtb39LWVkZN9xwAwDXX389t912G2+//TbnnXcejz76KHFxcdx7770EAgEA7r//frKzs1m4cCHHH398k143KyuLxMREUlNT9+kKq66u5i9/+QuDBw8GYPbs2cydO5ctW7aQnp7O8OHDmTFjBq+//jrnnnsuK1eu5JFHHmH9+vX07dsXEFHy4osvcv/99/PrX/96fz8uAH71q19x9NFHA3Dddddx8sknU1FRQXJycsx+6enppKSkUFlZ2andfYbRUTAhZBhGizNixAji4rzBuVevXhx22GGR+8FgkLy8PLZu3QrAxx9/zIoVK8jIyIg5TkVFBStXrmyVMaampkZEkI5x4MCBMbE4vXr1iozxk08+IRwOc/DBB8ccp7Kykry8vAMez6hRoyJ/9+nTB4CtW7dSWFh4wMc2DKNxTAgZhtHiJCQkxNwPBAINbguFQgCEQiHGjx/P3//+93rH6tGjR4cZYzAY5OOPPyYYDMbs11ggs4rBcDgc2VZdXb3P8ahVTF/bMIzWw4SQYRjtzrhx45g3bx49e/YkMzPzgI6VmJgYE+DcUowdO5ba2lq2bt3KtGnTmvQcFXGbNm1i7NixADGB0/tLa71Hw+iOWLC0YRjtzgUXXEB+fj6nnXYab775JqtWreKNN97gqquuYv369c061sCBA3n//fdZvXo127dvbzGrysEHH8wFF1zAnDlzePLJJ1m1ahUffvghv/nNb3j++ecbfE5KSgqTJk3itttu48svv2TRokX8/Oc/P+CxDBw4kCVLlrBs2TK2b9/eqJXJMIx9Y0LIMIx2JzU1lUWLFlFYWMiZZ57JoYceyqWXXkp5eXmzLUTXXHMNwWCQ4cOH06NHD9auXdti47z//vuZM2cOP/7xjxk2bBizZs3i/fffbzQzDeC+++6jurqaww8/nKuuuopbbrnlgMfxve99j2HDhnH44YfTo0cP3n777QM+pmF0VwLhaOe1YRhGJ2HhwoXMmDGDXbt2WUuJKB544AGuvvrqTttOxDDaGosRMgyjU1NQUMCpp57KI4880t5DaXfS09Opqampl3JvGEbjmEXIMIxOSXl5ORs2bABEAFhNHVixYgUg5QkGDRrUzqMxjM6BCSHDMAzDMLotFixtGIZhGEa3xYSQYRiGYRjdFhNChmEYhmF0W0wIGYZhGIbRbTEhZBiGYRhGt8WEkGEYhmEY3RYTQoZhGIZhdFtMCBmGYRiG0W35//hHX54WtfUUAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Show all of the 100 results in faint yellow\n", "plt.plot(res_one['dt'], res_resample_arr.transpose(), alpha=0.3, color='yellow')\n", @@ -851,30 +440,9 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "plt.hist(res_resample_arr.transpose()[0])\n", "err_manual = (np.quantile(res_resample_arr.transpose()[0], 0.84) -\n", @@ -911,7 +479,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -938,30 +506,9 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# plt.plot(res_basic['dt'], res_basic['sf2'], 'b', label='Basic', lw = 3, marker = 'o')\n", "plt.plot(res_macleod['dt'], res_macleod['sf2'], 'g',marker='.', label='MacLeod 2012')\n", @@ -1004,7 +551,7 @@ ], "metadata": { "kernelspec": { - "display_name": "tape", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -1018,7 +565,12 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.10" + "version": "3.8.9" + }, + "vscode": { + "interpreter": { + "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" + } } }, "nbformat": 4, diff --git a/docs/tutorials/tape_datasets.ipynb b/docs/tutorials/tape_datasets.ipynb index 788b8883..1cd3670f 100644 --- a/docs/tutorials/tape_datasets.ipynb +++ b/docs/tutorials/tape_datasets.ipynb @@ -85,8 +85,7 @@ " flux_col=\"psFlux\",\n", " err_col=\"psFluxErr\",\n", " band_col=\"filterName\",\n", - " nobs_total_col=\"nobs_total\",\n", - " nobs_band_cols=[\"nobs_g\", \"nobs_r\"])\n", + ")\n", "\n", "# Read in data from a parquet file that contains source (timeseries) data\n", "ens.from_parquet(source_file=f\"{rel_path}/source/test_source.parquet\",\n", diff --git a/docs/tutorials/working_with_the_ensemble.ipynb b/docs/tutorials/working_with_the_ensemble.ipynb index c5098095..10110329 100644 --- a/docs/tutorials/working_with_the_ensemble.ipynb +++ b/docs/tutorials/working_with_the_ensemble.ipynb @@ -19,7 +19,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:34.203827Z", @@ -58,23 +58,14 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:36.125402Z", "start_time": "2023-08-30T14:58:34.190790Z" } }, - "outputs": [ - { - "data": { - "text/plain": "" - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from tape.ensemble import Ensemble\n", "\n", @@ -109,23 +100,14 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:36.209050Z", "start_time": "2023-08-30T14:58:36.115521Z" } }, - "outputs": [ - { - "data": { - "text/plain": "" - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from tape.utils import ColumnMapper\n", "\n", @@ -160,24 +142,14 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:36.219081Z", "start_time": "2023-08-30T14:58:36.205629Z" } }, - "outputs": [ - { - "data": { - "text/plain": "Dask DataFrame Structure:\n time flux error band\nnpartitions=1 \n0 float64 float64 float64 string\n9 ... ... ... ...\nDask Name: sort_index, 4 graph layers", - "text/html": "
Dask DataFrame Structure:
\n
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
timefluxerrorband
npartitions=1
0float64float64float64string
9............
\n
\n
Dask Name: sort_index, 4 graph layers
" - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ens._source # We have not actually loaded any data into memory" ] @@ -191,24 +163,14 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:36.484627Z", "start_time": "2023-08-30T14:58:36.213215Z" } }, - "outputs": [ - { - "data": { - "text/plain": " time flux error band\nid \n0 1.0 120.851100 11.633225 g\n0 2.0 136.016225 12.635291 g\n0 3.0 100.005719 14.429710 g\n0 4.0 115.116629 11.786349 g\n0 5.0 107.337795 14.542676 g\n.. ... ... ... ...\n9 96.0 138.371176 12.237541 r\n9 97.0 104.060829 10.920638 r\n9 98.0 149.920678 14.143664 r\n9 99.0 119.480601 10.154990 r\n9 100.0 145.260138 14.733641 r\n\n[1000 rows x 4 columns]", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
timefluxerrorband
id
01.0120.85110011.633225g
02.0136.01622512.635291g
03.0100.00571914.429710g
04.0115.11662911.786349g
05.0107.33779514.542676g
...............
996.0138.37117612.237541r
997.0104.06082910.920638r
998.0149.92067814.143664r
999.0119.48060110.154990r
9100.0145.26013814.733641r
\n

1000 rows Ă— 4 columns

\n
" - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ens.compute(\"source\") # Compute lets dask know we're ready to bring the data into memory" ] @@ -243,44 +205,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:36.696142Z", "start_time": "2023-08-30T14:58:36.361967Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Object Table\n", - "\n", - "Index: 10 entries, 0 to 9\n", - "Data columns (total 3 columns):\n", - " # Column Non-Null Count Dtype\n", - "--- ------ -------------- -----\n", - " 0 nobs_g 10 non-null float64\n", - " 1 nobs_r 10 non-null float64\n", - " 2 nobs_total 10 non-null float64\n", - "dtypes: float64(3)\n", - "memory usage: 320.0 bytes\n", - "Source Table\n", - "\n", - "Index: 1000 entries, 0 to 9\n", - "Data columns (total 4 columns):\n", - " # Column Non-Null Count Dtype\n", - "--- ------ -------------- -----\n", - " 0 time 1000 non-null float64\n", - " 1 flux 1000 non-null float64\n", - " 2 error 1000 non-null float64\n", - " 3 band 1000 non-null string\n", - "dtypes: float64(3), string(1)\n", - "memory usage: 36.1 KB\n" - ] - } - ], + "outputs": [], "source": [ "# Inspection\n", "\n", @@ -296,48 +228,28 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:36.696879Z", "start_time": "2023-08-30T14:58:36.510953Z" } }, - "outputs": [ - { - "data": { - "text/plain": "band nobs_g nobs_r nobs_total\nid \n0 50 50 100\n1 50 50 100\n2 50 50 100\n3 50 50 100\n4 50 50 100", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
bandnobs_gnobs_rnobs_total
id
05050100
15050100
25050100
35050100
45050100
\n
" - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ens.head(\"object\", 5) # Grabs the first 5 rows of the object table" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:36.697259Z", "start_time": "2023-08-30T14:58:36.561399Z" } }, - "outputs": [ - { - "data": { - "text/plain": " time flux error band\nid \n9 96.0 138.371176 12.237541 r\n9 97.0 104.060829 10.920638 r\n9 98.0 149.920678 14.143664 r\n9 99.0 119.480601 10.154990 r\n9 100.0 145.260138 14.733641 r", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
timefluxerrorband
id
996.0138.37117612.237541r
997.0104.06082910.920638r
998.0149.92067814.143664r
999.0119.48060110.154990r
9100.0145.26013814.733641r
\n
" - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ens.tail(\"source\", 5) # Grabs the last 5 rows of the source table" ] @@ -351,24 +263,14 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:36.697769Z", "start_time": "2023-08-30T14:58:36.592238Z" } }, - "outputs": [ - { - "data": { - "text/plain": " time flux error band\nid \n0 1.0 120.851100 11.633225 g\n0 2.0 136.016225 12.635291 g\n0 3.0 100.005719 14.429710 g\n0 4.0 115.116629 11.786349 g\n0 5.0 107.337795 14.542676 g\n.. ... ... ... ...\n9 96.0 138.371176 12.237541 r\n9 97.0 104.060829 10.920638 r\n9 98.0 149.920678 14.143664 r\n9 99.0 119.480601 10.154990 r\n9 100.0 145.260138 14.733641 r\n\n[1000 rows x 4 columns]", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
timefluxerrorband
id
01.0120.85110011.633225g
02.0136.01622512.635291g
03.0100.00571914.429710g
04.0115.11662911.786349g
05.0107.33779514.542676g
...............
996.0138.37117612.237541r
997.0104.06082910.920638r
998.0149.92067814.143664r
999.0119.48060110.154990r
9100.0145.26013814.733641r
\n

1000 rows Ă— 4 columns

\n
" - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ens.compute(\"source\")" ] @@ -386,24 +288,14 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:36.698305Z", "start_time": "2023-08-30T14:58:36.615492Z" } }, - "outputs": [ - { - "data": { - "text/plain": " time flux error band\nid \n0 2.0 136.016225 12.635291 g\n0 12.0 134.260975 10.685679 g\n0 14.0 143.905872 13.484091 g\n0 16.0 133.523376 13.777315 g\n0 21.0 140.037228 10.099401 g\n.. ... ... ... ...\n9 91.0 140.368263 14.320720 r\n9 92.0 148.476901 12.239495 r\n9 96.0 138.371176 12.237541 r\n9 98.0 149.920678 14.143664 r\n9 100.0 145.260138 14.733641 r\n\n[422 rows x 4 columns]", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
timefluxerrorband
id
02.0136.01622512.635291g
012.0134.26097510.685679g
014.0143.90587213.484091g
016.0133.52337613.777315g
021.0140.03722810.099401g
...............
991.0140.36826314.320720r
992.0148.47690112.239495r
996.0138.37117612.237541r
998.0149.92067814.143664r
9100.0145.26013814.733641r
\n

422 rows Ă— 4 columns

\n
" - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ens.query(f\"{ens._flux_col} > 130.0\", table=\"source\")\n", "ens.compute(\"source\")" @@ -418,23 +310,14 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:36.754980Z", "start_time": "2023-08-30T14:58:36.669055Z" } }, - "outputs": [ - { - "data": { - "text/plain": "id\n0 False\n0 True\n0 False\n0 False\n0 True\n ... \n9 False\n9 False\n9 False\n9 False\n9 False\nName: error, Length: 422, dtype: bool" - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "keep_rows = ens._source[\"error\"] < 12.0\n", "keep_rows.compute()" @@ -449,24 +332,14 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:36.792088Z", "start_time": "2023-08-30T14:58:36.690772Z" } }, - "outputs": [ - { - "data": { - "text/plain": " time flux error band\nid \n0 12.0 134.260975 10.685679 g\n0 21.0 140.037228 10.099401 g\n0 22.0 148.413079 10.131055 g\n0 24.0 134.616131 11.231055 g\n0 30.0 143.907125 11.395918 g\n.. ... ... ... ...\n9 81.0 149.016644 10.755373 r\n9 85.0 130.071670 11.960329 r\n9 86.0 136.297942 11.419338 r\n9 88.0 134.215481 11.202422 r\n9 89.0 147.302751 11.271162 r\n\n[169 rows x 4 columns]", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
timefluxerrorband
id
012.0134.26097510.685679g
021.0140.03722810.099401g
022.0148.41307910.131055g
024.0134.61613111.231055g
030.0143.90712511.395918g
...............
981.0149.01664410.755373r
985.0130.07167011.960329r
986.0136.29794211.419338r
988.0134.21548111.202422r
989.0147.30275111.271162r
\n

169 rows Ă— 4 columns

\n
" - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ens.filter_from_series(keep_rows, table=\"source\")\n", "ens.compute(\"source\")" @@ -481,44 +354,14 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:37.026887Z", "start_time": "2023-08-30T14:58:36.715537Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Object Table\n", - "\n", - "Index: 10 entries, 0 to 9\n", - "Data columns (total 3 columns):\n", - " # Column Non-Null Count Dtype\n", - "--- ------ -------------- -----\n", - " 0 nobs_g 10 non-null float64\n", - " 1 nobs_r 10 non-null float64\n", - " 2 nobs_total 10 non-null float64\n", - "dtypes: float64(3)\n", - "memory usage: 320.0 bytes\n", - "Source Table\n", - "\n", - "Index: 169 entries, 0 to 9\n", - "Data columns (total 4 columns):\n", - " # Column Non-Null Count Dtype\n", - "--- ------ -------------- -----\n", - " 0 time 169 non-null float64\n", - " 1 flux 169 non-null float64\n", - " 2 error 169 non-null float64\n", - " 3 band 169 non-null string\n", - "dtypes: float64(3), string(1)\n", - "memory usage: 6.1 KB\n" - ] - } - ], + "outputs": [], "source": [ "# Cleaning nans\n", "ens.dropna(table=\"source\") # clean nans from source table\n", @@ -549,24 +392,14 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:37.095991Z", "start_time": "2023-08-30T14:58:36.917820Z" } }, - "outputs": [ - { - "data": { - "text/plain": " time flux error band band2\nid \n0 12.0 134.260975 10.685679 g g2\n0 21.0 140.037228 10.099401 g g2\n0 22.0 148.413079 10.131055 g g2\n0 24.0 134.616131 11.231055 g g2\n0 30.0 143.907125 11.395918 g g2\n.. ... ... ... ... ...\n9 81.0 149.016644 10.755373 r r2\n9 85.0 130.071670 11.960329 r r2\n9 86.0 136.297942 11.419338 r r2\n9 88.0 134.215481 11.202422 r r2\n9 89.0 147.302751 11.271162 r r2\n\n[169 rows x 5 columns]", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
timefluxerrorbandband2
id
012.0134.26097510.685679gg2
021.0140.03722810.099401gg2
022.0148.41307910.131055gg2
024.0134.61613111.231055gg2
030.0143.90712511.395918gg2
..................
981.0149.01664410.755373rr2
985.0130.07167011.960329rr2
986.0136.29794211.419338rr2
988.0134.21548111.202422rr2
989.0147.30275111.271162rr2
\n

169 rows Ă— 5 columns

\n
" - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Add a new column so we can filter it out later.\n", "ens._source = ens._source.assign(band2=ens._source[\"band\"] + \"2\")\n", @@ -575,24 +408,14 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:37.096860Z", "start_time": "2023-08-30T14:58:36.937579Z" } }, - "outputs": [ - { - "data": { - "text/plain": " time flux error band\nid \n0 12.0 134.260975 10.685679 g\n0 21.0 140.037228 10.099401 g\n0 22.0 148.413079 10.131055 g\n0 24.0 134.616131 11.231055 g\n0 30.0 143.907125 11.395918 g\n.. ... ... ... ...\n9 81.0 149.016644 10.755373 r\n9 85.0 130.071670 11.960329 r\n9 86.0 136.297942 11.419338 r\n9 88.0 134.215481 11.202422 r\n9 89.0 147.302751 11.271162 r\n\n[169 rows x 4 columns]", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
timefluxerrorband
id
012.0134.26097510.685679g
021.0140.03722810.099401g
022.0148.41307910.131055g
024.0134.61613111.231055g
030.0143.90712511.395918g
...............
981.0149.01664410.755373r
985.0130.07167011.960329r
986.0136.29794211.419338r
988.0134.21548111.202422r
989.0147.30275111.271162r
\n

169 rows Ă— 4 columns

\n
" - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ens.select([\"time\", \"flux\", \"error\", \"band\"], table=\"source\")\n", "ens.compute(\"source\")" @@ -611,24 +434,14 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:37.097571Z", "start_time": "2023-08-30T14:58:36.958927Z" } }, - "outputs": [ - { - "data": { - "text/plain": " time flux error band lower_bnd\nid \n0 12.0 134.260975 10.685679 g 112.889618\n0 21.0 140.037228 10.099401 g 119.838427\n0 22.0 148.413079 10.131055 g 128.150969\n0 24.0 134.616131 11.231055 g 112.154020\n0 30.0 143.907125 11.395918 g 121.115288\n.. ... ... ... ... ...\n9 81.0 149.016644 10.755373 r 127.505899\n9 85.0 130.071670 11.960329 r 106.151012\n9 86.0 136.297942 11.419338 r 113.459267\n9 88.0 134.215481 11.202422 r 111.810638\n9 89.0 147.302751 11.271162 r 124.760428\n\n[169 rows x 5 columns]", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
timefluxerrorbandlower_bnd
id
012.0134.26097510.685679g112.889618
021.0140.03722810.099401g119.838427
022.0148.41307910.131055g128.150969
024.0134.61613111.231055g112.154020
030.0143.90712511.395918g121.115288
..................
981.0149.01664410.755373r127.505899
985.0130.07167011.960329r106.151012
986.0136.29794211.419338r113.459267
988.0134.21548111.202422r111.810638
989.0147.30275111.271162r124.760428
\n

169 rows Ă— 5 columns

\n
" - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ens.assign(table=\"source\", lower_bnd=lambda x: x[\"flux\"] - 2.0 * x[\"error\"])\n", "ens.compute(table=\"source\")" @@ -646,23 +459,14 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:37.492980Z", "start_time": "2023-08-30T14:58:36.981314Z" } }, - "outputs": [ - { - "data": { - "text/plain": "id\n0 {'g': -0.8833723170736909, 'r': -0.81291313232...\n1 {'g': -0.7866661902102343, 'r': -0.79927945599...\n2 {'g': -0.8650811883274131, 'r': -0.87939085289...\n3 {'g': -0.9140015912865537, 'r': -0.90284371456...\n4 {'g': -0.8232578922439672, 'r': -0.81922455220...\n5 {'g': -0.668795976899231, 'r': -0.784477243304...\n6 {'g': -0.8115552290707235, 'r': -0.90666227394...\n7 {'g': -0.6217573153267577, 'r': -0.60999974938...\n8 {'g': -0.7001359525394822, 'r': -0.73620435205...\n9 {'g': -0.7266040976469818, 'r': -0.68878460237...\nName: stetsonJ, dtype: object" - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# using tape analysis functions\n", "from tape.analysis import calc_stetson_J\n", @@ -673,43 +477,32 @@ }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "## Using light-curve package features\n", "\n", "`Ensemble.batch` also supports the use of [light-curve](https://pypi.org/project/light-curve/) package feature extractor:" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": 18, - "outputs": [ - { - "data": { - "text/plain": " amplitude anderson_darling_normal stetson_K\nid \n0 7.076052 0.177751 0.834036\n1 8.591493 0.513749 0.769344\n2 8.141189 0.392628 0.856307\n3 5.751674 0.295631 0.809191\n4 7.871321 0.555775 0.849305\n5 8.666473 0.342937 0.823194\n6 8.649326 0.241117 0.832815\n7 8.856443 1.141906 0.772267\n8 9.297713 0.984247 0.968132\n9 8.774109 0.335798 0.754355", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
amplitudeanderson_darling_normalstetson_K
id
07.0760520.1777510.834036
18.5914930.5137490.769344
28.1411890.3926280.856307
35.7516740.2956310.809191
47.8713210.5557750.849305
58.6664730.3429370.823194
68.6493260.2411170.832815
78.8564431.1419060.772267
89.2977130.9842470.968132
98.7741090.3357980.754355
\n
" - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-08-30T14:58:37.514514Z", + "start_time": "2023-08-30T14:58:37.494001Z" } - ], + }, + "outputs": [], "source": [ "import light_curve as licu\n", "\n", "extractor = licu.Extractor(licu.Amplitude(), licu.AndersonDarlingNormal(), licu.StetsonK())\n", "res = ens.batch(extractor, compute=True, band_to_calc=\"g\")\n", "res" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2023-08-30T14:58:37.514514Z", - "start_time": "2023-08-30T14:58:37.494001Z" - } - } + ] }, { "attachments": {}, @@ -724,7 +517,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:37.519972Z", @@ -760,23 +553,14 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:37.583850Z", "start_time": "2023-08-30T14:58:37.519056Z" } }, - "outputs": [ - { - "data": { - "text/plain": "id\n0 {'g': 140.03722843377682, 'r': 138.955084796142}\n1 {'g': 140.91515408243285, 'r': 141.44229039903...\n2 {'g': 139.42093950235392, 'r': 142.21649742828...\n3 {'g': 137.01337116218363, 'r': 139.05032340951...\n4 {'g': 134.61800608117045, 'r': 139.76505837028...\n5 {'g': 135.55144382138587, 'r': 139.41361800167...\n6 {'g': 142.93611137557423, 'r': 137.20679606847...\n7 {'g': 144.52647796976, 'r': 132.2470836256106}\n8 {'g': 144.7469760076462, 'r': 137.5226773361662}\n9 {'g': 136.89977482019205, 'r': 136.29794229244...\nName: id, dtype: object" - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Applying the function to the ensemble\n", "res = ens.batch(my_flux_average, \"flux\", \"band\", compute=True, meta=None, method=\"median\")\n", @@ -792,7 +576,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-08-30T14:58:37.764841Z", @@ -821,7 +605,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.6" }, "vscode": { "interpreter": { diff --git a/src/tape/__init__.py b/src/tape/__init__.py index e2dbb691..e2ac94ab 100644 --- a/src/tape/__init__.py +++ b/src/tape/__init__.py @@ -2,3 +2,4 @@ from .ensemble import * # noqa from .ensemble_frame import * # noqa from .timeseries import * # noqa +from .ensemble_readers import * # noqa diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index 42056f57..6befc6f8 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -2,10 +2,10 @@ import os import warnings import requests - import dask.dataframe as dd import numpy as np import pandas as pd + from dask.distributed import Client from .analysis.base import AnalysisFunction @@ -46,11 +46,13 @@ def __init__(self, client=True, **kwargs): self.source = None # Source Table EnsembleFrame self.object = None # Object Table EnsembleFrame + self._source_temp = [] # List of temporary columns in Source + self._object_temp = [] # List of temporary columns in Object + # Default to removing empty objects. self.keep_empty_objects = kwargs.get("keep_empty_objects", False) # Initialize critical column quantities - # Source self._id_col = None self._time_col = None self._flux_col = None @@ -58,10 +60,6 @@ def __init__(self, client=True, **kwargs): self._band_col = None self._provenance_col = None - # Object, _id_col is shared - self._nobs_tot_col = None - self._nobs_band_cols = [] - self.client = None self.cleanup_client = False @@ -543,7 +541,7 @@ def filter_from_series(self, keep_series, table="object"): self.update_frame(self._source[keep_series]) return self - def assign(self, table="object", **kwargs): + def assign(self, table="object", temporary=False, **kwargs): """Wrapper for dask.dataframe.DataFrame.assign() Parameters @@ -554,6 +552,13 @@ def assign(self, table="object", **kwargs): kwargs: dict of {str: callable or Series} Each argument is the name of a new column to add and its value specifies how to fill it. A callable is called for each row and a series is copied in. + temporary: 'bool', optional + Dictates whether the resulting columns are flagged as "temporary" + columns within the Ensemble. Temporary columns are dropped when + table syncs are performed, as their information is often made + invalid by future operations. For example, the number of + observations information is made invalid by a filter on the source + table. Defaults to False. Returns ------- @@ -571,10 +576,23 @@ def assign(self, table="object", **kwargs): self._lazy_sync_tables(table) if table == "object": + pre_cols = self._object.columns self.update_frame(self._object.assign(**kwargs)) + self._object.set_dirty(True) + post_cols = self._object.columns + + if temporary: + self._object_temp.extend(col for col in post_cols if col not in pre_cols) elif table == "source": + pre_cols = self._source.columns self.update_frame(self._source.assign(**kwargs)) + self._source.set_dirty(True) + post_cols = self._source.columns + + if temporary: + self._source_temp.extend(col for col in post_cols if col not in pre_cols) + else: raise ValueError(f"{table} is not one of 'object' or 'source'") return self @@ -611,22 +629,27 @@ def coalesce(self, input_cols, output_col, table="object", drop_inputs=False): else: raise ValueError(f"{table} is not one of 'object' or 'source'") - # Create a subset dataframe with the coalesced columns - # Drop index for dask series operations - unfortunate - coal_ddf = table_ddf[input_cols].reset_index() + def coalesce_partition(df, input_cols, output_col): + """Coalescing function for a single partition (pandas dataframe)""" - # Coalesce each column iteratively - i = 0 - coalesce_col = coal_ddf[input_cols[0]] - while i < len(input_cols) - 1: - coalesce_col = coalesce_col.combine_first(coal_ddf[input_cols[i + 1]]) - i += 1 - print("am I using this code") - # Assign the new column to the subset df, and reintroduce index - coal_ddf = coal_ddf.assign(**{output_col: coalesce_col}).set_index(self._id_col) + # Create a subset dataframe per input column + # Rename column to output to allow combination + input_dfs = [] + for col in input_cols: + col_df = df[[col]] + input_dfs.append(col_df.rename(columns={col: output_col})) - # assign the result to the desired column name - table_ddf = table_ddf.assign(**{output_col: coal_ddf[output_col]}) + # Combine each dataframe + coal_df = input_dfs.pop() + while input_dfs: + coal_df = coal_df.combine_first(input_dfs.pop()) + + # Assign the output column to the partition dataframe + out_df = df.assign(**{output_col: coal_df[output_col]}) + + return out_df + + table_ddf = table_ddf.map_partitions(lambda x: coalesce_partition(x, input_cols, output_col)) # Drop the input columns if wanted if drop_inputs: @@ -663,6 +686,74 @@ def coalesce(self, input_cols, output_col, table="object", drop_inputs=False): return self + def calc_nobs(self, by_band=False, label="nobs", temporary=True): + """Calculates the number of observations per lightcurve. + + Parameters + ---------- + by_band: `bool`, optional + If True, also calculates the number of observations for each band + in addition to providing the number of observations in total + label: `str`, optional + The label used to generate output columns. "_total" and the band + labels (e.g. "_g") are appended. + temporary: 'bool', optional + Dictates whether the resulting columns are flagged as "temporary" + columns within the Ensemble. Temporary columns are dropped when + table syncs are performed, as their information is often made + invalid by future operations. For example, the number of + observations information is made invalid by a filter on the source + table. Defaults to True. + + Returns + ------- + ensemble: `tape.ensemble.Ensemble` + The ensemble object with nobs columns added to the object table. + """ + + if by_band: + band_counts = ( + self._source.groupby([self._id_col])[self._band_col] # group by each object + .value_counts() # count occurence of each band + .to_frame() # convert series to dataframe + .reset_index() # break up the multiindex + .categorize(columns=[self._band_col]) # retype the band labels as categories + .pivot_table(values=self._band_col, index=self._id_col, columns=self._band_col, aggfunc="sum") + ) # the pivot_table call makes each band_count a column of the id_col row + + # repartition the result to align with object + if self._object.known_divisions: + self._object.divisions = tuple([None for i in range(self._object.npartitions + 1)]) + band_counts = band_counts.repartition(npartitions=self._object.npartitions) + else: + band_counts = band_counts.repartition(npartitions=self._object.npartitions) + + # short-hand for calculating nobs_total + band_counts["total"] = band_counts[list(band_counts.columns)].sum(axis=1) + + bands = band_counts.columns.values + self._object = self._object.assign(**{label + "_" + band: band_counts[band] for band in bands}) + + if temporary: + self._object_temp.extend(label + "_" + band for band in bands) + + else: + counts = self._source.groupby([self._id_col])[[self._band_col]].aggregate("count") + + # repartition the result to align with object + if self._object.known_divisions: + self._object.divisions = tuple([None for i in range(self._object.npartitions + 1)]) + counts = counts.repartition(npartitions=self._object.npartitions) + else: + counts = counts.repartition(npartitions=self._object.npartitions) + + self._object = self._object.assign(**{label + "_total": counts[self._band_col]}) + + if temporary: + self._object_temp.extend([label + "_total"]) + + return self + def prune(self, threshold=50, col_name=None): """remove objects with less observations than a given threshold @@ -672,19 +763,24 @@ def prune(self, threshold=50, col_name=None): The minimum number of observations needed to retain an object. Default is 50. col_name: `str`, optional - The name of the column to assess the threshold + The name of the column to assess the threshold if available in + the object table. If not specified, the ensemble will calculate + the number of observations and filter on the total (sum across + bands). Returns ------- ensemble: `tape.ensemble.Ensemble` The ensemble object with pruned rows removed """ - if not col_name: - col_name = self._nobs_tot_col # Sync Required if source is dirty self._lazy_sync_tables(table="object") + if not col_name: + self.calc_nobs(label="nobs") + col_name = "nobs_total" + # Mask on object table mask = self._object[col_name] >= threshold self.update_frame(self._object[mask]) @@ -1003,7 +1099,7 @@ def from_pandas( """ # Construct Dask DataFrames of the source and object tables source = dd.from_pandas(source_frame, npartitions=npartitions) - object = None if object_frame is None else dd.from_pandas(object_frame) + object = None if object_frame is None else dd.from_pandas(object_frame, npartitions=npartitions) return self.from_dask_dataframe( source, object_frame=object, @@ -1062,22 +1158,10 @@ def from_dask_dataframe( if object_frame is None: # generate an indexed object table from source self.update_frame(self._generate_object_table()) - self._nobs_bands = [col for col in list(self._object.columns) if col != self._nobs_tot_col] + else: # TODO(wbeebe@uw.edu): Determine most efficient way to convert to SourceFrame/ObjectFrame self.update_frame(ObjectFrame.from_dask_dataframe(object_frame, ensemble=self)) - if self._nobs_band_cols is None: - # sets empty nobs cols in object - unq_filters = np.unique(self._source[self._band_col]) - self._nobs_band_cols = [f"nobs_{filt}" for filt in unq_filters] - for col in self._nobs_band_cols: - self._object[col] = np.nan - - # Handle nobs_total column - if self._nobs_tot_col is None: - self._object["nobs_total"] = np.nan - self._nobs_tot_col = "nobs_total" - self.update_frame(self._object.set_index(self._id_col)) # Optionally sync the tables, recalculates nobs columns @@ -1087,9 +1171,9 @@ def from_dask_dataframe( self._sync_tables() if npartitions and npartitions > 1: - self.update_frame(self._source.repartition(npartitions=npartitions)) + self._source = self._source.repartition(npartitions=npartitions) elif partition_size: - self.update_frame(self._source.repartition(partition_size=partition_size)) + self._source = self._source.repartition(partition_size=partition_size) return self @@ -1148,8 +1232,6 @@ def make_column_map(self): err_col=self._err_col, band_col=self._band_col, provenance_col=self._provenance_col, - nobs_total_col=self._nobs_tot_col, - nobs_band_cols=self._nobs_band_cols, ) return result @@ -1211,10 +1293,6 @@ def _load_column_mapper(self, column_mapper, **kwargs): # Assign optional columns if provided if column_mapper.map["provenance_col"] is not None: self._provenance_col = column_mapper.map["provenance_col"] - if column_mapper.map["nobs_total_col"] is not None: - self._nobs_tot_col = column_mapper.map["nobs_total_col"] - if column_mapper.map["nobs_band_cols"] is not None: - self._nobs_band_cols = column_mapper.map["nobs_band_cols"] else: raise ValueError(f"Missing required column mapping information: {needed}") @@ -1281,17 +1359,13 @@ def from_parquet( columns = [self._time_col, self._flux_col, self._err_col, self._band_col] if self._provenance_col is not None: columns.append(self._provenance_col) - if self._nobs_tot_col is not None: - columns.append(self._nobs_tot_col) - if self._nobs_band_cols is not None: - for col in self._nobs_band_cols: - columns.append(col) # Read in the source parquet file(s) source = SourceFrame.from_parquet(source_file, index=self._id_col, columns=columns, ensemble=self) # Generate a provenance column if not provided if self._provenance_col is None: + source["provenance"] = provenance_label source["provenance"] = provenance_label self._provenance_col = "provenance" @@ -1395,21 +1469,15 @@ def from_source_dict(self, source_dict, column_mapper=None, npartitions=1, **kwa **kwargs, ) - def convert_flux_to_mag(self, flux_col, zero_point, err_col=None, zp_form="mag", out_col_name=None): + def convert_flux_to_mag(self, zero_point, zp_form="mag", out_col_name=None, flux_col=None, err_col=None): """Converts a flux column into a magnitude column. Parameters ---------- - flux_col: 'str' - The name of the ensemble flux column to convert into magnitudes. - zero_point: 'str' + zero_point: 'str' or 'float' The name of the ensemble column containing the zero point - information for column transformation. - err_col: 'str', optional - The name of the ensemble column containing the errors to propagate. - Errors are propagated using the following approximation: - Err= (2.5/log(10))*(flux_error/flux), which holds mainly when the - error in flux is much smaller than the flux. + information for column transformation. Alternatively, a single + float number to apply for all fluxes. zp_form: `str`, optional The form of the zero point column, either "flux" or "magnitude"/"mag". Determines how the zero point (zp) is applied in @@ -1420,6 +1488,15 @@ def convert_flux_to_mag(self, flux_col, zero_point, err_col=None, zp_form="mag", The name of the output magnitude column, if None then the output is just the flux column name + "_mag". The error column is also generated as the out_col_name + "_err". + flux_col: 'str', optional + The name of the ensemble flux column to convert into magnitudes. + Uses the Ensemble mapped flux column if not specified. + err_col: 'str', optional + The name of the ensemble column containing the errors to propagate. + Errors are propagated using the following approximation: + Err= (2.5/log(10))*(flux_error/flux), which holds mainly when the + error in flux is much smaller than the flux. Uses the Ensemble + mapped error column if not specified. Returns ---------- @@ -1427,19 +1504,35 @@ def convert_flux_to_mag(self, flux_col, zero_point, err_col=None, zp_form="mag", The ensemble object with a new magnitude (and error) column. """ + + # Assign Ensemble cols if not provided + if flux_col is None: + flux_col = self._flux_col + if err_col is None: + err_col = self._err_col + if out_col_name is None: out_col_name = flux_col + "_mag" if zp_form == "flux": # mag = -2.5*np.log10(flux/zp) - self.update_frame(self._source.assign( - **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / x[zero_point])} - )) + if isinstance(zero_point, str): + self.update_frame(self._source.assign( + **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / x[zero_point])} + )) + else: + self.update_frame(self._source.assign( + **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / zero_point)} + )) elif zp_form == "magnitude" or zp_form == "mag": # mag = -2.5*np.log10(flux) + zp - self.update_frame(self._source.assign( - **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + x[zero_point]} - )) - + if isinstance(zero_point, str): + self.update_frame(self._source.assign( + **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + x[zero_point]} + )) + else: + self.update_frame(self._source.assign( + **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + zero_point} + )) else: raise ValueError(f"{zp_form} is not a valid zero_point format.") @@ -1452,51 +1545,14 @@ def convert_flux_to_mag(self, flux_col, zero_point, err_col=None, zp_form="mag", return self def _generate_object_table(self): - """Generate the object table from the source table.""" - counts = self._source.groupby([self._id_col, self._band_col])[self._time_col].aggregate("count") - res = ( - counts.to_frame() - .reset_index() - .categorize(columns=[self._band_col]) - .pivot_table(values=self._time_col, index=self._id_col, columns=self._band_col, aggfunc="sum") - ) + """Generate an empty object table from the source table.""" + sor_idx = self._source.index.unique() + obj_df = pd.DataFrame(index=sor_idx) # Convert the resulting dataframe into an ObjectFrame - # TODO(wbeebe@uw.edu): Inveestigate if we can correctly infer that `res` is an ObjectFrame instead - res = ObjectFrame.from_dask_dataframe(res, ensemble=self) - - # If the ensemble's keep_empty_objects attribute is True and there are previous - # objects, then copy them into the res table with counts of zero. - if self.keep_empty_objects and self._object is not None: - prev_partitions = self._object.npartitions - - # Check that there are existing object ids. - object_inds = self._object.index.unique().values.compute() - if len(object_inds) > 0: - # Determine which object IDs are missing from the source table. - source_inds = self._source.index.unique().values.compute() - missing_inds = np.setdiff1d(object_inds, source_inds).tolist() - - # Create a dataframe of the missing IDs with zeros for all bands and counts. - rows = {self._id_col: missing_inds} - for i in res.columns.values: - rows[i] = [0] * len(missing_inds) - - zero_pdf = pd.DataFrame(rows, dtype=int).set_index(self._id_col) - zero_ddf = dd.from_pandas(zero_pdf, sort=True, npartitions=1) - - # Concatenate the zero dataframe onto the results. - res = dd.concat([res, zero_ddf], interleave_partitions=True).astype(int) - res = res.repartition(npartitions=prev_partitions) - - # Rename bands to nobs_[band] - band_cols = {col: f"nobs_{col}" for col in list(res.columns)} - res = res.rename(columns=band_cols) - - # Add total nobs by summing across each band. - if self._nobs_tot_col is None: - self._nobs_tot_col = "nobs_total" - res[self._nobs_tot_col] = res.sum(axis=1) + # TODO(wbeebe): Switch for a cleaner loading fucnction + res = ObjectFrame.from_dask_dataframe( + dd.from_pandas(obj_df, npartitions=int(np.ceil(self._source.npartitions / 100))), ensemble=self) return res @@ -1530,23 +1586,28 @@ def _sync_tables(self): if self._object.is_dirty(): # Sync Object to Source; remove any missing objects from source - s_cols = self._source.columns - self.update_frame(self._source.merge( - self._object, how="right", on=[self._id_col], suffixes=(None, "_obj") - )) - cols_to_drop = [col for col in self._source.columns if col not in s_cols] - self.update_frame(self._source.drop(cols_to_drop, axis=1)) - self.update_frame(self._source.persist()) # persist source - - if self._source._is_dirty: # not elif - # Generate a new object table; updates n_obs, removes missing ids - new_obj = self._generate_object_table() - - # Join old obj to new obj; pulls in other existing obj columns - self.update_frame(new_obj.join(self._object, on=self._id_col, how="left", lsuffix="", rsuffix="_old")) - old_cols = [col for col in list(self._object.columns) if "_old" in col] - self.update_frame(self._object.drop(old_cols, axis=1)) - self.update_frame(self._object.persist()) # persist object + obj_idx = list(self._object.index.compute()) + self.update_frame(self._source.map_partitions(lambda x: x[x.index.isin(obj_idx)])) + self.update_frame(self._source.persist()) # persist the source frame + + # Drop Temporary Source Columns on Sync + if len(self._source_temp): + self.update_frame(self._source.drop(columns=self._source_temp)) + print(f"Temporary columns dropped from Source Table: {self._source_temp}") + self._source_temp = [] + + if self._source.is_dirty(): # not elif + if not self.keep_empty_objects: + # Sync Source to Object; remove any objects that do not have sources + sor_idx = list(self._source.index.unique().compute()) + self.update_frame(self._object.map_partitions(lambda x: x[x.index.isin(sor_idx)])) + self.update_frame(self._object.persist()) # persist the object frame + + # Drop Temporary Object Columns on Sync + if len(self._object_temp): + self.update_frame(self._object.drop(columns=self._object_temp)) + print(f"Temporary columns dropped from Object Table: {self._object_temp}") + self._object_temp = [] # Now synced and clean self._source.set_dirty(False) diff --git a/src/tape/ensemble_readers.py b/src/tape/ensemble_readers.py new file mode 100644 index 00000000..119bb206 --- /dev/null +++ b/src/tape/ensemble_readers.py @@ -0,0 +1,328 @@ +""" + The following package-level methods can be used to create a new Ensemble object + by reading in the given data source. +""" +import requests + +import dask.dataframe as dd + +from tape import Ensemble +from tape.utils import ColumnMapper + + +def read_pandas_dataframe( + source_frame, + object_frame=None, + dask_client=True, + column_mapper=None, + sync_tables=True, + npartitions=None, + partition_size=None, + **kwargs, +): + """Read in Pandas dataframe(s) and return an ensemble object + + Parameters + ---------- + source_frame: 'pandas.Dataframe' + A Dask dataframe that contains source information to be read into the ensemble + object_frame: 'pandas.Dataframe', optional + If not specified, the object frame is generated from the source frame + dask_client: `dask.distributed.client` or `bool`, optional + Accepts an existing `dask.distributed.Client`, or creates one if + `client=True`, passing any additional kwargs to a + dask.distributed.Client constructor call. If `client=False`, the + Ensemble is created without a distributed client. + column_mapper: 'ColumnMapper' object + If provided, the ColumnMapper is used to populate relevant column + information mapped from the input dataset. + sync_tables: 'bool', optional + In the case where an `object_frame`is provided, determines whether an + initial sync is performed between the object and source tables. If + not performed, dynamic information like the number of observations + may be out of date until a sync is performed internally. + npartitions: `int`, optional + If specified, attempts to repartition the ensemble to the specified + number of partitions + partition_size: `int`, optional + If specified, attempts to repartition the ensemble to partitions + of size `partition_size`. + + Returns + ---------- + ensemble: `tape.ensemble.Ensemble` + The ensemble object with the Dask dataframe data loaded. + """ + # Construct Dask DataFrames of the source and object tables + source = dd.from_pandas(source_frame, npartitions=npartitions) + object = None if object_frame is None else dd.from_pandas(object_frame, npartitions=npartitions) + + return read_dask_dataframe( + source_frame=source, + object_frame=object, + dask_client=dask_client, + column_mapper=column_mapper, + sync_tables=sync_tables, + npartitions=npartitions, + partition_size=partition_size, + **kwargs, + ) + + +def read_dask_dataframe( + source_frame, + object_frame=None, + dask_client=True, + column_mapper=None, + sync_tables=True, + npartitions=None, + partition_size=None, + **kwargs, +): + """Read in Dask dataframe(s) and return an ensemble object + + Parameters + ---------- + source_frame: 'dask.Dataframe' + A Dask dataframe that contains source information to be read into the ensemble + object_frame: 'dask.Dataframe', optional + If not specified, the object frame is generated from the source frame + dask_client: `dask.distributed.client` or `bool`, optional + Accepts an existing `dask.distributed.Client`, or creates one if + `client=True`, passing any additional kwargs to a + dask.distributed.Client constructor call. If `client=False`, the + Ensemble is created without a distributed client. + column_mapper: 'ColumnMapper' object + If provided, the ColumnMapper is used to populate relevant column + information mapped from the input dataset. + sync_tables: 'bool', optional + In the case where an `object_frame`is provided, determines whether an + initial sync is performed between the object and source tables. If + not performed, dynamic information like the number of observations + may be out of date until a sync is performed internally. + npartitions: `int`, optional + If specified, attempts to repartition the ensemble to the specified + number of partitions + partition_size: `int`, optional + If specified, attempts to repartition the ensemble to partitions + of size `partition_size`. + + Returns + ---------- + ensemble: `tape.ensemble.Ensemble` + The ensemble object with the Dask dataframe data loaded. + """ + new_ens = Ensemble(dask_client, **kwargs) + + new_ens.from_dask_dataframe( + source_frame=source_frame, + object_frame=object_frame, + column_mapper=column_mapper, + sync_tables=sync_tables, + npartitions=npartitions, + partition_size=partition_size, + **kwargs, + ) + + return new_ens + + +def read_parquet( + source_file, + object_file=None, + column_mapper=None, + dask_client=True, + provenance_label="survey_1", + sync_tables=True, + additional_cols=True, + npartitions=None, + partition_size=None, + **kwargs, +): + """Read in parquet file(s) into an ensemble object + + Parameters + ---------- + source_file: 'str' + Path to a parquet file, or multiple parquet files that contain + source information to be read into the ensemble + object_file: 'str' + Path to a parquet file, or multiple parquet files that contain + object information. If not specified, it is generated from the + source table + column_mapper: 'ColumnMapper' object + If provided, the ColumnMapper is used to populate relevant column + information mapped from the input dataset. + dask_client: `dask.distributed.client` or `bool`, optional + Accepts an existing `dask.distributed.Client`, or creates one if + `client=True`, passing any additional kwargs to a + dask.distributed.Client constructor call. If `client=False`, the + Ensemble is created without a distributed client. + provenance_label: 'str', optional + Determines the label to use if a provenance column is generated + sync_tables: 'bool', optional + In the case where object files are loaded in, determines whether an + initial sync is performed between the object and source tables. If + not performed, dynamic information like the number of observations + may be out of date until a sync is performed internally. + additional_cols: 'bool', optional + Boolean to indicate whether to carry in columns beyond the + critical columns, true will, while false will only load the columns + containing the critical quantities (id,time,flux,err,band) + npartitions: `int`, optional + If specified, attempts to repartition the ensemble to the specified + number of partitions + partition_size: `int`, optional + If specified, attempts to repartition the ensemble to partitions + of size `partition_size`. + + Returns + ---------- + ensemble: `tape.ensemble.Ensemble` + The ensemble object with parquet data loaded + """ + + new_ens = Ensemble(dask_client, **kwargs) + + new_ens.from_parquet( + source_file=source_file, + object_file=object_file, + column_mapper=column_mapper, + provenance_label=provenance_label, + sync_tables=sync_tables, + additional_cols=additional_cols, + npartitions=npartitions, + partition_size=partition_size, + **kwargs, + ) + + return new_ens + + +def read_hipscat( + dir, + source_subdir="source", + object_subdir="object", + column_mapper=None, + dask_client=True, + **kwargs, +): + """Read in parquet files from a hipscat-formatted directory structure + + Parameters + ---------- + dir: 'str' + Path to the directory structure + source_subdir: 'str' + Path to the subdirectory which contains source files + object_subdir: 'str' + Path to the subdirectory which contains object files, if None then + files will only be read from the source_subdir + column_mapper: 'ColumnMapper' object + If provided, the ColumnMapper is used to populate relevant column + information mapped from the input dataset. + dask_client: `dask.distributed.client` or `bool`, optional + Accepts an existing `dask.distributed.Client`, or creates one if + `client=True`, passing any additional kwargs to a + dask.distributed.Client constructor call. If `client=False`, the + Ensemble is created without a distributed client. + **kwargs: + keyword arguments passed along to + `tape.ensemble.Ensemble.from_parquet` + + Returns + ---------- + ensemble: `tape.ensemble.Ensemble` + The ensemble object with parquet data loaded + """ + + new_ens = Ensemble(dask_client, **kwargs) + + new_ens.from_hipscat( + dir=dir, + source_subdir=source_subdir, + object_subdir=object_subdir, + column_mapper=column_mapper, + **kwargs, + ) + + return new_ens + + +def read_source_dict(source_dict, column_mapper=None, npartitions=1, dask_client=True, **kwargs): + """Load the sources into an ensemble from a dictionary. + + Parameters + ---------- + source_dict: 'dict' + The dictionary containing the source information. + column_mapper: 'ColumnMapper' object + If provided, the ColumnMapper is used to populate relevant column + information mapped from the input dataset. + npartitions: `int`, optional + If specified, attempts to repartition the ensemble to the specified + number of partitions + dask_client: `dask.distributed.client` or `bool`, optional + Accepts an existing `dask.distributed.Client`, or creates one if + `client=True`, passing any additional kwargs to a + dask.distributed.Client constructor call. If `client=False`, the + Ensemble is created without a distributed client. + + Returns + ---------- + ensemble: `tape.ensemble.Ensemble` + The ensemble object with dictionary data loaded + """ + + new_ens = Ensemble(dask_client, **kwargs) + + new_ens.from_source_dict( + source_dict=source_dict, column_mapper=column_mapper, npartitions=npartitions, **kwargs + ) + + return new_ens + + +def read_dataset(dataset, dask_client=True, **kwargs): + """Load the ensemble from a TAPE dataset. + + Parameters + ---------- + dataset: 'str' + The name of the dataset to import + dask_client: `dask.distributed.client` or `bool`, optional + Accepts an existing `dask.distributed.Client`, or creates one if + `client=True`, passing any additional kwargs to a + dask.distributed.Client constructor call. If `client=False`, the + Ensemble is created without a distributed client. + + Returns + ------- + ensemble: `tape.ensemble.Ensemble` + The ensemble object with the dataset loaded + """ + + req = requests.get( + "https://github.com/lincc-frameworks/tape_benchmarking/blob/main/data/datasets.json?raw=True" + ) + datasets_file = req.json() + dataset_info = datasets_file[dataset] + + # Make column map from dataset + dataset_map = dataset_info["column_map"] + col_map = ColumnMapper( + id_col=dataset_map["id"], + time_col=dataset_map["time"], + flux_col=dataset_map["flux"], + err_col=dataset_map["error"], + band_col=dataset_map["band"], + ) + + return read_parquet( + source_file=dataset_info["source_file"], + object_file=dataset_info["object_file"], + column_mapper=col_map, + provenance_label=dataset, + dask_client=dask_client, + **kwargs, + ) diff --git a/src/tape/utils/column_mapper/column_mapper.py b/src/tape/utils/column_mapper/column_mapper.py index 185d7e22..48d3ee6e 100644 --- a/src/tape/utils/column_mapper/column_mapper.py +++ b/src/tape/utils/column_mapper/column_mapper.py @@ -12,8 +12,6 @@ def __init__( err_col=None, band_col=None, provenance_col=None, - nobs_total_col=None, - nobs_band_cols=None, ): """ @@ -32,12 +30,6 @@ def __init__( provenance_col: 'str', optional Identifies which column contains the provenance information, if None the provenance column is generated. - nobs_band_cols: list of 'str', optional - Identifies which columns contain number of observations for each - band, if available in the input object file - nobs_total_col: 'str', optional - Identifies which column contains the total number of observations, - if available in the input object file Returns ------- @@ -53,8 +45,6 @@ def __init__( "err_col": err_col, "band_col": band_col, "provenance_col": provenance_col, - "nobs_total_col": nobs_total_col, - "nobs_band_cols": nobs_band_cols, } self.required = [ @@ -64,8 +54,6 @@ def __init__( Column("err_col", True), Column("band_col", True), Column("provenance_col", False), - Column("nobs_total_col", False), - Column("nobs_band_cols", False), ] self.known_maps = {"ZTF": ZTFColumnMapper} @@ -135,8 +123,6 @@ def assign( err_col=None, band_col=None, provenance_col=None, - nobs_total_col=None, - nobs_band_cols=None, ): """Updates a given set of columns @@ -169,8 +155,6 @@ def assign( "err_col": err_col, "band_col": band_col, "provenance_col": provenance_col, - "nobs_total_col": nobs_total_col, - "nobs_band_cols": nobs_band_cols, } for item in assign_map.items(): @@ -192,8 +176,6 @@ def _set_known_map(self): "err_col": "psFluxErr", "band_col": "filterName", "provenance_col": None, - "nobs_total_col": "nobs_total", - "nobs_band_cols": None, } return self diff --git a/tests/tape_tests/conftest.py b/tests/tape_tests/conftest.py index a62c6e2e..e416a04a 100644 --- a/tests/tape_tests/conftest.py +++ b/tests/tape_tests/conftest.py @@ -2,14 +2,212 @@ import numpy as np import pandas as pd import dask.dataframe as dd - import pytest +import tape + from dask.distributed import Client from tape import Ensemble from tape.utils import ColumnMapper +@pytest.fixture +def create_test_rows(): + num_points = 1000 + all_bands = np.array(["r", "g", "b", "i"]) + + rows = { + "id": 8000 + (np.arange(num_points) % 5), + "time": np.arange(num_points), + "flux": np.arange(num_points) % len(all_bands), + "band": np.repeat(all_bands, num_points / len(all_bands)), + "err": 0.1 * (np.arange(num_points) % 10), + "count": np.arange(num_points), + "something_else": np.full(num_points, None), + } + + return rows + + +@pytest.fixture +def create_test_column_mapper(): + return ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="err", band_col="band") + + +@pytest.fixture +@pytest.mark.parametrize("create_test_rows", [("create_test_rows")]) +def create_test_source_table(create_test_rows, npartitions=1): + return dd.from_dict(create_test_rows, npartitions) + + +@pytest.fixture +def create_test_object_table(npartitions=1): + n_obj = 5 + id = 8000 + np.arange(n_obj) + name = id.astype(str) + return dd.from_dict(dict(id=id, name=name), npartitions) + + +# pylint: disable=redefined-outer-name +@pytest.fixture +@pytest.mark.parametrize( + "create_test_source_table, create_test_column_mapper", + [("create_test_source_table", "create_test_column_mapper")], +) +def read_dask_dataframe_ensemble(dask_client, create_test_source_table, create_test_column_mapper): + return tape.read_dask_dataframe( + dask_client=dask_client, + source_frame=create_test_source_table, + column_mapper=create_test_column_mapper, + ) + + +# pylint: disable=redefined-outer-name +@pytest.fixture +@pytest.mark.parametrize( + "create_test_source_table, create_test_object_table, create_test_column_mapper", + [("create_test_source_table", "create_test_object_table", "create_test_column_mapper")], +) +def read_dask_dataframe_with_object_ensemble( + dask_client, create_test_source_table, create_test_object_table, create_test_column_mapper +): + return tape.read_dask_dataframe( + source_frame=create_test_source_table, + object_frame=create_test_object_table, + dask_client=dask_client, + column_mapper=create_test_column_mapper, + ) + + +# pylint: disable=redefined-outer-name +@pytest.fixture +@pytest.mark.parametrize( + "create_test_rows, create_test_column_mapper", [("create_test_rows", "create_test_column_mapper")] +) +def read_pandas_ensemble(dask_client, create_test_rows, create_test_column_mapper): + return tape.read_pandas_dataframe( + source_frame=pd.DataFrame(create_test_rows), + column_mapper=create_test_column_mapper, + dask_client=dask_client, + npartitions=1, + ) + + +# pylint: disable=redefined-outer-name +@pytest.fixture +@pytest.mark.parametrize( + "create_test_rows, create_test_column_mapper", [("create_test_rows", "create_test_column_mapper")] +) +def read_pandas_with_object_ensemble(dask_client, create_test_rows, create_test_column_mapper): + n_obj = 5 + id = 8000 + np.arange(n_obj) + name = id.astype(str) + object_table = pd.DataFrame(dict(id=id, name=name)) + + """Create an Ensemble from pandas dataframes.""" + return tape.read_pandas_dataframe( + dask_client=dask_client, + source_frame=pd.DataFrame(create_test_rows), + object_frame=object_table, + column_mapper=create_test_column_mapper, + npartitions=1, + ) + + +# pylint: disable=redefined-outer-name +@pytest.fixture +def read_parquet_ensemble_without_client(): + """Create an Ensemble from parquet data without a dask client.""" + return tape.read_parquet( + source_file="tests/tape_tests/data/source/test_source.parquet", + object_file="tests/tape_tests/data/object/test_object.parquet", + dask_client=False, + id_col="ps1_objid", + time_col="midPointTai", + band_col="filterName", + flux_col="psFlux", + err_col="psFluxErr", + ) + + +# pylint: disable=redefined-outer-name +@pytest.fixture +def read_parquet_ensemble(dask_client): + """Create an Ensemble from parquet data.""" + return tape.read_parquet( + source_file="tests/tape_tests/data/source/test_source.parquet", + object_file="tests/tape_tests/data/object/test_object.parquet", + dask_client=dask_client, + id_col="ps1_objid", + time_col="midPointTai", + band_col="filterName", + flux_col="psFlux", + err_col="psFluxErr", + ) + + +# pylint: disable=redefined-outer-name +@pytest.fixture +def read_parquet_ensemble_from_source(dask_client): + """Create an Ensemble from parquet data, with object file withheld.""" + return tape.read_parquet( + source_file="tests/tape_tests/data/source/test_source.parquet", + dask_client=dask_client, + id_col="ps1_objid", + time_col="midPointTai", + band_col="filterName", + flux_col="psFlux", + err_col="psFluxErr", + ) + + +# pylint: disable=redefined-outer-name +@pytest.fixture +def read_parquet_ensemble_with_column_mapper(dask_client): + """Create an Ensemble from parquet data, with object file withheld.""" + colmap = ColumnMapper().assign( + id_col="ps1_objid", + time_col="midPointTai", + flux_col="psFlux", + err_col="psFluxErr", + band_col="filterName", + ) + + return tape.read_parquet( + source_file="tests/tape_tests/data/source/test_source.parquet", + column_mapper=colmap, + dask_client=dask_client, + ) + + +# pylint: disable=redefined-outer-name +@pytest.fixture +def read_parquet_ensemble_with_known_column_mapper(dask_client): + """Create an Ensemble from parquet data, with object file withheld.""" + colmap = ColumnMapper().use_known_map("ZTF") + + return tape.read_parquet( + source_file="tests/tape_tests/data/source/test_source.parquet", + column_mapper=colmap, + dask_client=dask_client, + ) + + +# pylint: disable=redefined-outer-name +@pytest.fixture +def read_parquet_ensemble_from_hipscat(dask_client): + """Create an Ensemble from a hipscat/hive-style directory.""" + return tape.read_hipscat( + "tests/tape_tests/data", + id_col="ps1_objid", + time_col="midPointTai", + band_col="filterName", + flux_col="psFlux", + err_col="psFluxErr", + dask_client=dask_client, + ) + + @pytest.fixture(scope="package", name="dask_client") def dask_client(): """Create a single client for use by all unit test cases.""" @@ -162,7 +360,46 @@ def dask_dataframe_ensemble(dask_client): cmap = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="err", band_col="band") ens.from_dask_dataframe( - dd.from_dict(rows, npartitions=1), + source_frame=dd.from_dict(rows, npartitions=1), + column_mapper=cmap, + ) + + return ens + + +# pylint: disable=redefined-outer-name +@pytest.fixture +def dask_dataframe_with_object_ensemble(dask_client): + """Create an Ensemble from parquet data.""" + ens = Ensemble(client=dask_client) + + n_obj = 5 + id = 8000 + np.arange(n_obj) + name = id.astype(str) + object_table = dd.from_dict( + dict(id=id, name=name), + npartitions=1, + ) + + num_points = 1000 + all_bands = np.array(["r", "g", "b", "i"]) + source_table = dd.from_dict( + { + "id": 8000 + (np.arange(num_points) % n_obj), + "time": np.arange(num_points), + "flux": np.arange(num_points) % len(all_bands), + "band": np.repeat(all_bands, num_points / len(all_bands)), + "err": 0.1 * (np.arange(num_points) % 10), + "count": np.arange(num_points), + "something_else": np.full(num_points, None), + }, + npartitions=1, + ) + cmap = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="err", band_col="band") + + ens.from_dask_dataframe( + source_frame=source_table, + object_frame=object_table, column_mapper=cmap, ) @@ -196,6 +433,44 @@ def pandas_ensemble(dask_client): return ens + +# pylint: disable=redefined-outer-name +@pytest.fixture +def pandas_with_object_ensemble(dask_client): + """Create an Ensemble from parquet data.""" + ens = Ensemble(client=dask_client) + + n_obj = 5 + id = 8000 + np.arange(n_obj) + name = id.astype(str) + object_table = pd.DataFrame( + dict(id=id, name=name), + ) + + num_points = 1000 + all_bands = np.array(["r", "g", "b", "i"]) + source_table = pd.DataFrame( + { + "id": 8000 + (np.arange(num_points) % n_obj), + "time": np.arange(num_points), + "flux": np.arange(num_points) % len(all_bands), + "band": np.repeat(all_bands, num_points / len(all_bands)), + "err": 0.1 * (np.arange(num_points) % 10), + "count": np.arange(num_points), + "something_else": np.full(num_points, None), + }, + ) + cmap = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="err", band_col="band") + + ens.from_pandas( + source_frame=source_table, + object_frame=object_table, + column_mapper=cmap, + npartitions=1, + ) + + return ens + # pylint: disable=redefined-outer-name @pytest.fixture def ensemble_from_source_dict(dask_client): diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index e2aecd6f..3d3bbf80 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -5,6 +5,7 @@ import numpy as np import pandas as pd import pytest +import tape from tape import Ensemble, EnsembleFrame, ObjectFrame, SourceFrame, TapeFrame, TapeObjectFrame, TapeSourceFrame from tape.analysis.stetsonj import calc_stetson_J @@ -36,9 +37,15 @@ def test_with_client(): "parquet_ensemble_from_hipscat", "parquet_ensemble_with_column_mapper", "parquet_ensemble_with_known_column_mapper", + "read_parquet_ensemble", + "read_parquet_ensemble_without_client", + "read_parquet_ensemble_from_source", + "read_parquet_ensemble_from_hipscat", + "read_parquet_ensemble_with_column_mapper", + "read_parquet_ensemble_with_known_column_mapper", ], ) -def test_from_parquet(data_fixture, request): +def test_parquet_construction(data_fixture, request): """ Test that ensemble loader functions successfully load parquet files """ @@ -67,14 +74,21 @@ def test_from_parquet(data_fixture, request): # Check to make sure the critical quantity labels are bound to real columns assert parquet_ensemble._source[col] is not None + @pytest.mark.parametrize( "data_fixture", [ "dask_dataframe_ensemble", + "dask_dataframe_with_object_ensemble", "pandas_ensemble", + "pandas_with_object_ensemble", + "read_dask_dataframe_ensemble", + "read_dask_dataframe_with_object_ensemble", + "read_pandas_ensemble", + "read_pandas_with_object_ensemble", ], ) -def test_from_dataframe(data_fixture, request): +def test_dataframe_constructors(data_fixture, request): """ Tests constructing an ensemble from pandas and dask dataframes. """ @@ -102,6 +116,9 @@ def test_from_dataframe(data_fixture, request): # Check to make sure the critical quantity labels are bound to real columns assert ens._source[col] is not None + # Check that we can compute an analysis function on the ensemble. + amplitude = ens.batch(calc_stetson_J) + assert len(amplitude) == 5 @pytest.mark.parametrize( "data_fixture", @@ -159,6 +176,7 @@ def test_update_ensemble(data_fixture, request): result_frame.ensemble = None assert result_frame.update_ensemble() is None + def test_available_datasets(dask_client): """ Test that the ensemble is able to successfully read in the list of available TAPE datasets @@ -270,7 +288,7 @@ def test_from_rrl_dataset(dask_client): ens = Ensemble(client=dask_client) ens.from_dataset("s82_rrlyrae") - # larger dataset, let's just use a subset of ~100 + # larger dataset, let's just use a subset ens.prune(350) res = ens.batch(calc_stetson_J) @@ -293,7 +311,51 @@ def test_from_qso_dataset(dask_client): ens = Ensemble(client=dask_client) ens.from_dataset("s82_qso") - # larger dataset, let's just use a subset of ~100 + # larger dataset, let's just use a subset + ens.prune(650) + + res = ens.batch(calc_stetson_J) + + assert 1257836 in res # find a specific object + + # Check Stetson J results for a specific object + assert res.loc[1257836]["g"] == pytest.approx(411.19885, rel=0.001) + assert res.loc[1257836]["i"] == pytest.approx(86.371310, rel=0.001) + assert res.loc[1257836]["r"] == pytest.approx(133.56796, rel=0.001) + assert res.loc[1257836]["u"] == pytest.approx(231.93229, rel=0.001) + assert res.loc[1257836]["z"] == pytest.approx(53.013018, rel=0.001) + + +def test_read_rrl_dataset(dask_client): + """ + Test a basic load and analyze workflow from the S82 RR Lyrae Dataset + """ + + ens = tape.read_dataset("s82_rrlyrae", dask_client=dask_client) + + # larger dataset, let's just use a subset + ens.prune(350) + + res = ens.batch(calc_stetson_J) + + assert 377927 in res.index # find a specific object + + # Check Stetson J results for a specific object + assert res[377927]["g"] == pytest.approx(9.676014, rel=0.001) + assert res[377927]["i"] == pytest.approx(14.22723, rel=0.001) + assert res[377927]["r"] == pytest.approx(6.958200, rel=0.001) + assert res[377927]["u"] == pytest.approx(9.499280, rel=0.001) + assert res[377927]["z"] == pytest.approx(14.03794, rel=0.001) + + +def test_read_qso_dataset(dask_client): + """ + Test a basic load and analyze workflow from the S82 QSO Dataset + """ + + ens = tape.read_dataset("s82_qso", dask_client=dask_client) + + # larger dataset, let's just use a subset ens.prune(650) res = ens.batch(calc_stetson_J) @@ -343,9 +405,49 @@ def test_from_source_dict(dask_client): assert src_table.iloc[i][ens._err_col] == rows[ens._err_col][i] # Check that the derived object table is correct. - assert obj_table.shape[0] == 2 - assert obj_table.iloc[0][ens._nobs_tot_col] == 4 - assert obj_table.iloc[1][ens._nobs_tot_col] == 5 + assert 8001 in obj_table.index + assert 8002 in obj_table.index + + +def test_read_source_dict(dask_client): + """ + Test that tape.read_source_dict() successfully creates data from a dictionary. + """ + ens = Ensemble(client=dask_client) + + # Create some fake data with two IDs (8001, 8002), two bands ["g", "b"] + # and a few time steps. Leave out the flux data initially. + rows = { + "id": [8001, 8001, 8001, 8001, 8002, 8002, 8002, 8002, 8002], + "time": [10.1, 10.2, 10.2, 11.1, 11.2, 11.3, 11.4, 15.0, 15.1], + "band": ["g", "g", "b", "g", "b", "g", "g", "g", "g"], + "err": [1.0, 2.0, 1.0, 3.0, 2.0, 3.0, 4.0, 5.0, 6.0], + } + + # We get an error without all of the required rows. + with pytest.raises(ValueError): + tape.read_source_dict(rows) + + # Add the last row and build the ensemble. + rows["flux"] = [1.0, 2.0, 5.0, 3.0, 1.0, 2.0, 3.0, 4.0, 5.0] + + cmap = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="err", band_col="band") + + ens = tape.read_source_dict(rows, column_mapper=cmap, dask_client=dask_client) + + (obj_table, src_table) = ens.compute() + + # Check that the loaded source table is correct. + assert src_table.shape[0] == 9 + for i in range(9): + assert src_table.iloc[i][ens._flux_col] == rows[ens._flux_col][i] + assert src_table.iloc[i][ens._time_col] == rows[ens._time_col][i] + assert src_table.iloc[i][ens._band_col] == rows[ens._band_col][i] + assert src_table.iloc[i][ens._err_col] == rows[ens._err_col][i] + + # Check that the derived object table is correct. + assert 8001 in obj_table.index + assert 8002 in obj_table.index def test_insert(parquet_ensemble): @@ -570,14 +672,16 @@ def test_sync_tables(parquet_ensemble): lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float) ) parquet_ensemble.dropna(table="source") - assert len(parquet_ensemble._source.compute()) == 1999 # We dropped one source row due to a NaN assert parquet_ensemble._source.is_dirty() # Dropna should set the source dirty flag + # Drop a whole object to test that the object is dropped in the object table + parquet_ensemble.query(f"{parquet_ensemble._id_col} != 88472935274829959", table="source") + parquet_ensemble._sync_tables() # both tables should have the expected number of rows after a sync - assert len(parquet_ensemble.compute("object")) == 5 - assert len(parquet_ensemble.compute("source")) == 1562 + assert len(parquet_ensemble.compute("object")) == 4 + assert len(parquet_ensemble.compute("source")) == 1063 # dirty flags should be unset after sync assert not parquet_ensemble._object.is_dirty() @@ -630,6 +734,127 @@ def test_lazy_sync_tables(parquet_ensemble): assert not parquet_ensemble._source.is_dirty() +def test_temporary_cols(parquet_ensemble): + """ + Test that temporary columns are tracked and dropped as expected. + """ + + ens = parquet_ensemble + ens.update_frame(ens._object.drop(columns=["nobs_r", "nobs_g", "nobs_total"])) + + # Make sure temp lists are available but empty + assert not len(ens._source_temp) + assert not len(ens._object_temp) + + ens.calc_nobs(temporary=True) # Generates "nobs_total" + + # nobs_total should be a temporary column + assert "nobs_total" in ens._object_temp + assert "nobs_total" in ens._object.columns + + ens.assign(nobs2=lambda x: x["nobs_total"] * 2, table="object", temporary=True) + + # nobs2 should be a temporary column + assert "nobs2" in ens._object_temp + assert "nobs2" in ens._object.columns + + # drop NaNs from source, source should be dirty now + ens.dropna(how="any", table="source") + + assert ens._source.is_dirty() + + # try a sync + ens._sync_tables() + + # nobs_total should be removed from object + assert "nobs_total" not in ens._object_temp + assert "nobs_total" not in ens._object.columns + + # nobs2 should be removed from object + assert "nobs2" not in ens._object_temp + assert "nobs2" not in ens._object.columns + + # add a source column that we manually set as dirty, don't have a function + # that adds temporary source columns at the moment + ens.assign(f2=lambda x: x[ens._flux_col] ** 2, table="source", temporary=True) + + # prune object, object should be dirty + ens.prune(threshold=10) + + assert ens._object_dirty + + # try a sync + ens._sync_tables() + + # f2 should be removed from source + assert "f2" not in ens._source_temp + assert "f2" not in ens._source.columns + + +def test_temporary_cols(parquet_ensemble): + """ + Test that temporary columns are tracked and dropped as expected. + """ + + ens = parquet_ensemble + ens._object = ens._object.drop(columns=["nobs_r", "nobs_g", "nobs_total"]) + + # Make sure temp lists are available but empty + assert not len(ens._source_temp) + assert not len(ens._object_temp) + + ens.calc_nobs(temporary=True) # Generates "nobs_total" + + # nobs_total should be a temporary column + assert "nobs_total" in ens._object_temp + assert "nobs_total" in ens._object.columns + + ens.assign(nobs2=lambda x: x["nobs_total"] * 2, table="object", temporary=True) + + # nobs2 should be a temporary column + assert "nobs2" in ens._object_temp + assert "nobs2" in ens._object.columns + + # Replace the maximum flux value with a NaN so that we will have a row to drop. + max_flux = max(parquet_ensemble._source[parquet_ensemble._flux_col]) + parquet_ensemble._source[parquet_ensemble._flux_col] = parquet_ensemble._source[ + parquet_ensemble._flux_col].apply( + lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float) + ) + + # drop NaNs from source, source should be dirty now + ens.dropna(how="any", table="source") + + assert ens._source.is_dirty() + + # try a sync + ens._sync_tables() + + # nobs_total should be removed from object + assert "nobs_total" not in ens._object_temp + assert "nobs_total" not in ens._object.columns + + # nobs2 should be removed from object + assert "nobs2" not in ens._object_temp + assert "nobs2" not in ens._object.columns + + # add a source column that we manually set as dirty, don't have a function + # that adds temporary source columns at the moment + ens.assign(f2=lambda x: x[ens._flux_col] ** 2, table="source", temporary=True) + + # prune object, object should be dirty + ens.prune(threshold=10) + + assert ens._object.is_dirty() + + # try a sync + ens._sync_tables() + + # f2 should be removed from source + assert "f2" not in ens._source_temp + assert "f2" not in ens._source.columns + + def test_dropna(parquet_ensemble): # Try passing in an unrecognized 'table' parameter and verify an exception is thrown with pytest.raises(ValueError): @@ -716,18 +941,33 @@ def test_keep_zeros(parquet_ensemble): parquet_ensemble.dropna(table="source") parquet_ensemble._sync_tables() + # Check that objects are preserved after sync new_objects_pdf = parquet_ensemble._object.compute() assert len(new_objects_pdf.index) == len(old_objects_pdf.index) assert parquet_ensemble._object.npartitions == prev_npartitions - # Check that all counts have stayed the same except the filtered index, - # which should now be all zeros. - for i in old_objects_pdf.index.values: - for c in new_objects_pdf.columns.values: - if i == valid_id: - assert new_objects_pdf.loc[i, c] == 0 - else: - assert new_objects_pdf.loc[i, c] == old_objects_pdf.loc[i, c] + +@pytest.mark.parametrize("by_band", [True, False]) +@pytest.mark.parametrize("know_divisions", [True, False]) +def test_calc_nobs(parquet_ensemble, by_band, know_divisions): + ens = parquet_ensemble + ens._object = ens._object.drop(["nobs_g", "nobs_r", "nobs_total"], axis=1) + + if know_divisions: + ens._object = ens._object.reset_index().set_index(ens._id_col) + assert ens._object.known_divisions + + ens.calc_nobs(by_band) + + lc = ens._object.loc[88472935274829959].compute() + + if by_band: + assert np.all([col in ens._object.columns for col in ["nobs_g", "nobs_r"]]) + assert lc["nobs_g"].values[0] == 98 + assert lc["nobs_r"].values[0] == 401 + + assert "nobs_total" in ens._object.columns + assert lc["nobs_total"].values[0] == 499 def test_prune(parquet_ensemble): @@ -905,6 +1145,55 @@ def test_coalesce(dask_client, drop_inputs): for col in ["flux1", "flux2", "flux3"]: assert col in ens._source.columns + +@pytest.mark.parametrize("zero_point", [("zp_mag", "zp_flux"), (25.0, 10**10)]) +@pytest.mark.parametrize("zp_form", ["flux", "mag", "magnitude", "lincc"]) +@pytest.mark.parametrize("out_col_name", [None, "mag"]) +def test_convert_flux_to_mag(dask_client, zero_point, zp_form, out_col_name): + ens = Ensemble(client=dask_client) + + source_dict = { + "id": [0, 0, 0, 0, 0], + "time": [1, 2, 3, 4, 5], + "flux": [30.5, 70, 80.6, 30.2, 60.3], + "zp_mag": [25.0, 25.0, 25.0, 25.0, 25.0], + "zp_flux": [10**10, 10**10, 10**10, 10**10, 10**10], + "error": [10, 10, 10, 10, 10], + "band": ["g", "g", "g", "g", "g"], + } + + if out_col_name is None: + output_column = "flux_mag" + else: + output_column = out_col_name + + # map flux_col to one of the flux columns at the start + col_map = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="error", band_col="band") + ens.from_source_dict(source_dict, column_mapper=col_map) + + if zp_form == "flux": + ens.convert_flux_to_mag(zero_point[1], zp_form, out_col_name) + + res_mag = ens._source.compute()[output_column].to_list()[0] + assert pytest.approx(res_mag, 0.001) == 21.28925 + + res_err = ens._source.compute()[output_column + "_err"].to_list()[0] + assert pytest.approx(res_err, 0.001) == 0.355979 + + elif zp_form == "mag" or zp_form == "magnitude": + ens.convert_flux_to_mag(zero_point[0], zp_form, out_col_name) + + res_mag = ens._source.compute()[output_column].to_list()[0] + assert pytest.approx(res_mag, 0.001) == 21.28925 + + res_err = ens._source.compute()[output_column + "_err"].to_list()[0] + assert pytest.approx(res_err, 0.001) == 0.355979 + + else: + with pytest.raises(ValueError): + ens.convert_flux_to_mag(zero_point[0], zp_form, "mag") + + def test_find_day_gap_offset(dask_client): ens = Ensemble(client=dask_client) diff --git a/tests/tape_tests/test_utils.py b/tests/tape_tests/test_utils.py index 9b882fde..0a75aff8 100644 --- a/tests/tape_tests/test_utils.py +++ b/tests/tape_tests/test_utils.py @@ -23,9 +23,7 @@ def test_column_mapper(): assert col_map.is_ready() # col_map should now be ready # Assign the remaining columns - col_map.assign( - provenance_col="provenance", nobs_total_col="nobs_total", nobs_band_cols=["nobs_g", "nobs_r"] - ) + col_map.assign(provenance_col="provenance") expected_map = { "id_col": "id", @@ -34,8 +32,6 @@ def test_column_mapper(): "err_col": "err", "band_col": "band", "provenance_col": "provenance", - "nobs_total_col": "nobs_total", - "nobs_band_cols": ["nobs_g", "nobs_r"], } assert col_map.map == expected_map # The expected mapping @@ -53,8 +49,6 @@ def test_column_mapper_init(): err_col="err", band_col="band", provenance_col="provenance", - nobs_total_col="nobs_total", - nobs_band_cols=["nobs_g", "nobs_r"], ) assert col_map.is_ready() # col_map should be ready @@ -66,8 +60,6 @@ def test_column_mapper_init(): "err_col": "err", "band_col": "band", "provenance_col": "provenance", - "nobs_total_col": "nobs_total", - "nobs_band_cols": ["nobs_g", "nobs_r"], } assert col_map.map == expected_map # The expected mapping From 0d4da10d863756bf3cfe9ca8f86cf84e8095d89f Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Fri, 27 Oct 2023 09:04:48 -0700 Subject: [PATCH 19/35] Fix EnsembleFrame.set_dirty and map_partitions metadata propagation (#280) * FIx _Frame.set_dirty * Update propgating data in map_partitions * Fix typo --- src/tape/ensemble_frame.py | 94 +++++++++++++++++++++++-- tests/tape_tests/test_ensemble_frame.py | 22 +++++- 2 files changed, 108 insertions(+), 8 deletions(-) diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index 34e2b2e8..a50a415b 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -81,18 +81,20 @@ def _creates_meta(cls, meta, schema): class _Frame(dd.core._Frame): """Base class for extensions of Dask Dataframes that track additional Ensemble-related metadata.""" - _is_dirty = False # True if the underlying data is out of sync with the Ensemble - def __init__(self, dsk, name, meta, divisions, label=None, ensemble=None): - super().__init__(dsk, name, meta, divisions) + # We define relevant object fields before super().__init__ since that call may lead to a + # map_partitions call which will assume these fields exist. self.label = label # A label used by the Ensemble to identify this frame. self.ensemble = ensemble # The Ensemble object containing this frame. + self.dirty = False # True if the underlying data is out of sync with the Ensemble + + super().__init__(dsk, name, meta, divisions) def is_dirty(self): - return self._is_dirty + return self.dirty - def set_dirty(self, is_dirty): - self._is_dirty = is_dirty + def set_dirty(self, dirty): + self.dirty = dirty @property def _args(self): @@ -115,7 +117,7 @@ def _propagate_metadata(self, new_frame): """ new_frame.label = self.label new_frame.ensemble = self.ensemble - new_frame.set_dirty(self.is_dirty) + new_frame.set_dirty(self.is_dirty()) return new_frame def copy(self): @@ -442,6 +444,84 @@ def set_index( """ result = super().set_index(other, drop, sorted, npartitions, divisions, inplace, sort, **kwargs) return self._propagate_metadata(result) + + def map_partitions(self, func, *args, **kwargs): + """Apply Python function on each DataFrame partition. + + Doc string below derived from dask.dataframe.core + + If ``sort=False``, this function operates exactly like ``pandas.set_index`` + and sets the index on the DataFrame. If ``sort=True`` (default), + this function also sorts the DataFrame by the new index. This can have a + significant impact on performance, because joins, groupbys, lookups, etc. + are all much faster on that column. However, this performance increase + comes with a cost, sorting a parallel dataset requires expensive shuffles. + Often we ``set_index`` once directly after data ingest and filtering and + then perform many cheap computations off of the sorted dataset. + + With ``sort=True``, this function is much more expensive. Under normal + operation this function does an initial pass over the index column to + compute approximate quantiles to serve as future divisions. It then passes + over the data a second time, splitting up each input partition into several + pieces and sharing those pieces to all of the output partitions now in + sorted order. + + In some cases we can alleviate those costs, for example if your dataset is + sorted already then we can avoid making many small pieces or if you know + good values to split the new index column then we can avoid the initial + pass over the data. For example if your new index is a datetime index and + your data is already sorted by day then this entire operation can be done + for free. You can control these options with the following parameters. + + Parameters + ---------- + other: string or Dask Series + Column to use as index. + drop: boolean, default True + Delete column to be used as the new index. + sorted: bool, optional + If the index column is already sorted in increasing order. + Defaults to False + npartitions: int, None, or 'auto' + The ideal number of output partitions. If None, use the same as + the input. If 'auto' then decide by memory use. + Only used when ``divisions`` is not given. If ``divisions`` is given, + the number of output partitions will be ``len(divisions) - 1``. + divisions: list, optional + The "dividing lines" used to split the new index into partitions. + For ``divisions=[0, 10, 50, 100]``, there would be three output partitions, + where the new index contained [0, 10), [10, 50), and [50, 100), respectively. + See https://docs.dask.org/en/latest/dataframe-design.html#partitions. + If not given (default), good divisions are calculated by immediately computing + the data and looking at the distribution of its values. For large datasets, + this can be expensive. + Note that if ``sorted=True``, specified divisions are assumed to match + the existing partitions in the data; if this is untrue you should + leave divisions empty and call ``repartition`` after ``set_index``. + inplace: bool, optional + Modifying the DataFrame in place is not supported by Dask. + Defaults to False. + sort: bool, optional + If ``True``, sort the DataFrame by the new index. Otherwise + set the index on the individual existing partitions. + Defaults to ``True``. + shuffle: {'disk', 'tasks', 'p2p'}, optional + Either ``'disk'`` for single-node operation or ``'tasks'`` and + ``'p2p'`` for distributed operation. Will be inferred by your + current scheduler. + compute: bool, default False + Whether or not to trigger an immediate computation. Defaults to False. + Note, that even if you set ``compute=False``, an immediate computation + will still be triggered if ``divisions`` is ``None``. + partition_size: int, optional + Desired size of each partitions in bytes. + Only used when ``npartitions='auto'`` + """ + result = super().map_partitions(func, *args, **kwargs) + if isinstance(result, self.__class__): + # If the output of func is another _Frame, let's propagate any metadata. + return self._propagate_metadata(result) + return result class TapeSeries(pd.Series): """A barebones extension of a Pandas series to be used for underlying Ensemble data. diff --git a/tests/tape_tests/test_ensemble_frame.py b/tests/tape_tests/test_ensemble_frame.py index fcb138f3..fdf0f527 100644 --- a/tests/tape_tests/test_ensemble_frame.py +++ b/tests/tape_tests/test_ensemble_frame.py @@ -87,6 +87,14 @@ def test_ensemble_frame_propagation(data_fixture, request): assert copied_frame.ensemble == ens assert copied_frame.is_dirty() + # Verify that the above is also true by calling copy via map_partitions + mapped_frame = ens_frame.copy().map_partitions(lambda x: x.copy()) + assert isinstance(mapped_frame, EnsembleFrame) + assert isinstance(mapped_frame._meta, TapeFrame) + assert mapped_frame.label == TEST_LABEL + assert mapped_frame.ensemble == ens + assert mapped_frame.is_dirty() + # Test that a filtered EnsembleFrame is still an EnsembleFrame. filtered_frame = ens_frame[["id", "time"]] assert isinstance(filtered_frame, EnsembleFrame) @@ -220,6 +228,7 @@ def test_object_and_source_frame_propagation(data_fixture, request): # proper SourceFrame with appropriate metadata propagated. source_frame["psFlux"].mean().compute() result_source_frame = source_frame.copy()[["psFlux", "psFluxErr"]] + result_source_frame = result_source_frame.map_partitions(lambda x: x.copy()) assert isinstance(result_source_frame, SourceFrame) assert isinstance(result_source_frame._meta, TapeSourceFrame) assert len(result_source_frame) > 0 @@ -228,10 +237,14 @@ def test_object_and_source_frame_propagation(data_fixture, request): assert result_source_frame.ensemble is ens assert result_source_frame.is_dirty() + # Mark the frame clean to verify that we propagate that state as well + result_source_frame.set_dirty(False) + # Set an index and then group by that index. result_source_frame = result_source_frame.set_index("psFlux", drop=True) assert result_source_frame.label == SOURCE_LABEL assert result_source_frame.ensemble == ens + assert not result_source_frame.is_dirty() # frame is still clean. group_result = result_source_frame.groupby(["psFlux"]).count() assert len(group_result) > 0 assert isinstance(group_result, SourceFrame) @@ -250,20 +263,27 @@ def test_object_and_source_frame_propagation(data_fixture, request): assert not object_frame.is_dirty() object_frame.set_dirty(True) + # Verify that the source frame stays clean when object frame is marked dirty. + assert not result_source_frame.is_dirty() # Perform a series of operations on the ObjectFrame and then verify the result is still a # proper ObjectFrame with appropriate metadata propagated. result_object_frame = object_frame.copy()[["nobs_g", "nobs_total"]] + result_object_frame = result_object_frame.map_partitions(lambda x: x.copy()) assert isinstance(result_object_frame, ObjectFrame) assert isinstance(result_object_frame._meta, TapeObjectFrame) assert result_object_frame.label == OBJECT_LABEL assert result_object_frame.ensemble is ens assert result_object_frame.is_dirty() + # Mark the frame clean to verify that we propagate that state as well + result_object_frame.set_dirty(False) + # Set an index and then group by that index. result_object_frame = result_object_frame.set_index("nobs_g", drop=True) assert result_object_frame.label == OBJECT_LABEL - assert result_object_frame.ensemble == ens + assert result_object_frame.ensemble == ens + assert not result_object_frame.is_dirty() # frame is still clean group_result = result_object_frame.groupby(["nobs_g"]).count() assert len(group_result) > 0 assert isinstance(group_result, ObjectFrame) From c86d7ab6bfe1135c8d087bb892811037386c29d4 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Wed, 1 Nov 2023 12:38:53 -0700 Subject: [PATCH 20/35] Ensemble.update_frame no longer infers if a frame is dirty by checking if row count changed (#281) * Mark frames dirty without len() call * Move calls to set_dirty to EnsembleFrame --- src/tape/ensemble.py | 11 ++------ src/tape/ensemble_frame.py | 46 +++++++++++++++++++++++++++---- tests/tape_tests/test_ensemble.py | 1 + 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index 6befc6f8..f0ae995b 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -150,10 +150,6 @@ def update_frame(self, frame): self._object = frame self.object = frame - # Set a frame as dirty if it was previously tracked and the number of rows has changed. - if frame.label in self.frames and len(self.frames[frame.label]) != len(frame): - frame.set_dirty(True) - # Ensure this frame is assigned to this Ensemble. frame.ensemble = self self.frames[frame.label] = frame @@ -325,6 +321,7 @@ def insert_sources( # Append the new rows to the correct divisions. self.update_frame(dd.concat([self._source, df2], axis=0, interleave_partitions=True)) + self._source.set_dirty(True) # Do the repartitioning if requested. If the divisions were set, reuse them. # Otherwise, use the same number of partitions. @@ -482,11 +479,9 @@ def select(self, columns, table="object"): if table == "object": cols_to_drop = [col for col in self._object.columns if col not in columns] self.update_frame(self._object.drop(cols_to_drop, axis=1)) - self._object.set_dirty(True) elif table == "source": cols_to_drop = [col for col in self._source.columns if col not in columns] self.update_frame(self._source.drop(cols_to_drop, axis=1)) - self._source.set_dirty(True) else: raise ValueError(f"{table} is not one of 'object' or 'source'") @@ -578,7 +573,6 @@ def assign(self, table="object", temporary=False, **kwargs): if table == "object": pre_cols = self._object.columns self.update_frame(self._object.assign(**kwargs)) - self._object.set_dirty(True) post_cols = self._object.columns if temporary: @@ -587,7 +581,6 @@ def assign(self, table="object", temporary=False, **kwargs): elif table == "source": pre_cols = self._source.columns self.update_frame(self._source.assign(**kwargs)) - self._source.set_dirty(True) post_cols = self._source.columns if temporary: @@ -785,6 +778,7 @@ def prune(self, threshold=50, col_name=None): mask = self._object[col_name] >= threshold self.update_frame(self._object[mask]) + self._object.set_dirty(True) # Object table is now dirty return self @@ -929,6 +923,7 @@ def bin_sources( self.update_frame(self._source.reset_index().set_index(self._id_col).drop(tmp_time_col, axis=1)) # Mark the source table as dirty. + self._source.set_dirty(True) return self def batch(self, func, *args, meta=None, use_map=True, compute=True, on=None, **kwargs): diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index a50a415b..3eb01b99 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -147,8 +147,9 @@ def assign(self, **kwargs): result: `tape._Frame` The modifed frame """ - result = super().assign(**kwargs) - return self._propagate_metadata(result) + result = self._propagate_metadata(super().assign(**kwargs)) + result.set_dirty(True) + return result def query(self, expr, **kwargs): """Filter dataframe with complex expression @@ -186,8 +187,9 @@ def query(self, expr, **kwargs): import numexpr numexpr.set_num_threads(1) """ - result = super().query(expr, **kwargs) - return self._propagate_metadata(result) + result = self._propagate_metadata(super().query(expr, **kwargs)) + result.set_dirty(True) + return result def merge(self, right, **kwargs): """Merge the Dataframe with another DataFrame @@ -317,9 +319,41 @@ def drop(self, labels=None, axis=0, columns=None, errors="raise"): Returns the frame or Nonewith the specified index or column labels removed or None if inplace=True. """ - result = super().drop(labels=labels, axis=axis, columns=columns, errors=errors) - return self._propagate_metadata(result) + result = self._propagate_metadata(super().drop(labels=labels, axis=axis, columns=columns, errors=errors)) + result.set_dirty(True) + return result + def dropna(self, **kwargs): + """ + Remove missing values. + + Doc string below derived from dask.dataframe.core + + Parameters + ---------- + + how : {'any', 'all'}, default 'any' + Determine if row or column is removed from DataFrame, when we have + at least one NA or all NA. + + * 'any' : If any NA values are present, drop that row or column. + * 'all' : If all values are NA, drop that row or column. + + thresh : int, optional + Require that many non-NA values. Cannot be combined with how. + subset : column label or sequence of labels, optional + Labels along other axis to consider, e.g. if you are dropping rows + these would be a list of columns to include. + + Returns + ---------- + result: `tape._Frame` + The modifed frame with NA entries dropped from it or None if ``inplace=True``. + """ + result = self._propagate_metadata(super().dropna(**kwargs)) + result.set_dirty(True) + return result + def persist(self, **kwargs): """Persist this dask collection into memory diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index 3d3bbf80..8da6b98d 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -136,6 +136,7 @@ def test_update_ensemble(data_fixture, request): # Filter the object table and have the ensemble track the updated table. updated_obj = ens._object.query("nobs_total > 50") assert updated_obj is not ens._object + assert updated_obj.is_dirty() # Update the ensemble and validate that it marks the object table dirty assert ens._object.is_dirty() == False updated_obj.update_ensemble() From 0ce6f2e487b0df717106fedbb2d06a94a7b03a60 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Tue, 7 Nov 2023 10:25:54 -0800 Subject: [PATCH 21/35] Support storing batch results for custom meta (#285) * Add meta handling for batch * Add unit tests for custom meta * Remove unit test sanity check, fix warning output * Provide default labels for result frames. --- src/tape/ensemble.py | 71 ++++++++++++++++++++- src/tape/ensemble_frame.py | 5 +- tests/tape_tests/test_ensemble.py | 100 +++++++++++++++++++++++++++++- 3 files changed, 168 insertions(+), 8 deletions(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index 9e5d9cb7..54cd148e 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -13,7 +13,7 @@ from .analysis.feature_extractor import BaseLightCurveFeature, FeatureExtractor from .analysis.structure_function import SF_METHODS from .analysis.structurefunction2 import calc_sf2 -from .ensemble_frame import ObjectFrame, SourceFrame, TapeObjectFrame, TapeSourceFrame +from .ensemble_frame import EnsembleFrame, EnsembleSeries, ObjectFrame, SourceFrame, TapeFrame, TapeSeries from .timeseries import TimeSeries from .utils import ColumnMapper @@ -21,6 +21,8 @@ SOURCE_FRAME_LABEL = "source" OBJECT_FRAME_LABEL = "object" +DEFAULT_FRAME_LABEL = "result" # A base default label for an Ensemble's result frames. + class Ensemble: """Ensemble object is a collection of light curve ids""" @@ -43,6 +45,9 @@ def __init__(self, client=True, **kwargs): self.frames = {} # Frames managed by this Ensemble, keyed by label + # A unique ID to allocate new result frame labels. + self.default_frame_id = 1 + # TODO(wbeebe@uw.edu) Replace self._source and self._object with these self.source = None # Source Table EnsembleFrame self.object = None # Object Table EnsembleFrame @@ -208,7 +213,7 @@ def select_frame(self, label): f"Unable to select frame: no frame with label" f"'{label}'" f" is in the Ensemble." ) return self.frames[label] - + def frame_info(self, labels=None, verbose=True, memory_usage=True, **kwargs): """Wrapper for calling dask.dataframe.DataFrame.info() on frames tracked by the Ensemble. @@ -243,6 +248,18 @@ def frame_info(self, labels=None, verbose=True, memory_usage=True, **kwargs): print(label, "Frame") print(self.frames[label].info(verbose=verbose, memory_usage=memory_usage, **kwargs)) + def _generate_frame_label(self): + """ Generates a new unique label for a result frame. """ + result = DEFAULT_FRAME_LABEL + "_" + str(self.default_frame_id) + self.default_frame_id += 1 # increment to guarantee uniqueness + while result in self.frames: + # If the generated label has been taken by a user, increment again. + # In most workflows, we expect the number of frames to be O(100) so it's unlikely for + # the performance cost of this method to be high. + result = DEFAULT_FRAME_LABEL + "_" + str(self.default_frame_id) + self.default_frame_id += 1 + return result + def insert_sources( self, obj_ids, @@ -983,7 +1000,7 @@ def bin_sources( self._source.set_dirty(True) return self - def batch(self, func, *args, meta=None, use_map=True, compute=True, on=None, **kwargs): + def batch(self, func, *args, meta=None, use_map=True, compute=True, on=None, label="", **kwargs): """Run a function from tape.TimeSeries on the available ids Parameters @@ -1021,6 +1038,11 @@ def batch(self, func, *args, meta=None, use_map=True, compute=True, on=None, **k Designates which column(s) to groupby. Columns may be from the source or object tables. For TAPE and `light-curve` functions this is populated automatically. + label: 'str', optional + If provided the ensemble will use this label to track the result + dataframe. If not provided, a label of the from "result_{x}" where x + is a monotonically increasing integer is generated. If `None`, + the result frame will not be tracked. **kwargs: Additional optional parameters passed for the selected function @@ -1071,6 +1093,10 @@ def s2n_inter_quartile_range(flux, err): if meta is None: meta = (self._id_col, float) # return a series of ids, default assume a float is returned + # Translate the meta into an appropriate TapeFrame or TapeSeries. This ensures that the + # batch result will be an EnsembleFrame or EnsembleSeries. + meta = self._translate_meta(meta) + if on is None: on = self._id_col # Default grouping is by id_col if isinstance(on, str): @@ -1108,6 +1134,12 @@ def s2n_inter_quartile_range(flux, err): meta=meta, ) + if label is not None: + if label == "": + label = self._generate_frame_label() + print(f"Using generated label, {label}, for a batch result.") + # Track the result frame under the provided label + self.add_frame(batch, label) if compute: return batch.compute() else: @@ -1830,3 +1862,36 @@ def sf2(self, sf_method="basic", argument_container=None, use_map=True): result = self.batch(calc_sf2, use_map=use_map, argument_container=argument_container) return result + + def _translate_meta(self, meta): + """Translates Dask-style meta into a TapeFrame or TapeSeries object. + + Parameters + ---------- + meta : `dict`, `tuple`, `list`, `pd.Series`, `pd.DataFrame`, `pd.Index`, `dtype`, `scalar` + + Returns + ---------- + result : `ensemble.TapeFrame` or `ensemble.TapeSeries` + The appropriate meta for Dask producing an `Ensemble.EnsembleFrame` or + `Ensemble.EnsembleSeries` respectively + """ + if isinstance(meta, TapeFrame) or isinstance(meta, TapeSeries): + return meta + + # If the meta is not a DataFrame or Series, have Dask attempt translate the meta into an + # appropriate Pandas object. + meta_object = meta + if not (isinstance(meta_object, pd.DataFrame) or isinstance(meta_object, pd.Series)): + meta_object = dd.backends.make_meta_object(meta_object) + + # Convert meta_object into the appropriate TAPE extension. + if isinstance(meta_object, pd.DataFrame): + return TapeFrame(meta_object) + elif isinstance(meta_object, pd.Series): + return TapeSeries(meta_object) + else: + raise ValueError( + "Unsupported Meta: " + str(meta) + "\nTry a Pandas DataFrame or Series instead." + ) + \ No newline at end of file diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index 3eb01b99..cda7c2b8 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -4,7 +4,7 @@ import dask from dask.dataframe.dispatch import make_meta_dispatch -from dask.dataframe.backends import _nonempty_index, meta_nonempty, meta_nonempty_dataframe +from dask.dataframe.backends import _nonempty_index, meta_nonempty, meta_nonempty_dataframe, _nonempty_series from dask.dataframe.core import get_parallel_type from dask.dataframe.extensions import make_array_nonempty @@ -978,7 +978,8 @@ def make_meta_frame(x, index=None): @meta_nonempty.register(TapeSeries) def _nonempty_tapeseries(x, index=None): # Construct a new TapeSeries with the same underlying data. - return TapeSeries(data, name=x.name, crs=x.crs) + data = _nonempty_series(x) + return TapeSeries(data) @meta_nonempty.register(TapeFrame) def _nonempty_tapeseries(x, index=None): diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index 9f852f6a..11aaefeb 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -7,7 +7,7 @@ import pytest import tape -from tape import Ensemble, EnsembleFrame, ObjectFrame, SourceFrame, TapeFrame, TapeObjectFrame, TapeSourceFrame +from tape import Ensemble, EnsembleFrame, EnsembleSeries, ObjectFrame, SourceFrame, TapeFrame, TapeSeries, TapeObjectFrame, TapeSourceFrame from tape.analysis.stetsonj import calc_stetson_J from tape.analysis.structure_function.base_argument_container import StructureFunctionArgumentContainer from tape.analysis.structurefunction2 import calc_sf2 @@ -1398,15 +1398,29 @@ def test_batch(data_fixture, request, use_map, on): """ Test that ensemble.batch() returns the correct values of the first result """ - parquet_ensemble = request.getfixturevalue(data_fixture) + frame_cnt = len(parquet_ensemble.frames) result = ( parquet_ensemble.prune(10) .dropna(table="source") - .batch(calc_stetson_J, use_map=use_map, on=on, band_to_calc=None) + .batch( + calc_stetson_J, + use_map=use_map, + on=on, + band_to_calc=None, + compute=False, + label="stetson_j") ) + # Validate that the ensemble is now tracking a new result frame. + assert len(parquet_ensemble.frames) == frame_cnt + 1 + tracked_result = parquet_ensemble.select_frame("stetson_j") + assert isinstance(tracked_result, EnsembleSeries) + assert result is tracked_result + + result = result.compute() + if on is None: assert pytest.approx(result.values[0]["g"], 0.001) == -0.04174282 assert pytest.approx(result.values[0]["r"], 0.001) == 0.6075282 @@ -1417,6 +1431,41 @@ def test_batch(data_fixture, request, use_map, on): assert pytest.approx(result.values[1]["g"], 0.001) == 1.2208577 assert pytest.approx(result.values[1]["r"], 0.001) == -0.49639028 +def test_batch_labels(parquet_ensemble): + """ + Test that ensemble.batch() generates unique labels for result frames when none are provided. + """ + # Since no label was provided we generate a label of "result_1" + parquet_ensemble.prune(10).batch(np.mean, parquet_ensemble._flux_col) + assert "result_1" in parquet_ensemble.frames + assert len(parquet_ensemble.select_frame("result_1")) > 0 + + # Now give a user-provided custom label. + parquet_ensemble.batch(np.mean, parquet_ensemble._flux_col, label="flux_mean") + assert "flux_mean" in parquet_ensemble.frames + assert len(parquet_ensemble.select_frame("flux_mean")) > 0 + + # Since this is the second batch call where a label is *not* provided, we generate label "result_2" + parquet_ensemble.batch(np.mean, parquet_ensemble._flux_col) + assert "result_2" in parquet_ensemble.frames + assert len(parquet_ensemble.select_frame("result_2")) > 0 + + # Explicitly provide label "result_3" + parquet_ensemble.batch(np.mean, parquet_ensemble._flux_col, label="result_3") + assert "result_3" in parquet_ensemble.frames + assert len(parquet_ensemble.select_frame("result_3")) > 0 + + # Validate that the next generated label is "result_4" since "result_3" is taken. + parquet_ensemble.batch(np.mean, parquet_ensemble._flux_col) + assert "result_4" in parquet_ensemble.frames + assert len(parquet_ensemble.select_frame("result_4")) > 0 + + frame_cnt = len(parquet_ensemble.frames) + + # Validate that when the label is None, the result frame isn't tracked by the Ensemble.s + result = parquet_ensemble.batch(np.mean, parquet_ensemble._flux_col, label=None) + assert frame_cnt == len(parquet_ensemble.frames) + assert len(result) > 0 def test_batch_with_custom_func(parquet_ensemble): """ @@ -1426,6 +1475,51 @@ def test_batch_with_custom_func(parquet_ensemble): result = parquet_ensemble.prune(10).batch(np.mean, parquet_ensemble._flux_col) assert len(result) > 0 +@pytest.mark.parametrize("custom_meta", [ + ("flux_mean", float), # A tuple representing a series + pd.Series(name="flux_mean_pandas", dtype="float64"), + TapeSeries(name="flux_mean_tape", dtype="float64")]) +def test_batch_with_custom_series_meta(parquet_ensemble, custom_meta): + """ + Test Ensemble.batch with various styles of output meta for a Series-style result. + """ + num_frames = len(parquet_ensemble.frames) + + parquet_ensemble.prune(10).batch( + np.mean, parquet_ensemble._flux_col, meta=custom_meta, label="flux_mean") + + assert len(parquet_ensemble.frames) == num_frames + 1 + assert len(parquet_ensemble.select_frame("flux_mean")) > 0 + assert isinstance(parquet_ensemble.select_frame("flux_mean"), EnsembleSeries) + +@pytest.mark.parametrize("custom_meta", [ + {"lc_id": int, "band": str, "dt": float, "sf2": float, "1_sigma": float}, + [("lc_id", int), ("band", str), ("dt", float), ("sf2", float), ("1_sigma", float)], + pd.DataFrame({ + "lc_id": pd.Series([], dtype=int), + "band": pd.Series([], dtype=str), + "dt": pd.Series([], dtype=float), + "sf2": pd.Series([], dtype=float), + "1_sigma": pd.Series([], dtype=float)}), + TapeFrame({ + "lc_id": pd.Series([], dtype=int), + "band": pd.Series([], dtype=str), + "dt": pd.Series([], dtype=float), + "sf2": pd.Series([], dtype=float), + "1_sigma": pd.Series([], dtype=float)}), +]) +def test_batch_with_custom_frame_meta(parquet_ensemble, custom_meta): + """ + Test Ensemble.batch with various sytles of output meta for a DataFrame-style result. + """ + num_frames = len(parquet_ensemble.frames) + + parquet_ensemble.prune(10).batch( + calc_sf2, parquet_ensemble._flux_col, meta=custom_meta, label="sf2_result") + + assert len(parquet_ensemble.frames) == num_frames + 1 + assert len(parquet_ensemble.select_frame("sf2_result")) > 0 + assert isinstance(parquet_ensemble.select_frame("sf2_result"), EnsembleFrame) def test_to_timeseries(parquet_ensemble): """ From 5ca0cc38b4429cf777e812a131f622ade7e45919 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Mon, 27 Nov 2023 14:07:46 -0800 Subject: [PATCH 22/35] Update Remaining TAPE Documentation Notebooks for the Refactor (#298) * Remove ._source and ._object * Update notebooks for refactor * Fix find-replace error --- .../binning_slowly_changing_sources.ipynb | 16 ++++++++-------- docs/tutorials/scaling_to_large_data.ipynb | 4 ++-- docs/tutorials/structure_function_showcase.ipynb | 4 ++-- docs/tutorials/tape_datasets.ipynb | 8 ++++---- docs/tutorials/using_ray_with_the_ensemble.ipynb | 8 ++++---- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/tutorials/binning_slowly_changing_sources.ipynb b/docs/tutorials/binning_slowly_changing_sources.ipynb index 853e62b8..767b34c8 100644 --- a/docs/tutorials/binning_slowly_changing_sources.ipynb +++ b/docs/tutorials/binning_slowly_changing_sources.ipynb @@ -60,7 +60,7 @@ "outputs": [], "source": [ "fig, ax = plt.subplots(1, 1)\n", - "ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 500)\n", + "ax.hist(ens.source[\"midPointTai\"].compute().tolist(), 500)\n", "ax.set_xlabel(\"Time (MJD)\")\n", "ax.set_ylabel(\"Source Count\")" ] @@ -90,7 +90,7 @@ "source": [ "ens.bin_sources(time_window=7.0, offset=0.0)\n", "fig, ax = plt.subplots(1, 1)\n", - "ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 500)\n", + "ax.hist(ens.source[\"midPointTai\"].compute().tolist(), 500)\n", "ax.set_xlabel(\"Time (MJD)\")\n", "ax.set_ylabel(\"Source Count\")" ] @@ -120,7 +120,7 @@ "source": [ "ens.bin_sources(time_window=28.0, offset=0.0, custom_aggr={\"midPointTai\": \"min\"})\n", "fig, ax = plt.subplots(1, 1)\n", - "ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 500)\n", + "ax.hist(ens.source[\"midPointTai\"].compute().tolist(), 500)\n", "ax.set_xlabel(\"Time (MJD)\")\n", "ax.set_ylabel(\"Source Count\")" ] @@ -150,7 +150,7 @@ "ens.from_source_dict(rows, column_mapper=cmap)\n", "\n", "fig, ax = plt.subplots(1, 1)\n", - "ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 60)\n", + "ax.hist(ens.source[\"midPointTai\"].compute().tolist(), 60)\n", "ax.set_xlabel(\"Time (MJD)\")\n", "ax.set_ylabel(\"Source Count\")" ] @@ -179,7 +179,7 @@ "ens.bin_sources(time_window=1.0, offset=0.0)\n", "\n", "fig, ax = plt.subplots(1, 1)\n", - "ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 60)\n", + "ax.hist(ens.source[\"midPointTai\"].compute().tolist(), 60)\n", "ax.set_xlabel(\"Time (MJD)\")\n", "ax.set_ylabel(\"Source Count\")" ] @@ -209,7 +209,7 @@ "ens.bin_sources(time_window=1.0, offset=0.5)\n", "\n", "fig, ax = plt.subplots(1, 1)\n", - "ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 60)\n", + "ax.hist(ens.source[\"midPointTai\"].compute().tolist(), 60)\n", "ax.set_xlabel(\"Time (MJD)\")\n", "ax.set_ylabel(\"Source Count\")" ] @@ -259,7 +259,7 @@ "ens.bin_sources(time_window=1.0, offset=0.5)\n", "\n", "fig, ax = plt.subplots(1, 1)\n", - "ax.hist(ens._source[\"midPointTai\"].compute().tolist(), 60)\n", + "ax.hist(ens.source[\"midPointTai\"].compute().tolist(), 60)\n", "ax.set_xlabel(\"Time (MJD)\")\n", "ax.set_ylabel(\"Source Count\")" ] @@ -290,7 +290,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.6" + "version": "3.10.13" }, "vscode": { "interpreter": { diff --git a/docs/tutorials/scaling_to_large_data.ipynb b/docs/tutorials/scaling_to_large_data.ipynb index b1238409..9e38f6d2 100644 --- a/docs/tutorials/scaling_to_large_data.ipynb +++ b/docs/tutorials/scaling_to_large_data.ipynb @@ -216,7 +216,7 @@ "\n", "print(\"number of lightcurve results in mapres: \", len(mapres))\n", "print(\"number of lightcurve results in groupres: \", len(groupres))\n", - "print(\"True number of lightcurves in the dataset:\", len(np.unique(ens._source.index)))" + "print(\"True number of lightcurves in the dataset:\", len(np.unique(ens.source.index)))" ] }, { @@ -263,7 +263,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.6" + "version": "3.10.13" }, "vscode": { "interpreter": { diff --git a/docs/tutorials/structure_function_showcase.ipynb b/docs/tutorials/structure_function_showcase.ipynb index 592436fe..f2168f23 100644 --- a/docs/tutorials/structure_function_showcase.ipynb +++ b/docs/tutorials/structure_function_showcase.ipynb @@ -267,7 +267,7 @@ "metadata": {}, "outputs": [], "source": [ - "ens.head(\"object\", 5) \n" + "ens.object.head(5) \n" ] }, { @@ -276,7 +276,7 @@ "metadata": {}, "outputs": [], "source": [ - "ens.head(\"source\", 5) " + "ens.source.head(5) " ] }, { diff --git a/docs/tutorials/tape_datasets.ipynb b/docs/tutorials/tape_datasets.ipynb index 1cd3670f..ddcec2de 100644 --- a/docs/tutorials/tape_datasets.ipynb +++ b/docs/tutorials/tape_datasets.ipynb @@ -52,7 +52,7 @@ " column_mapper=col_map\n", " )\n", "\n", - "ens.head(\"source\") # View the first 5 entries of the source table" + "ens.source.head(5) # View the first 5 entries of the source table" ] }, { @@ -93,7 +93,7 @@ " column_mapper=col_map\n", " )\n", "\n", - "ens.head(\"object\") # View the first 5 entries of the object table" + "ens.object.head(5) # View the first 5 entries of the object table" ] }, { @@ -168,7 +168,7 @@ "source": [ "ens.from_dataset(\"s82_rrlyrae\") # Let's grab the Stripe 82 RR Lyrae\n", "\n", - "ens.head(\"object\", 5)" + "ens.object.head(5)" ] }, { @@ -270,7 +270,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.13" }, "vscode": { "interpreter": { diff --git a/docs/tutorials/using_ray_with_the_ensemble.ipynb b/docs/tutorials/using_ray_with_the_ensemble.ipynb index f0ba09a0..b19ca28f 100644 --- a/docs/tutorials/using_ray_with_the_ensemble.ipynb +++ b/docs/tutorials/using_ray_with_the_ensemble.ipynb @@ -81,7 +81,7 @@ "outputs": [], "source": [ "ens.from_dataset(\"s82_qso\")\n", - "ens._source = ens._source.repartition(npartitions=10)\n", + "ens.source = ens.source.repartition(npartitions=10)\n", "ens.batch(calc_sf2, use_map=False) # use_map is false as we repartition naively, splitting per-object sources across partitions" ] }, @@ -116,7 +116,7 @@ "\n", "ens=Ensemble(client=False) # Do not use a client\n", "ens.from_dataset(\"s82_qso\")\n", - "ens._source = ens._source.repartition(npartitions=10)\n", + "ens.source = ens.source.repartition(npartitions=10)\n", "ens.batch(calc_sf2, use_map=False)" ] }, @@ -150,7 +150,7 @@ "\n", "ens = Ensemble()\n", "ens.from_dataset(\"s82_qso\")\n", - "ens._source = ens._source.repartition(npartitions=10)\n", + "ens.source = ens.source.repartition(npartitions=10)\n", "ens.batch(calc_sf2, use_map=False)" ] } @@ -171,7 +171,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.13" }, "vscode": { "interpreter": { From 1dfa8df1a2e3ae8fcad6e22a6ce5f721f06ddf01 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Mon, 27 Nov 2023 15:49:22 -0800 Subject: [PATCH 23/35] Update Docs for TAPE EnsembleFrame Refactor (#290) * Initial commit for notebooks with refactor API * Removed _object and _source references * Added sync tables example * Address comment * Addressed frame renaming * Update docs/tutorials/working_with_the_ensemble.ipynb Co-authored-by: Konstantin Malanchev * Addressed comments * Clear output --------- Co-authored-by: Konstantin Malanchev --- .../tutorials/working_with_the_ensemble.ipynb | 369 +++++++++++++++--- 1 file changed, 313 insertions(+), 56 deletions(-) diff --git a/docs/tutorials/working_with_the_ensemble.ipynb b/docs/tutorials/working_with_the_ensemble.ipynb index 10110329..2d2eb993 100644 --- a/docs/tutorials/working_with_the_ensemble.ipynb +++ b/docs/tutorials/working_with_the_ensemble.ipynb @@ -20,32 +20,41 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2023-08-30T14:58:34.203827Z", - "start_time": "2023-08-30T14:58:34.187300Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", - "np.random.seed(1)\n", + "import pandas as pd\n", + "\n", + "np.random.seed(1) \n", "\n", - "# initialize a dictionary of empty arrays\n", - "source_dict = {\"id\": np.array([]),\n", - " \"time\": np.array([]),\n", - " \"flux\": np.array([]),\n", - " \"error\": np.array([]),\n", - " \"band\": np.array([])}\n", + "# Generate 10 astronomical objects\n", + "n_obj = 10\n", + "ids = 8000 + np.arange(n_obj)\n", + "names = ids.astype(str)\n", + "object_table = pd.DataFrame(\n", + " {\n", + " \"id\": ids, \n", + " \"name\": names,\n", + " \"ddf_bool\": np.random.randint(0, 2, n_obj), # 0 if from deep drilling field, 1 otherwise\n", + " \"libid_cadence\": np.random.randint(1, 130, n_obj),\n", + " }\n", + ")\n", "\n", - "# Create 10 lightcurves with 100 measurements each\n", + "# Create 1000 lightcurves with 100 measurements each\n", "lc_len = 100\n", - "for i in range(10):\n", - " source_dict[\"id\"] = np.append(source_dict[\"id\"], np.array([i]*lc_len)).astype(int)\n", - " source_dict[\"time\"] = np.append(source_dict[\"time\"], np.linspace(1, lc_len, lc_len))\n", - " source_dict[\"flux\"] = np.append(source_dict[\"flux\"], 100 + 50 * np.random.rand(lc_len))\n", - " source_dict[\"error\"] = np.append(source_dict[\"error\"], 10 + 5 * np.random.rand(lc_len))\n", - " source_dict[\"band\"] = np.append(source_dict[\"band\"], [\"g\"]*50+[\"r\"]*50)" + "num_points = 1000\n", + "all_bands = np.array([\"r\", \"g\", \"b\", \"i\"])\n", + "source_table = pd.DataFrame(\n", + " {\n", + " \"id\": 8000 + (np.arange(num_points) % n_obj),\n", + " \"time\": np.arange(num_points),\n", + " \"flux\": np.random.random_sample(size=num_points)*10,\n", + " \"band\": np.repeat(all_bands, num_points / len(all_bands)),\n", + " \"error\": np.random.random_sample(size=num_points),\n", + " \"count\": np.arange(num_points),\n", + " },\n", + ")" ] }, { @@ -53,7 +62,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can load these into the `Ensemble` using `Ensemble.from_source_dict()`:" + "We can load these into the `Ensemble` using `Ensemble.from_pandas()`:" ] }, { @@ -72,12 +81,15 @@ "ens = Ensemble() # initialize an ensemble object\n", "\n", "# Read in the generated lightcurve data\n", - "ens.from_source_dict(source_dict, \n", - " id_col=\"id\",\n", - " time_col=\"time\",\n", - " flux_col=\"flux\",\n", - " err_col=\"error\",\n", - " band_col=\"band\")" + "ens.from_pandas(\n", + " source_frame=source_table,\n", + " object_frame=object_table,\n", + " id_col=\"id\",\n", + " time_col=\"time\",\n", + " flux_col=\"flux\",\n", + " err_col=\"error\",\n", + " band_col=\"band\",\n", + " npartitions=1)" ] }, { @@ -85,7 +97,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We now have an `Ensemble` object, and have provided it with the constructed data in the source dictionary. Within the call to `Ensemble.from_source_dict`, we specified which columns of the input file mapped to timeseries quantities that the `Ensemble` needs to understand. It's important to link these arguments properly, as the `Ensemble` will use these columns when operations are requested on understood quantities. For example, if an TAPE analysis function requires the time column, from this linking the `Ensemble` will automatically supply that function with the 'time' column." + "We now have an `Ensemble` object, and have provided it with the constructed data in the source dictionary. Within the call to `Ensemble.from_pandas`, we specified which columns of the input file mapped to timeseries quantities that the `Ensemble` needs to understand. It's important to link these arguments properly, as the `Ensemble` will use these columns when operations are requested on understood quantities. For example, if a TAPE analysis function requires the time column, from this linking the `Ensemble` will automatically supply that function with the 'time' column." ] }, { @@ -95,7 +107,7 @@ "source": [ "## Column Mapping with the ColumnMapper\n", "\n", - "In the above example, we manually provide the column labels within the call to `Ensemble.from_source_dict`. Alternatively, the `tape.utils.ColumnMapper` class offers a means to assign the column mappings. Either manually as shown before, or even populated from a known mapping scheme." + "In the above example, we manually provide the column labels within the call to `Ensemble.from_pandas`. Alternatively, the `tape.utils.ColumnMapper` class offers a means to assign the column mappings. Either manually as shown before, or even populated from a known mapping scheme." ] }, { @@ -118,8 +130,12 @@ " err_col=\"error\",\n", " band_col=\"band\")\n", "\n", - "# Pass the ColumnMapper along to from_source_dict\n", - "ens.from_source_dict(source_dict, column_mapper=col_map)" + "# Pass the ColumnMapper along to from_pandas\n", + "ens.from_pandas(\n", + " source_frame=source_table,\n", + " object_frame=object_table,\n", + " column_mapper=col_map,\n", + " npartitions=1)" ] }, { @@ -128,7 +144,9 @@ "metadata": {}, "source": [ "## The Object and Source Frames\n", - "The `Ensemble` maintains two dataframes under the hood, the \"object dataframe\" and the \"source dataframe\". This borrows from the Rubin Observatories object-source convention, where object denotes a given astronomical object and source is the collection of measurements of that object. Essentially, the Object frame stores one-off information about objects, and the source frame stores the available time-domain data. As a result, `Ensemble` functions that operate on the underlying dataframes need to be pointed at either object or source. In most cases, the default is the object table as it's a more helpful interface for understanding the contents of the `Ensemble`, especially when dealing with large volumes of data." + "The `Ensemble` maintains two dataframes under the hood, the \"object dataframe\" and the \"source dataframe\". This borrows from the Rubin Observatories object-source convention, where object denotes a given astronomical object and source is the collection of measurements of that object. Essentially, the Object frame stores one-off information about objects, and the source frame stores the available time-domain data. As a result, `Ensemble` functions that operate on the underlying dataframes need to be pointed at either object or source. In most cases, the default is the object table as it's a more helpful interface for understanding the contents of the `Ensemble`, especially when dealing with large volumes of data.\n", + "\n", + "We can also access Ensemble frames individually with `Ensemble.source` and `Ensemble.object`" ] }, { @@ -151,14 +169,14 @@ }, "outputs": [], "source": [ - "ens._source # We have not actually loaded any data into memory" + "ens.source # We have not actually loaded any data into memory" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Here we are accessing the Dask dataframe underneath, and despite running a command to read in our data, we only see an empty dataframe with some high-level information available. To explicitly bring the data into memory, we must run a `compute()` command." + "Here we are accessing the Dask dataframe and despite running a command to read in our source data, we only see an empty dataframe with some high-level information available. To explicitly bring the data into memory, we must run a `compute()` command on the data's frame." ] }, { @@ -172,7 +190,7 @@ }, "outputs": [], "source": [ - "ens.compute(\"source\") # Compute lets dask know we're ready to bring the data into memory" + "ens.source.compute() # Compute lets dask know we're ready to bring the data into memory" ] }, { @@ -180,9 +198,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "With this compute, we see above that we now have a populated dataframe (a Pandas dataframe in fact!). From this, many workflows in Dask and by extension TAPE, will look like a series of lazily evaluated commands that are chained together and then executed with a .compute() call at the end of the workflow.\n", + "With this compute, we see above that we have returned a populated dataframe (a Pandas dataframe in fact!). From this, many workflows in Dask and by extension TAPE, will look like a series of lazily evaluated commands that are chained together and then executed with a .compute() call at the end of the workflow.\n", + "\n", + "Alternatively we can use `ens.persist()` to execute the chained commands without loading the result into memory. This can speed up future `compute()` calls.\n", "\n", - "Alternatively we can use `ens.persist()` to execute the chained commands without loading the result into memory. This can speed up future `compute()` calls." + "Note that `Ensemble.source` and `Ensemble.object` are instances of the `tape.SourceFrame` and `tape.ObjectFrame` classes respectively. These are subclasses of Dask dataframes that provide some additional utility for tracking by the ensemble while supporting most of the Dask dataframe API. " ] }, { @@ -223,7 +243,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "`Ensemble.info` shows that we have 2000 rows with 54.7 KBs of used memory, and shows the columns we've brought in with their respective data types. If you'd like to actually bring a few rows into memory to inspect, `Ensemble.head` and `Ensemble.tail` provide access to the first n and last n rows respectively." + "`Ensemble.info` shows that we have 2000 rows and the the memory they use, and it also shows the columns we've brought in with their respective data types. If you'd like to actually bring a few rows into memory to inspect, `EnsembleFrame.head` and `EnsembleFrame.tail` provide access to the first n and last n rows respectively." ] }, { @@ -237,7 +257,7 @@ }, "outputs": [], "source": [ - "ens.head(\"object\", 5) # Grabs the first 5 rows of the object table" + "ens.object.head(5) # Grabs the first 5 rows of the object table" ] }, { @@ -251,7 +271,7 @@ }, "outputs": [], "source": [ - "ens.tail(\"source\", 5) # Grabs the last 5 rows of the source table" + "ens.source.tail(5) # Grabs the last 5 rows of the source table" ] }, { @@ -272,7 +292,7 @@ }, "outputs": [], "source": [ - "ens.compute(\"source\")" + "ens.source.compute()" ] }, { @@ -281,9 +301,9 @@ "source": [ "### Filtering\n", "\n", - "The `Ensemble` provides a general filtering function `query` that mirrors a Pandas or Dask `query` command. Specifically, the function takes a string that provides an expression indicating which rows to **keep**. As with other `Ensemble` functions, an optional `table` parameter allows you to filter on either the object or the source table.\n", + "The `Ensemble` provides a general filtering function [`query`](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.query.html) that mirrors a Pandas or Dask `query` command. Specifically, the function takes a string that provides an expression indicating which rows to **keep**. As with other `Ensemble` functions, an optional `table` parameter allows you to filter on either the object or the source table.\n", "\n", - "For example, the following code filters the sources to only include rows with a flux value above 18.2. It uses `ens._flux_col` to retrieve the name of the column with that information." + "For example, the following code filters the sources to only include rows with flux values above the median. It uses `ens._flux_col` to retrieve the name of the column with that information." ] }, { @@ -297,8 +317,8 @@ }, "outputs": [], "source": [ - "ens.query(f\"{ens._flux_col} > 130.0\", table=\"source\")\n", - "ens.compute(\"source\")" + "highest_flux = ens.source[ens._flux_col].quantile(0.95).compute()\n", + "ens.source.query(f\"{ens._flux_col} < {highest_flux}\").compute()" ] }, { @@ -319,7 +339,8 @@ }, "outputs": [], "source": [ - "keep_rows = ens._source[\"error\"] < 12.0\n", + "# Find all of the source points with the lowest 90% of errors.\n", + "keep_rows = ens.source[\"error\"] < ens.source[\"error\"].quantile(0.9)\n", "keep_rows.compute()" ] }, @@ -327,7 +348,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We then pass that series to a `filter_from_series` function:" + "We also provide filtering at the `Ensemble` level, so you can pass the above series to the `Ensemble.filter_from_series` function:" ] }, { @@ -342,7 +363,7 @@ "outputs": [], "source": [ "ens.filter_from_series(keep_rows, table=\"source\")\n", - "ens.compute(\"source\")" + "ens.source.compute()" ] }, { @@ -364,8 +385,8 @@ "outputs": [], "source": [ "# Cleaning nans\n", - "ens.dropna(table=\"source\") # clean nans from source table\n", - "ens.dropna(table=\"object\") # clean nans from object table\n", + "ens.source.dropna() # clean nans from source table\n", + "ens.object.dropna() # clean nans from object table\n", "\n", "# Filtering on number of observations\n", "ens.prune(threshold=10) # threshold is the minimum number of observations needed to retain the object\n", @@ -402,8 +423,7 @@ "outputs": [], "source": [ "# Add a new column so we can filter it out later.\n", - "ens._source = ens._source.assign(band2=ens._source[\"band\"] + \"2\")\n", - "ens.compute(\"source\")" + "ens.source.assign(band2=ens.source[\"band\"] + \"2\").compute()" ] }, { @@ -418,7 +438,68 @@ "outputs": [], "source": [ "ens.select([\"time\", \"flux\", \"error\", \"band\"], table=\"source\")\n", - "ens.compute(\"source\")" + "print(\"The Source table is dirty: \" + str(ens.source.is_dirty()))\n", + "ens.source.compute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Updating an Ensemble's Frames" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `Ensemble` is a manager of `EnsembleFrame` objects (of which `Ensemble.source` and `Ensemble.object` are special cases). When performing operations on one of the tables, the results are not automatically sent to the `Ensemble`.\n", + "\n", + "So while in the above examples we demonstrate several methods where we generated filtered views of the source table, note that the underlying data remained unchanged, with no changes to the rows or columns of `Ensemble.source`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "queried_src = ens.source.query(f\"{ens._flux_col} < {highest_flux}\")\n", + "\n", + "print(len(queried_src))\n", + "print(len(ens.source))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When modifying the views of a dataframe tracked by the `Ensemble`, we can update the `Source` or `Object` frame to use the updated view by calling\n", + "\n", + "`Ensemble.update_frame(view_frame)`\n", + "\n", + "Or alternately:\n", + "\n", + "`view_frame.update_ensemble()`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Now apply the views filter to the source frame.\n", + "queried_src.update_ensemble()\n", + "\n", + "ens.source.compute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the above is still a series of lazy operations that will not be fully evaluated until an operation such as `compute`. So a call to `update_ensemble` will not yet alter or move any underlying data." ] }, { @@ -443,8 +524,8 @@ }, "outputs": [], "source": [ - "ens.assign(table=\"source\", lower_bnd=lambda x: x[\"flux\"] - 2.0 * x[\"error\"])\n", - "ens.compute(table=\"source\")" + "lower_bnd = ens.source.assign(lower_bnd=lambda x: x[\"flux\"] - 2.0 * x[\"error\"])\n", + "lower_bnd" ] }, { @@ -475,6 +556,175 @@ "res" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Storing and Accessing Result Frames" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note for the above `batch` operation, we also printed:\n", + "\n", + "`Using generated label, result_1, for a batch result.`\n", + "\n", + "In addition to the source and object frames, the `Ensemble` may track other frames as well, accessed by either generated or user-provided labels.\n", + "\n", + "We can access a saved frame with `Ensemble.select_frame(label)`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ens.select_frame(\"result_1\").compute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`Ensemble.batch` has an optional `label` argument that will store the result with a user-provided label." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "res = ens.batch(calc_stetson_J, compute=True, label=\"stetson_j\")\n", + "\n", + "ens.select_frame(\"stetson_j\").compute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Likewise we can rename a frame with with a new label, and drop the original frame." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ens.add_frame(ens.select_frame(\"stetson_j\"), \"stetson_j_result_1\") # Add result under new label\n", + "ens.drop_frame(\"stetson_j\") # Drop original label\n", + "\n", + "ens.select_frame(\"stetson_j_result_1\").compute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also add our own frames with `Ensemble.add_frame(frame, label)`. For instance, we can copy this result and add it to a new frame for the `Ensemble` to track as well." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ens.add_frame(res.copy(), \"new_res\")\n", + "ens.select_frame(\"new_res\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally we can also drop frames we are no longer interested in having the `Ensemble` track." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ens.drop_frame(\"result_1\")\n", + "\n", + "try:\n", + " ens.select_frame(\"result_1\") # This should result in a KeyError since the frame has been dropped.\n", + "except Exception as e:\n", + " print(\"As expected, the frame 'result_1 was dropped.\\n\" + str(e))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Keeping the Object and Source Tables in Sync\n", + "\n", + "The Tape `Ensemble` attempts to lazily \"sync\" the Object and Source tables such that:\n", + "\n", + "* If a series of operations removes all lightcurves for a particular object from the Source table, we will lazily remove that object from the Object table.\n", + "* If a series of operations removes an object from the Object table, we will lazily remove all light curves for that object from the Source table.\n", + "\n", + "As an example let's filter the Object table only for objects observed from deep drilling fields. This operation marks the result table as `dirty` indicating to the `Ensemble` that if used as part of a result computation, it should check if the object and source tables are synced. \n", + "\n", + "Note that because we have not called `update_ensemble()` the `Ensemble` is still using the original Object table which is **not** marked `dirty`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ddf_only = ens.object.query(\"ddf_bool == True\")\n", + "\n", + "print(\"Object table is dirty: \" + str(ens.object.is_dirty()))\n", + "print(\"ddf_only is dirty: \" + str(ddf_only.is_dirty()))\n", + "ddf_only.compute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's update the `Ensemble`'s Object table. We can see that the Object table is now considered \"dirty\" so a sync between the Source and Object tables will be triggered by computing a `batch` operation. \n", + "\n", + "As part of the sync the Source table has been modified to drop all sources for objects not observed via Deep Drilling Fields. This is reflected both in the `batch` result output and in the reduced number of rows in the Source table." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ddf_only.update_ensemble()\n", + "print(\"Updated object table is now dirty: \" + str(ens.object.is_dirty()))\n", + "\n", + "print(\"Length of the Source table before the batch operation: \" + str(len(ens.source)))\n", + "res = ens.batch(calc_stetson_J, compute=True)\n", + "print(\"Post-computation object table is now dirty: \" + str(ens.object.is_dirty()))\n", + "print(\"Length of the Source table after the batch operation: \" + str(len(ens.source)))\n", + "res" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To summarize:\n", + "\n", + "* An operation that alters a frame marks that frame as \"dirty\"\n", + "* Such an operation on `Ensemble.source` or `Ensemble.object` won't cause a sync unless the output frame is stored back to either `Ensemble.source` or `Ensemble.object` respectively. This is usually done by a call to `EnsembleFrame.update_ensemble()`\n", + "* Syncs are done lazily such that even when the Object and/or Source frames are \"dirty\", a sync between tables won't be triggered until a relevant computation yields an observable output, such as `batch(..., compute=True)`" + ] + }, { "cell_type": "markdown", "metadata": { @@ -587,6 +837,13 @@ "source": [ "ens.client.close() # Tear down the ensemble client" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -605,7 +862,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.6" + "version": "3.10.13" }, "vscode": { "interpreter": { From 7e6abaf5501364ac7164705f78e240ab542ca48f Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Mon, 27 Nov 2023 23:17:21 -0800 Subject: [PATCH 24/35] Allow EnsembleFrame.compute to Trigger Object-Source Table Syncing (#295) * Allow EnsembleFrame.compue to sync tables * Fixed docstring --- src/tape/ensemble.py | 19 +++ src/tape/ensemble_frame.py | 29 ++++ tests/tape_tests/test_ensemble.py | 238 +++++++++++++++++++++++------- 3 files changed, 231 insertions(+), 55 deletions(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index 54cd148e..63b25d74 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -1670,6 +1670,25 @@ def _generate_object_table(self): return res + def _lazy_sync_tables_from_frame(self, frame): + """Call the sync operation for the frame only if the + table being modified (`frame`) needs to be synced. + Does nothing in the case that only the table to be modified + is dirty or if it is not the object or source frame for this + `Ensemble`. + + Parameters + ---------- + frame: `tape.EnsembleFrame` + The frame being modified. Only an `ObjectFrame` or + `SourceFrame tracked by this `Ensemble` may trigger + a sync. + """ + if frame is self.object or frame is self.source: + # See if we should sync the Object or Source tables. + self._lazy_sync_tables(frame.label) + return self + def _lazy_sync_tables(self, table="object"): """Call the sync operation for the table only if the the table being modified (`table`) needs to be synced. diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index cda7c2b8..c1ad0337 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -556,6 +556,35 @@ def map_partitions(self, func, *args, **kwargs): # If the output of func is another _Frame, let's propagate any metadata. return self._propagate_metadata(result) return result + + def compute(self, **kwargs): + """Compute this Dask collection, returning the underlying dataframe or series. + If tracked by an `Ensemble`, the `Ensemble` is informed of this operation and + is given the opportunity to sync any of its tables prior to this Dask collection + being computed. + + Doc string below derived from dask.dataframe.DataFrame.compute + + This turns a lazy Dask collection into its in-memory equivalent. For example + a Dask array turns into a NumPy array and a Dask dataframe turns into a + Pandas dataframe. The entire dataset must fit into memory before calling + this operation. + + Parameters + ---------- + scheduler: `string`, optional + Which scheduler to use like “threads”, “synchronous” or “processes”. + If not provided, the default is to check the global settings first, + and then fall back to the collection defaults. + optimize_graph: `bool`, optional + If True [default], the graph is optimized before computation. + Otherwise the graph is run as is. This can be useful for debugging. + **kwargs: `dict`, optional + Extra keywords to forward to the scheduler function. + """ + if self.ensemble is not None: + self.ensemble._lazy_sync_tables_from_frame(self) + return super().compute(**kwargs) class TapeSeries(pd.Series): """A barebones extension of a Pandas series to be used for underlying Ensemble data. diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index 11aaefeb..89fb2dbc 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -723,46 +723,84 @@ def test_update_column_map(dask_client): assert cmap_2.map["provenance_col"] == "p" -def test_sync_tables(parquet_ensemble): +@pytest.mark.parametrize("legacy", [True, False]) +def test_sync_tables(parquet_ensemble, legacy): """ - Test that _sync_tables works as expected + Test that _sync_tables works as expected, using Ensemble-level APIs + when `legacy` is `True`, and EsnembleFrame APIs when `legacy` is `False`. """ - - assert len(parquet_ensemble.compute("object")) == 15 - assert len(parquet_ensemble.compute("source")) == 2000 + if legacy: + assert len(parquet_ensemble.compute("object")) == 15 + assert len(parquet_ensemble.compute("source")) == 2000 + else: + assert len(parquet_ensemble.object.compute()) == 15 + assert len(parquet_ensemble.source.compute()) == 2000 parquet_ensemble.prune(50, col_name="nobs_r").prune(50, col_name="nobs_g") - assert parquet_ensemble._object.is_dirty() # Prune should set the object dirty flag + assert parquet_ensemble.object.is_dirty() # Prune should set the object dirty flag + + if legacy: + assert len(parquet_ensemble.compute("object")) == 5 + else: + assert len(parquet_ensemble.object.compute()) == 5 # Replace the maximum flux value with a NaN so that we will have a row to drop. - max_flux = max(parquet_ensemble._source[parquet_ensemble._flux_col]) - parquet_ensemble._source[parquet_ensemble._flux_col] = parquet_ensemble._source[ + max_flux = max(parquet_ensemble.source[parquet_ensemble._flux_col]) + parquet_ensemble.source[parquet_ensemble._flux_col] = parquet_ensemble.source[ parquet_ensemble._flux_col].apply( lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float) ) - parquet_ensemble.dropna(table="source") - assert parquet_ensemble._source.is_dirty() # Dropna should set the source dirty flag + if legacy: + parquet_ensemble.dropna(table="source") + else: + parquet_ensemble.source.dropna().update_ensemble() + assert parquet_ensemble.source.is_dirty() # Dropna should set the source dirty flag # Drop a whole object to test that the object is dropped in the object table - parquet_ensemble.query(f"{parquet_ensemble._id_col} != 88472935274829959", table="source") + if legacy: + parquet_ensemble.query(f"{parquet_ensemble._id_col} != 88472935274829959", table="source") + assert parquet_ensemble.source.is_dirty() + parquet_ensemble.compute() + assert not parquet_ensemble.source.is_dirty() + else: + filtered_src = parquet_ensemble.source.query(f"{parquet_ensemble._id_col} != 88472935274829959") - parquet_ensemble._sync_tables() + # Since we have not yet called update_ensemble, the compute call should not trigger + # a sync and the source table should remain dirty. + assert parquet_ensemble.source.is_dirty() + filtered_src.compute() + assert parquet_ensemble.source.is_dirty() + + # After updating the ensemble validate that a sync occurred and the table is no longer dirty. + filtered_src.update_ensemble() + filtered_src.compute() # Now equivalent to parquet_ensemble.source.compute() + assert not parquet_ensemble.source.is_dirty() # both tables should have the expected number of rows after a sync - assert len(parquet_ensemble.compute("object")) == 4 - assert len(parquet_ensemble.compute("source")) == 1063 + if legacy: + assert len(parquet_ensemble.compute("object")) == 4 + assert len(parquet_ensemble.compute("source")) == 1063 + else: + assert len(parquet_ensemble.object.compute()) == 4 + assert len(parquet_ensemble.source.compute()) == 1063 # dirty flags should be unset after sync assert not parquet_ensemble._object.is_dirty() assert not parquet_ensemble._source.is_dirty() -def test_lazy_sync_tables(parquet_ensemble): +@pytest.mark.parametrize("legacy", [True, False]) +def test_lazy_sync_tables(parquet_ensemble, legacy): """ - Test that _lazy_sync_tables works as expected + Test that _lazy_sync_tables works as expected, using Ensemble-level APIs + when `legacy` is `True`, and EsnembleFrame APIs when `legacy` is `False`. """ - assert len(parquet_ensemble.compute("object")) == 15 - assert len(parquet_ensemble.compute("source")) == 2000 + if legacy: + assert len(parquet_ensemble.compute("object")) == 15 + assert len(parquet_ensemble.compute("source")) == 2000 + else: + assert len(parquet_ensemble.object.compute()) == 15 + assert len(parquet_ensemble.source.compute()) == 2000 # Modify only the object table. parquet_ensemble.prune(50, col_name="nobs_r").prune(50, col_name="nobs_g") @@ -771,12 +809,18 @@ def test_lazy_sync_tables(parquet_ensemble): # For a lazy sync on the object table, nothing should change, because # it is already dirty. - parquet_ensemble._lazy_sync_tables(table="object") + if legacy: + parquet_ensemble.compute("object") + else: + parquet_ensemble.object.compute() assert parquet_ensemble._object.is_dirty() assert not parquet_ensemble._source.is_dirty() # For a lazy sync on the source table, the source table should be updated. - parquet_ensemble._lazy_sync_tables(table="source") + if legacy: + parquet_ensemble.compute("source") + else: + parquet_ensemble.source.compute() assert not parquet_ensemble._object.is_dirty() assert not parquet_ensemble._source.is_dirty() @@ -787,22 +831,80 @@ def test_lazy_sync_tables(parquet_ensemble): parquet_ensemble._flux_col].apply( lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float) ) - parquet_ensemble.dropna(table="source") + + assert not parquet_ensemble._object.is_dirty() + assert not parquet_ensemble._source.is_dirty() + + if legacy: + parquet_ensemble.dropna(table="source") + else: + parquet_ensemble.source.dropna().update_ensemble() assert not parquet_ensemble._object.is_dirty() assert parquet_ensemble._source.is_dirty() # For a lazy sync on the source table, nothing should change, because # it is already dirty. - parquet_ensemble._lazy_sync_tables(table="source") + if legacy: + parquet_ensemble.compute("source") + else: + parquet_ensemble.source.compute() assert not parquet_ensemble._object.is_dirty() assert parquet_ensemble._source.is_dirty() # For a lazy sync on the source, the object table should be updated. - parquet_ensemble._lazy_sync_tables(table="object") + if legacy: + parquet_ensemble.compute("object") + else: + parquet_ensemble.object.compute() assert not parquet_ensemble._object.is_dirty() assert not parquet_ensemble._source.is_dirty() +def test_compute_triggers_syncing(parquet_ensemble): + """ + Tests that tape.EnsembleFrame.compute() only triggers an Ensemble sync if the + frame is the actively tracked source or object table of the Ensemble. + """ + # Test that an object table can trigger a sync that will clean a dirty + # source table. + parquet_ensemble.source.set_dirty(True) + updated_obj = parquet_ensemble.object.dropna() + + # Because we have not yet called update_ensemble(), a sync is not triggered + # and the source table remains dirty. + updated_obj.compute() + assert parquet_ensemble.source.is_dirty() + + # Update the Ensemble so that computing the object table will trigger + # a sync + updated_obj.update_ensemble() + updated_obj.compute() # Now equivalent to Ensemble.object.compute() + assert not parquet_ensemble.source.is_dirty() + + # Test that an source table can trigger a sync that will clean a dirty + # object table. + parquet_ensemble.object.set_dirty(True) + updated_src = parquet_ensemble.source.dropna() + + # Because we have not yet called update_ensemble(), a sync is not triggered + # and the object table remains dirty. + updated_src.compute() + assert parquet_ensemble.object.is_dirty() + + # Update the Ensemble so that computing the object table will trigger + # a sync + updated_src.update_ensemble() + updated_src.compute() # Now equivalent to Ensemble.source.compute() + assert not parquet_ensemble.object.is_dirty() + + # Generate a new Object frame and set the Ensemble to None to + # validate that we return a valid result even for untracked frames + # which cannot be synced. + new_obj_frame = parquet_ensemble.object.dropna() + new_obj_frame.ensemble = None + assert len(new_obj_frame.compute()) > 0 + + def test_temporary_cols(parquet_ensemble): """ Test that temporary columns are tracked and dropped as expected. @@ -924,19 +1026,24 @@ def test_temporary_cols(parquet_ensemble): assert "f2" not in ens._source.columns -def test_dropna(parquet_ensemble): +@pytest.mark.parametrize("legacy", [True, False]) +def test_dropna(parquet_ensemble, legacy): + """Tests dropna, using Ensemble.dropna when `legacy` is `True`, and + EnsembleFrame.dropna when `legacy` is `False`.""" # Try passing in an unrecognized 'table' parameter and verify an exception is thrown with pytest.raises(ValueError): parquet_ensemble.dropna(table="banana") # First test dropping na from the 'source' table - # - source_pdf = parquet_ensemble._source.compute() + source_pdf = parquet_ensemble.source.compute() source_length = len(source_pdf.index) # Try dropping NaNs from source and confirm nothing is dropped (there are no NaNs). - parquet_ensemble.dropna(table="source") - assert len(parquet_ensemble._source.compute().index) == source_length + if legacy: + parquet_ensemble.dropna(table="source") + else: + parquet_ensemble.source.dropna().update_ensemble() + assert len(parquet_ensemble.source) == source_length # Get a valid ID to use and count its occurrences. valid_source_id = source_pdf.index.values[1] @@ -949,19 +1056,26 @@ def test_dropna(parquet_ensemble): parquet_ensemble.update_frame(SourceFrame.from_tapeframe(TapeSourceFrame(source_pdf), label="source", npartitions=1)) # Try dropping NaNs from source and confirm that we did. - parquet_ensemble.dropna(table="source") + if legacy: + parquet_ensemble.dropna(table="source") + else: + parquet_ensemble.source.dropna().update_ensemble() assert len(parquet_ensemble._source.compute().index) == source_length - occurrences_source - # Sync the table and check that the number of objects decreased. - # parquet_ensemble._sync_tables() - # Now test dropping na from 'object' table - object_pdf = parquet_ensemble._object.compute() + # Sync the tables + parquet_ensemble._sync_tables() + + # Sync (triggered by the compute) the table and check that the number of objects decreased. + object_pdf = parquet_ensemble.object.compute() object_length = len(object_pdf.index) # Try dropping NaNs from object and confirm nothing is dropped (there are no NaNs). - parquet_ensemble.dropna(table="object") - assert len(parquet_ensemble._object.compute().index) == object_length + if legacy: + parquet_ensemble.dropna(table="object") + else: + parquet_ensemble.object.dropna().update_ensemble() + assert len(parquet_ensemble.object.compute().index) == object_length # get a valid object id and set at least two occurences of that id in the object table valid_object_id = object_pdf.index.values[1] @@ -975,10 +1089,12 @@ def test_dropna(parquet_ensemble): parquet_ensemble.update_frame(ObjectFrame.from_tapeframe(TapeObjectFrame(object_pdf), label="object", npartitions=1)) # Try dropping NaNs from object and confirm that we did. - parquet_ensemble.dropna(table="object") - assert len(parquet_ensemble._object.compute().index) == object_length - occurrences_object - - new_objects_pdf = parquet_ensemble._object.compute() + if legacy: + parquet_ensemble.dropna(table="object") + else: + parquet_ensemble.object.dropna().update_ensemble() + assert len(parquet_ensemble.object.compute().index) == object_length - occurrences_object + new_objects_pdf = parquet_ensemble.object.compute() assert len(new_objects_pdf.index) == len(object_pdf.index) - occurrences_object # Assert the filtered ID is no longer in the objects. @@ -989,9 +1105,10 @@ def test_dropna(parquet_ensemble): for c in new_objects_pdf.columns.values: assert new_objects_pdf.loc[i, c] == object_pdf.loc[i, c] - -def test_keep_zeros(parquet_ensemble): - """Test that we can sync the tables and keep objects with zero sources.""" +@pytest.mark.parametrize("legacy", [True, False]) +def test_keep_zeros(parquet_ensemble, legacy): + """Test that we can sync the tables and keep objects with zero sources, using + Ensemble.dropna when `legacy` is `True`, and EnsembleFrame.dropna when `legacy` is `False`.""" parquet_ensemble.keep_empty_objects = True prev_npartitions = parquet_ensemble._object.npartitions @@ -1007,7 +1124,10 @@ def test_keep_zeros(parquet_ensemble): parquet_ensemble.update_frame(SourceFrame.from_tapeframe(TapeSourceFrame(pdf), npartitions=1, label="source")) # Sync the table and check that the number of objects decreased. - parquet_ensemble.dropna(table="source") + if legacy: + parquet_ensemble.dropna("source") + else: + parquet_ensemble.source.dropna().update_ensemble() parquet_ensemble._sync_tables() # Check that objects are preserved after sync @@ -1130,8 +1250,10 @@ def test_select(dask_client): assert "count" not in ens._source.columns assert "something_else" not in ens._source.columns - -def test_assign(dask_client): +@pytest.mark.parametrize("legacy", [True, False]) +def test_assign(dask_client, legacy): + """Tests assign for column-manipulation, using Ensemble.assign when `legacy` is `True`, + and EnsembleFrame.assign when `legacy` is `False`.""" ens = Ensemble(client=dask_client) num_points = 1000 @@ -1145,29 +1267,35 @@ def test_assign(dask_client): } cmap = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="err", band_col="band") ens.from_source_dict(rows, column_mapper=cmap, npartitions=1) - assert len(ens._source.columns) == 4 + assert len(ens.source.columns) == 4 assert "lower_bnd" not in ens._source.columns # Insert a new column for the "lower bound" computation. - ens.assign(table="source", lower_bnd=lambda x: x["flux"] - 2.0 * x["err"]) - assert len(ens._source.columns) == 5 - assert "lower_bnd" in ens._source.columns + if legacy: + ens.assign(table="source", lower_bnd=lambda x: x["flux"] - 2.0 * x["err"]) + else: + ens.source.assign(lower_bnd=lambda x: x["flux"] - 2.0 * x["err"]).update_ensemble() + assert len(ens.source.columns) == 5 + assert "lower_bnd" in ens.source.columns # Check the values in the new column. - new_source = ens.compute(table="source") + new_source = ens.source.compute() if not legacy else ens.compute(table="source") assert new_source.shape[0] == 1000 for i in range(1000): expected = new_source.iloc[i]["flux"] - 2.0 * new_source.iloc[i]["err"] assert new_source.iloc[i]["lower_bnd"] == expected # Create a series directly from the table. - res_col = ens._source["band"] + "2" - ens.assign(table="source", band2=res_col) - assert len(ens._source.columns) == 6 - assert "band2" in ens._source.columns + res_col = ens.source["band"] + "2" + if legacy: + ens.assign(table="source", band2=res_col) + else: + ens.source.assign(band2=res_col).update_ensemble() + assert len(ens.source.columns) == 6 + assert "band2" in ens.source.columns # Check the values in the new column. - new_source = ens.compute(table="source") + new_source = ens.source.compute() if not legacy else ens.compute(table="source") for i in range(1000): assert new_source.iloc[i]["band2"] == new_source.iloc[i]["band"] + "2" From a714b1024484db5532f890080ca94e211936e03e Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Wed, 29 Nov 2023 12:27:13 -0800 Subject: [PATCH 25/35] Add Explicit Metadata Propagation for EnsembleFrame joins (#301) * Support propagating frame metadata in joins * Update doc strings and test --- src/tape/ensemble_frame.py | 64 ++++++++++++++++++++++++- tests/tape_tests/test_ensemble_frame.py | 39 ++++++++++++++- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index c1ad0337..38a57074 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -286,6 +286,68 @@ def merge(self, right, **kwargs): result = super().merge(right, **kwargs) return self._propagate_metadata(result) + def join(self, other, **kwargs): + """Join columns of another DataFrame. Note that if `other` is a different type, + we expect the result to have the type of this object regardless of the value + of the`how` parameter. + + This docstring was copied from pandas.core.frame.DataFrame.join. + + Some inconsistencies with this version may exist. + + Join columns with `other` DataFrame either on index or on a key + column. Efficiently join multiple DataFrame objects by index at once by + passing a list. + + Parameters + ---------- + other : DataFrame, Series, or a list containing any combination of them + Index should be similar to one of the columns in this one. If a + Series is passed, its name attribute must be set, and that will be + used as the column name in the resulting joined DataFrame. + on : str, list of str, or array-like, optional + Column or index level name(s) in the caller to join on the index + in `other`, otherwise joins index-on-index. If multiple + values given, the `other` DataFrame must have a MultiIndex. Can + pass an array as the join key if it is not already contained in + the calling DataFrame. Like an Excel VLOOKUP operation. + how : {'left', 'right', 'outer', 'inner', 'cross'}, default 'left' + How to handle the operation of the two objects. + + * left: use calling frame's index (or column if on is specified) + * right: use `other`'s index. + * outer: form union of calling frame's index (or column if on is + specified) with `other`'s index, and sort it lexicographically. + * inner: form intersection of calling frame's index (or column if + on is specified) with `other`'s index, preserving the order + of the calling's one. + * cross: creates the cartesian product from both frames, preserves the order + of the left keys. + lsuffix : str, default '' + Suffix to use from left frame's overlapping columns. + rsuffix : str, default '' + Suffix to use from right frame's overlapping columns. + sort : bool, default False + Order result DataFrame lexicographically by the join key. If False, + the order of the join key depends on the join type (how keyword). + validate : str, optional + If specified, checks if join is of specified type. + + * "one_to_one" or "1:1": check if join keys are unique in both left + and right datasets. + * "one_to_many" or "1:m": check if join keys are unique in left dataset. + * "many_to_one" or "m:1": check if join keys are unique in right dataset. + * "many_to_many" or "m:m": allowed, but does not result in checks. + + Returns + ------- + result: `tape._Frame` + A TAPE dataframe containing columns from both the caller and `other`. + + """ + result = super().join(other, **kwargs) + return self._propagate_metadata(result) + def drop(self, labels=None, axis=0, columns=None, errors="raise"): """Drop specified labels from rows or columns. @@ -316,7 +378,7 @@ def drop(self, labels=None, axis=0, columns=None, errors="raise"): Returns ------- result: `tape._Frame` - Returns the frame or Nonewith the specified + Returns the frame or None with the specified index or column labels removed or None if inplace=True. """ result = self._propagate_metadata(super().drop(labels=labels, axis=axis, columns=columns, errors=errors)) diff --git a/tests/tape_tests/test_ensemble_frame.py b/tests/tape_tests/test_ensemble_frame.py index fdf0f527..8f45e69e 100644 --- a/tests/tape_tests/test_ensemble_frame.py +++ b/tests/tape_tests/test_ensemble_frame.py @@ -297,4 +297,41 @@ def test_object_and_source_frame_propagation(data_fixture, request): assert isinstance(merged_frame, SourceFrame) assert merged_frame.label == SOURCE_LABEL assert merged_frame.ensemble == ens - assert merged_frame.is_dirty() \ No newline at end of file + assert merged_frame.is_dirty() + + +def test_object_and_source_joins(parquet_ensemble): + """ + Test that SourceFrame and ObjectFrame metadata and class type are correctly propagated across + joins. + """ + # Get Source and object frames to test joins on. + source_frame, object_frame = parquet_ensemble.source.copy(), parquet_ensemble.object.copy() + + # Verify their metadata was preserved in the copy() + assert source_frame.label == SOURCE_LABEL + assert source_frame.ensemble is parquet_ensemble + assert object_frame.label == OBJECT_LABEL + assert object_frame.ensemble is parquet_ensemble + + # Join a SourceFrame (left) with an ObjectFrame (right) + # Validate that metadata is preserved and the outputted object is a SourceFrame + joined_source = source_frame.join(object_frame, how='left') + assert joined_source.label is SOURCE_LABEL + assert type(joined_source) is SourceFrame + assert joined_source.ensemble is parquet_ensemble + + # Now the same form of join (in terms of left/right) but produce an ObjectFrame. This is + # because frame1.join(frame2) will yield frame1's type regardless of left vs right. + assert type(object_frame.join(source_frame, how='right')) is ObjectFrame + + # Join an ObjectFrame (left) with a SourceFrame (right) + # Validate that metadata is preserved and the outputted object is an ObjectFrame + joined_object = object_frame.join(source_frame, how='left') + assert joined_object.label is OBJECT_LABEL + assert type(joined_object) is ObjectFrame + assert joined_object.ensemble is parquet_ensemble + + # Now the same form of join (in terms of left/right) but produce a SourceFrame. This is + # because frame1.join(frame2) will yield frame1's type regardless of left vs right. + assert type(source_frame.join(object_frame, how='right')) is SourceFrame \ No newline at end of file From 8f8cc665f33d921d483e267dec705e54c612b5aa Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Thu, 30 Nov 2023 16:32:04 -0800 Subject: [PATCH 26/35] Update test --- tests/tape_tests/test_ensemble.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index 016fc4b0..4f1ff28e 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -759,12 +759,6 @@ def test_sync_tables(data_fixture, request, legacy): else: assert len(parquet_ensemble.object.compute()) == 5 - # Replace the maximum flux value with a NaN so that we will have a row to drop. - max_flux = max(parquet_ensemble.source[parquet_ensemble._flux_col]) - parquet_ensemble.source[parquet_ensemble._flux_col] = parquet_ensemble.source[ - parquet_ensemble._flux_col].apply( - lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float) - ) if legacy: parquet_ensemble.dropna(table="source") else: From 5c847e10bd6d4865a1016f8aa621f75129cf8431 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Thu, 30 Nov 2023 17:30:08 -0800 Subject: [PATCH 27/35] Merge Main into Ensemble Refactor Branch (#304) * check divisions, enable lazy syncs * check divisions, enable lazy syncs * initial tests * add tests; calc_nobs preserve divisions * batch with divisions * cleanup * fix sf2 tests * add sync_tables check * cleanup * fix calc_nobs reset_index issue * per table warnings; index comments * add map_partitions mode for calc_nobs when divisions are known * build metadata * build metadata * add multi partition test * add version file to init * add small test * Fix table syncing to use inner joins. (#303) * Fix table syncing to use inner joins. * fix lint error * Update test --------- Co-authored-by: Doug Branton --- src/tape/__init__.py | 1 + src/tape/ensemble.py | 156 ++++++++++++++++++-------- tests/tape_tests/conftest.py | 19 ++++ tests/tape_tests/test_ensemble.py | 172 ++++++++++++++++++++++------- tests/tape_tests/test_packaging.py | 6 + 5 files changed, 269 insertions(+), 85 deletions(-) create mode 100644 tests/tape_tests/test_packaging.py diff --git a/src/tape/__init__.py b/src/tape/__init__.py index e2ac94ab..1e9471fa 100644 --- a/src/tape/__init__.py +++ b/src/tape/__init__.py @@ -3,3 +3,4 @@ from .ensemble_frame import * # noqa from .timeseries import * # noqa from .ensemble_readers import * # noqa +from ._version import __version__ # noqa diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index 63b25d74..1002dff3 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -13,11 +13,10 @@ from .analysis.feature_extractor import BaseLightCurveFeature, FeatureExtractor from .analysis.structure_function import SF_METHODS from .analysis.structurefunction2 import calc_sf2 -from .ensemble_frame import EnsembleFrame, EnsembleSeries, ObjectFrame, SourceFrame, TapeFrame, TapeSeries +from .ensemble_frame import EnsembleFrame, EnsembleSeries, ObjectFrame, SourceFrame, TapeFrame, TapeObjectFrame, TapeSourceFrame, TapeSeries from .timeseries import TimeSeries from .utils import ColumnMapper -# TODO import from EnsembleFrame...? SOURCE_FRAME_LABEL = "source" OBJECT_FRAME_LABEL = "object" @@ -48,7 +47,6 @@ def __init__(self, client=True, **kwargs): # A unique ID to allocate new result frame labels. self.default_frame_id = 1 - # TODO(wbeebe@uw.edu) Replace self._source and self._object with these self.source = None # Source Table EnsembleFrame self.object = None # Object Table EnsembleFrame @@ -779,40 +777,68 @@ def calc_nobs(self, by_band=False, label="nobs", temporary=True): """ if by_band: - band_counts = ( - self._source.groupby([self._id_col])[self._band_col] # group by each object - .value_counts() # count occurence of each band - .to_frame() # convert series to dataframe - .reset_index() # break up the multiindex - .categorize(columns=[self._band_col]) # retype the band labels as categories - .pivot_table(values=self._band_col, index=self._id_col, columns=self._band_col, aggfunc="sum") - ) # the pivot_table call makes each band_count a column of the id_col row - # repartition the result to align with object if self._object.known_divisions: - self._object.divisions = tuple([None for i in range(self._object.npartitions + 1)]) - band_counts = band_counts.repartition(npartitions=self._object.npartitions) + # Grab these up front to help out the task graph + id_col = self._id_col + band_col = self._band_col + + # Get the band metadata + unq_bands = np.unique(self._source[band_col]) + meta = {band: float for band in unq_bands} + + # Map the groupby to each partition + band_counts = self._source.map_partitions( + lambda x: x.groupby(id_col)[[band_col]] + .value_counts() + .to_frame() + .reset_index() + .pivot_table(values=band_col, index=id_col, columns=band_col, aggfunc="sum"), + meta=meta, + ).repartition(divisions=self._object.divisions) else: + band_counts = ( + self._source.groupby([self._id_col])[self._band_col] # group by each object + .value_counts() # count occurence of each band + .to_frame() # convert series to dataframe + .rename(columns={self._band_col: "counts"}) # rename column + .reset_index() # break up the multiindex + .categorize(columns=[self._band_col]) # retype the band labels as categories + .pivot_table( + values=self._band_col, index=self._id_col, columns=self._band_col, aggfunc="sum" + ) + ) # the pivot_table call makes each band_count a column of the id_col row + band_counts = band_counts.repartition(npartitions=self._object.npartitions) # short-hand for calculating nobs_total band_counts["total"] = band_counts[list(band_counts.columns)].sum(axis=1) bands = band_counts.columns.values - self._object = self._object.assign(**{label + "_" + band: band_counts[band] for band in bands}) + self._object = self._object.assign( + **{label + "_" + str(band): band_counts[band] for band in bands} + ) if temporary: - self._object_temp.extend(label + "_" + band for band in bands) + self._object_temp.extend(label + "_" + str(band) for band in bands) else: - counts = self._source.groupby([self._id_col])[[self._band_col]].aggregate("count") - - # repartition the result to align with object - if self._object.known_divisions: - self._object.divisions = tuple([None for i in range(self._object.npartitions + 1)]) - counts = counts.repartition(npartitions=self._object.npartitions) + if self._object.known_divisions and self._source.known_divisions: + # Grab these up front to help out the task graph + id_col = self._id_col + band_col = self._band_col + + # Map the groupby to each partition + counts = self._source.map_partitions( + lambda x: x.groupby([id_col])[[band_col]].aggregate("count") + ).repartition(divisions=self._object.divisions) else: - counts = counts.repartition(npartitions=self._object.npartitions) + # Just do a groupby on all source + counts = ( + self._source.groupby([self._id_col])[[self._band_col]] + .aggregate("count") + .repartition(npartitions=self._object.npartitions) + ) self._object = self._object.assign(**{label + "_total": counts[self._band_col]}) @@ -849,8 +875,7 @@ def prune(self, threshold=50, col_name=None): col_name = "nobs_total" # Mask on object table - mask = self._object[col_name] >= threshold - self.update_frame(self._object[mask]) + self = self.query(f"{col_name} >= {threshold}", table="object") self._object.set_dirty(True) # Object table is now dirty @@ -1134,12 +1159,18 @@ def s2n_inter_quartile_range(flux, err): meta=meta, ) + # Inherit divisions if known from source and the resulting index is the id + # Groupby on index should always return a subset that adheres to the same divisions criteria + if self._source.known_divisions and batch.index.name == self._id_col: + batch.divisions = self._source.divisions + if label is not None: if label == "": label = self._generate_frame_label() print(f"Using generated label, {label}, for a batch result.") # Track the result frame under the provided label self.add_frame(batch, label) + if compute: return batch.compute() else: @@ -1243,8 +1274,6 @@ def from_dask_dataframe( The ensemble object with the Dask dataframe data loaded. """ self._load_column_mapper(column_mapper, **kwargs) - - # TODO(wbeebe@uw.edu): Determine most efficient way to convert to SourceFrame/ObjectFrame source_frame = SourceFrame.from_dask_dataframe(source_frame, self) # Set the index of the source frame and save the resulting table @@ -1255,7 +1284,6 @@ def from_dask_dataframe( self.update_frame(self._generate_object_table()) else: - # TODO(wbeebe@uw.edu): Determine most efficient way to convert to SourceFrame/ObjectFrame self.update_frame(ObjectFrame.from_dask_dataframe(object_frame, ensemble=self)) self.update_frame(self._object.set_index(self._id_col, sorted=sorted, sort=sort)) @@ -1270,6 +1298,12 @@ def from_dask_dataframe( elif partition_size: self._source = self._source.repartition(partition_size=partition_size) + # Check that Divisions are established, warn if not. + for name, table in [("object", self._object), ("source", self._source)]: + if not table.known_divisions: + warnings.warn( + f"Divisions for {name} are not set, certain downstream dask operations may fail as a result. We recommend setting the `sort` or `sorted` flags when loading data to establish division information." + ) return self def from_hipscat(self, dir, source_subdir="source", object_subdir="object", column_mapper=None, **kwargs): @@ -1464,7 +1498,10 @@ def from_parquet( columns.append(self._provenance_col) # Read in the source parquet file(s) - source = SourceFrame.from_parquet(source_file, index=self._id_col, columns=columns, ensemble=self) + # Index is set False so that we can set it with a future set_index call + # This has the advantage of letting Dask set partition boundaries based + # on the divisions between the sources of different objects. + source = SourceFrame.from_parquet(source_file, index=False, columns=columns, ensemble=self) # Generate a provenance column if not provided if self._provenance_col is None: @@ -1474,7 +1511,9 @@ def from_parquet( object = None if object_file: # Read in the object file(s) - object = ObjectFrame.from_parquet(object_file, index=self._id_col, ensemble=self) + # Index is False so that we can set it with a future set_index call + # More meaningful for source than object but parity seems good here + object = ObjectFrame.from_parquet(object_file, index=False, ensemble=self) return self.from_dask_dataframe( source_frame=source, object_frame=object, @@ -1660,13 +1699,7 @@ def convert_flux_to_mag(self, zero_point, zp_form="mag", out_col_name=None, flux def _generate_object_table(self): """Generate an empty object table from the source table.""" - sor_idx = self._source.index.unique() - obj_df = pd.DataFrame(index=sor_idx) - - # Convert the resulting dataframe into an ObjectFrame - # TODO(wbeebe): Switch for a cleaner loading fucnction - res = ObjectFrame.from_dask_dataframe( - dd.from_pandas(obj_df, npartitions=int(np.ceil(self._source.npartitions / 100))), ensemble=self) + res = self._source.map_partitions(lambda x: TapeObjectFrame(index=x.index.unique())) return res @@ -1719,9 +1752,20 @@ def _sync_tables(self): if self._object.is_dirty(): # Sync Object to Source; remove any missing objects from source - obj_idx = list(self._object.index.compute()) - self.update_frame(self._source.map_partitions(lambda x: x[x.index.isin(obj_idx)])) - self.update_frame(self._source.persist()) # persist the source frame + + if self._object.known_divisions and self._source.known_divisions: + # Lazily Create an empty object table (just index) for joining + empty_obj = self._object.map_partitions(lambda x: TapeObjectFrame(index=x.index)) + if type(empty_obj) != type(self._object): + raise ValueError("Bad type for empty_obj: " + str(type(empty_obj))) + + # Join source onto the empty object table to align + self.update_frame(self._source.join(empty_obj, how="inner")) + else: + warnings.warn("Divisions are not known, syncing using a non-lazy method.") + obj_idx = list(self._object.index.compute()) + self.update_frame(self._source.map_partitions(lambda x: x[x.index.isin(obj_idx)])) + self.update_frame(self._source.persist()) # persist the source frame # Drop Temporary Source Columns on Sync if len(self._source_temp): @@ -1731,10 +1775,20 @@ def _sync_tables(self): if self._source.is_dirty(): # not elif if not self.keep_empty_objects: - # Sync Source to Object; remove any objects that do not have sources - sor_idx = list(self._source.index.unique().compute()) - self.update_frame(self._object.map_partitions(lambda x: x[x.index.isin(sor_idx)])) - self.update_frame(self._object.persist()) # persist the object frame + if self._object.known_divisions and self._source.known_divisions: + # Lazily Create an empty source table (just unique indexes) for joining + empty_src = self._source.map_partitions(lambda x: TapeSourceFrame(index=x.index.unique())) + if type(empty_src) != type(self._source): + raise ValueError("Bad type for empty_src: " + str(type(empty_src))) + + # Join object onto the empty unique source table to align + self.update_frame(self._object.join(empty_src, how="inner")) + else: + warnings.warn("Divisions are not known, syncing using a non-lazy method.") + # Sync Source to Object; remove any objects that do not have sources + sor_idx = list(self._source.index.unique().compute()) + self.update_frame(self._object.map_partitions(lambda x: x[x.index.isin(sor_idx)])) + self.update_frame(self._object.persist()) # persist the object frame # Drop Temporary Object Columns on Sync if len(self._object_temp): @@ -1834,7 +1888,7 @@ def _build_index(self, obj_id, band): index = pd.MultiIndex.from_tuples(tuples, names=["object_id", "band", "index"]) return index - def sf2(self, sf_method="basic", argument_container=None, use_map=True): + def sf2(self, sf_method="basic", argument_container=None, use_map=True, compute=True): """Wrapper interface for calling structurefunction2 on the ensemble Parameters @@ -1876,11 +1930,17 @@ def sf2(self, sf_method="basic", argument_container=None, use_map=True): self._source.index, argument_container=argument_container, ) - return result + else: - result = self.batch(calc_sf2, use_map=use_map, argument_container=argument_container) + result = self.batch( + calc_sf2, use_map=use_map, argument_container=argument_container, compute=compute + ) - return result + # Inherit divisions information if known + if self._source.known_divisions and self._object.known_divisions: + result.divisions = self._source.divisions + + return result def _translate_meta(self, meta): """Translates Dask-style meta into a TapeFrame or TapeSeries object. diff --git a/tests/tape_tests/conftest.py b/tests/tape_tests/conftest.py index e416a04a..c0af84c3 100644 --- a/tests/tape_tests/conftest.py +++ b/tests/tape_tests/conftest.py @@ -270,6 +270,25 @@ def parquet_ensemble(dask_client): return ens +# pylint: disable=redefined-outer-name +@pytest.fixture +def parquet_ensemble_with_divisions(dask_client): + """Create an Ensemble from parquet data.""" + ens = Ensemble(client=dask_client) + ens.from_parquet( + "tests/tape_tests/data/source/test_source.parquet", + "tests/tape_tests/data/object/test_object.parquet", + id_col="ps1_objid", + time_col="midPointTai", + band_col="filterName", + flux_col="psFlux", + err_col="psFluxErr", + sort=True, + ) + + return ens + + # pylint: disable=redefined-outer-name @pytest.fixture def parquet_ensemble_from_source(dask_client): diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index 89fb2dbc..c36d5dd9 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -32,6 +32,7 @@ def test_with_client(): "data_fixture", [ "parquet_ensemble", + "parquet_ensemble_with_divisions", "parquet_ensemble_without_client", "parquet_ensemble_from_source", "parquet_ensemble_from_hipscat", @@ -61,6 +62,11 @@ def test_parquet_construction(data_fixture, request): assert parquet_ensemble._source is not None assert parquet_ensemble._object is not None + # Make sure divisions are set + if data_fixture == "parquet_ensemble_with_divisions": + assert parquet_ensemble._source.known_divisions + assert parquet_ensemble._object.known_divisions + # Check that the data is not empty. obj, source = parquet_ensemble.compute() assert len(source) == 2000 @@ -723,12 +729,21 @@ def test_update_column_map(dask_client): assert cmap_2.map["provenance_col"] == "p" +@pytest.mark.parametrize( + "data_fixture", + [ + "parquet_ensemble", + "parquet_ensemble_with_divisions", + ], +) @pytest.mark.parametrize("legacy", [True, False]) -def test_sync_tables(parquet_ensemble, legacy): +def test_sync_tables(data_fixture, request, legacy): """ Test that _sync_tables works as expected, using Ensemble-level APIs when `legacy` is `True`, and EsnembleFrame APIs when `legacy` is `False`. """ + parquet_ensemble = request.getfixturevalue(data_fixture) + if legacy: assert len(parquet_ensemble.compute("object")) == 15 assert len(parquet_ensemble.compute("source")) == 2000 @@ -744,24 +759,16 @@ def test_sync_tables(parquet_ensemble, legacy): else: assert len(parquet_ensemble.object.compute()) == 5 - # Replace the maximum flux value with a NaN so that we will have a row to drop. - max_flux = max(parquet_ensemble.source[parquet_ensemble._flux_col]) - parquet_ensemble.source[parquet_ensemble._flux_col] = parquet_ensemble.source[ - parquet_ensemble._flux_col].apply( - lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float) - ) if legacy: parquet_ensemble.dropna(table="source") else: parquet_ensemble.source.dropna().update_ensemble() assert parquet_ensemble.source.is_dirty() # Dropna should set the source dirty flag - # Drop a whole object to test that the object is dropped in the object table + # Drop a whole object from Source to test that the object is dropped in the object table + dropped_obj_id = 88472935274829959 if legacy: - parquet_ensemble.query(f"{parquet_ensemble._id_col} != 88472935274829959", table="source") - assert parquet_ensemble.source.is_dirty() - parquet_ensemble.compute() - assert not parquet_ensemble.source.is_dirty() + parquet_ensemble.query(f"{parquet_ensemble._id_col} != {dropped_obj_id}", table="source") else: filtered_src = parquet_ensemble.source.query(f"{parquet_ensemble._id_col} != 88472935274829959") @@ -771,12 +778,16 @@ def test_sync_tables(parquet_ensemble, legacy): filtered_src.compute() assert parquet_ensemble.source.is_dirty() - # After updating the ensemble validate that a sync occurred and the table is no longer dirty. + # Update the ensemble to use the filtered source. filtered_src.update_ensemble() - filtered_src.compute() # Now equivalent to parquet_ensemble.source.compute() - assert not parquet_ensemble.source.is_dirty() - # both tables should have the expected number of rows after a sync + # Verify that the object ID we removed from the source table is present in the object table + assert dropped_obj_id in parquet_ensemble._object.index.compute().values + + # Perform an operation which should trigger syncing both tables. + parquet_ensemble.compute() + + # Both tables should have the expected number of rows after a sync if legacy: assert len(parquet_ensemble.compute("object")) == 4 assert len(parquet_ensemble.compute("source")) == 1063 @@ -784,9 +795,18 @@ def test_sync_tables(parquet_ensemble, legacy): assert len(parquet_ensemble.object.compute()) == 4 assert len(parquet_ensemble.source.compute()) == 1063 - # dirty flags should be unset after sync - assert not parquet_ensemble._object.is_dirty() - assert not parquet_ensemble._source.is_dirty() + # Validate that the filtered object has been removed from both tables. + assert dropped_obj_id not in parquet_ensemble.source.index.compute().values + assert dropped_obj_id not in parquet_ensemble.object.index.compute().values + + # Dirty flags should be unset after sync + assert not parquet_ensemble.object_dirty + assert not parquet_ensemble.source_dirty + + # Make sure that divisions are preserved + if data_fixture == "parquet_ensemble_with_divisions": + assert parquet_ensemble.source.known_divisions + assert parquet_ensemble.object.known_divisions @pytest.mark.parametrize("legacy", [True, False]) @@ -1026,10 +1046,19 @@ def test_temporary_cols(parquet_ensemble): assert "f2" not in ens._source.columns +@pytest.mark.parametrize( + "data_fixture", + [ + "parquet_ensemble", + "parquet_ensemble_with_divisions", + ], +) @pytest.mark.parametrize("legacy", [True, False]) -def test_dropna(parquet_ensemble, legacy): +def test_dropna(data_fixture, request, legacy): """Tests dropna, using Ensemble.dropna when `legacy` is `True`, and EnsembleFrame.dropna when `legacy` is `False`.""" + parquet_ensemble = request.getfixturevalue(data_fixture) + # Try passing in an unrecognized 'table' parameter and verify an exception is thrown with pytest.raises(ValueError): parquet_ensemble.dropna(table="banana") @@ -1062,6 +1091,10 @@ def test_dropna(parquet_ensemble, legacy): parquet_ensemble.source.dropna().update_ensemble() assert len(parquet_ensemble._source.compute().index) == source_length - occurrences_source + if data_fixture == "parquet_ensemble_with_divisions": + # divisions should be preserved + assert parquet_ensemble._source.known_divisions + # Now test dropping na from 'object' table # Sync the tables parquet_ensemble._sync_tables() @@ -1077,10 +1110,8 @@ def test_dropna(parquet_ensemble, legacy): parquet_ensemble.object.dropna().update_ensemble() assert len(parquet_ensemble.object.compute().index) == object_length - # get a valid object id and set at least two occurences of that id in the object table + # select an id from the object table valid_object_id = object_pdf.index.values[1] - object_pdf.index.values[0] = valid_object_id - occurrences_object = len(object_pdf.loc[valid_object_id].values) # Set the nobs_g values for one object to NaN so we can drop it. # We do this on the instantiated object (pdf) and convert it back into a @@ -1088,14 +1119,19 @@ def test_dropna(parquet_ensemble, legacy): object_pdf.loc[valid_object_id, parquet_ensemble._object.columns[0]] = pd.NA parquet_ensemble.update_frame(ObjectFrame.from_tapeframe(TapeObjectFrame(object_pdf), label="object", npartitions=1)) - # Try dropping NaNs from object and confirm that we did. + # Try dropping NaNs from object and confirm that we dropped a row if legacy: parquet_ensemble.dropna(table="object") else: parquet_ensemble.object.dropna().update_ensemble() - assert len(parquet_ensemble.object.compute().index) == object_length - occurrences_object + assert len(parquet_ensemble.object.compute().index) == object_length - 1 + + if data_fixture == "parquet_ensemble_with_divisions": + # divisions should be preserved + assert parquet_ensemble._object.known_divisions + new_objects_pdf = parquet_ensemble.object.compute() - assert len(new_objects_pdf.index) == len(object_pdf.index) - occurrences_object + assert len(new_objects_pdf.index) == len(object_pdf.index) - 1 # Assert the filtered ID is no longer in the objects. assert valid_source_id not in new_objects_pdf.index.values @@ -1136,18 +1172,29 @@ def test_keep_zeros(parquet_ensemble, legacy): assert parquet_ensemble._object.npartitions == prev_npartitions +@pytest.mark.parametrize( + "data_fixture", + [ + "parquet_ensemble", + "parquet_ensemble_with_divisions", + ], +) @pytest.mark.parametrize("by_band", [True, False]) -@pytest.mark.parametrize("know_divisions", [True, False]) -def test_calc_nobs(parquet_ensemble, by_band, know_divisions): - ens = parquet_ensemble - ens._object = ens._object.drop(["nobs_g", "nobs_r", "nobs_total"], axis=1) +@pytest.mark.parametrize("multi_partition", [True, False]) +def test_calc_nobs(data_fixture, request, by_band, multi_partition): + # Get the Ensemble from a fixture + ens = request.getfixturevalue(data_fixture) - if know_divisions: - ens._object = ens._object.reset_index().set_index(ens._id_col) - assert ens._object.known_divisions + if multi_partition: + ens._source = ens._source.repartition(3) + + # Drop the existing nobs columns + ens._object = ens._object.drop(["nobs_g", "nobs_r", "nobs_total"], axis=1) + # Calculate nobs ens.calc_nobs(by_band) + # Check that things turned out as we expect lc = ens._object.loc[88472935274829959].compute() if by_band: @@ -1158,16 +1205,46 @@ def test_calc_nobs(parquet_ensemble, by_band, know_divisions): assert "nobs_total" in ens._object.columns assert lc["nobs_total"].values[0] == 499 + # Make sure that if divisions were set previously, they are preserved + if data_fixture == "parquet_ensemble_with_divisions": + assert ens._object.known_divisions + assert ens._source.known_divisions + -def test_prune(parquet_ensemble): +@pytest.mark.parametrize( + "data_fixture", + [ + "parquet_ensemble", + "parquet_ensemble_with_divisions", + ], +) +@pytest.mark.parametrize("generate_nobs", [False, True]) +def test_prune(data_fixture, request, generate_nobs): """ Test that ensemble.prune() appropriately filters the dataframe """ + + # Get the Ensemble from a fixture + parquet_ensemble = request.getfixturevalue(data_fixture) + threshold = 10 - parquet_ensemble.prune(threshold) + # Generate the nobs cols from within prune + if generate_nobs: + # Drop the existing nobs columns + parquet_ensemble._object = parquet_ensemble._object.drop(["nobs_g", "nobs_r", "nobs_total"], axis=1) + parquet_ensemble.prune(threshold) + + # Use an existing column + else: + parquet_ensemble.prune(threshold, col_name="nobs_total") assert not np.any(parquet_ensemble._object["nobs_total"].values < threshold) + # Make sure that if divisions were set previously, they are preserved + if data_fixture == "parquet_ensemble_with_divisions": + assert parquet_ensemble._source.known_divisions + assert parquet_ensemble._object.known_divisions + def test_query(dask_client): ens = Ensemble(client=dask_client) @@ -1517,6 +1594,7 @@ def test_bin_sources_two_days(dask_client): "data_fixture", [ "parquet_ensemble", + "parquet_ensemble_with_divisions", "parquet_ensemble_without_client", ], ) @@ -1547,6 +1625,10 @@ def test_batch(data_fixture, request, use_map, on): assert isinstance(tracked_result, EnsembleSeries) assert result is tracked_result + # Make sure that divisions information is propagated if known + if parquet_ensemble._source.known_divisions and parquet_ensemble._object.known_divisions: + assert result.known_divisions + result = result.compute() if on is None: @@ -1681,25 +1763,41 @@ def test_build_index(dask_client): assert result_ids == target +@pytest.mark.parametrize( + "data_fixture", + [ + "parquet_ensemble", + "parquet_ensemble_with_divisions", + ], +) @pytest.mark.parametrize("method", ["size", "length", "loglength"]) @pytest.mark.parametrize("combine", [True, False]) @pytest.mark.parametrize("sthresh", [50, 100]) -def test_sf2(parquet_ensemble, method, combine, sthresh, use_map=False): +def test_sf2(data_fixture, request, method, combine, sthresh, use_map=False): """ Test calling sf2 from the ensemble """ + parquet_ensemble = request.getfixturevalue(data_fixture) arg_container = StructureFunctionArgumentContainer() arg_container.bin_method = method arg_container.combine = combine arg_container.bin_count_target = sthresh - res_sf2 = parquet_ensemble.sf2(argument_container=arg_container, use_map=use_map) + if not combine: + res_sf2 = parquet_ensemble.sf2(argument_container=arg_container, use_map=use_map, compute=False) + else: + res_sf2 = parquet_ensemble.sf2(argument_container=arg_container, use_map=use_map) res_batch = parquet_ensemble.batch(calc_sf2, use_map=use_map, argument_container=arg_container) + if parquet_ensemble._source.known_divisions and parquet_ensemble._object.known_divisions: + if not combine: + assert res_sf2.known_divisions + if combine: assert not res_sf2.equals(res_batch) # output should be different else: + res_sf2 = res_sf2.compute() assert res_sf2.equals(res_batch) # output should be identical diff --git a/tests/tape_tests/test_packaging.py b/tests/tape_tests/test_packaging.py new file mode 100644 index 00000000..ef36cc82 --- /dev/null +++ b/tests/tape_tests/test_packaging.py @@ -0,0 +1,6 @@ +import tape + + +def test_version(): + """Check to see that the version property returns something""" + assert tape.__version__ is not None From 6779ba0b4a3cf655791bfcab82cccf75df74d4a1 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Thu, 30 Nov 2023 17:45:53 -0800 Subject: [PATCH 28/35] Revert "Merge Main into Ensemble Refactor Branch (#304)" This reverts commit 5c847e10bd6d4865a1016f8aa621f75129cf8431. --- src/tape/__init__.py | 1 - src/tape/ensemble.py | 156 ++++++++------------------ tests/tape_tests/conftest.py | 19 ---- tests/tape_tests/test_ensemble.py | 172 +++++++---------------------- tests/tape_tests/test_packaging.py | 6 - 5 files changed, 85 insertions(+), 269 deletions(-) delete mode 100644 tests/tape_tests/test_packaging.py diff --git a/src/tape/__init__.py b/src/tape/__init__.py index 1e9471fa..e2ac94ab 100644 --- a/src/tape/__init__.py +++ b/src/tape/__init__.py @@ -3,4 +3,3 @@ from .ensemble_frame import * # noqa from .timeseries import * # noqa from .ensemble_readers import * # noqa -from ._version import __version__ # noqa diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index 1002dff3..63b25d74 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -13,10 +13,11 @@ from .analysis.feature_extractor import BaseLightCurveFeature, FeatureExtractor from .analysis.structure_function import SF_METHODS from .analysis.structurefunction2 import calc_sf2 -from .ensemble_frame import EnsembleFrame, EnsembleSeries, ObjectFrame, SourceFrame, TapeFrame, TapeObjectFrame, TapeSourceFrame, TapeSeries +from .ensemble_frame import EnsembleFrame, EnsembleSeries, ObjectFrame, SourceFrame, TapeFrame, TapeSeries from .timeseries import TimeSeries from .utils import ColumnMapper +# TODO import from EnsembleFrame...? SOURCE_FRAME_LABEL = "source" OBJECT_FRAME_LABEL = "object" @@ -47,6 +48,7 @@ def __init__(self, client=True, **kwargs): # A unique ID to allocate new result frame labels. self.default_frame_id = 1 + # TODO(wbeebe@uw.edu) Replace self._source and self._object with these self.source = None # Source Table EnsembleFrame self.object = None # Object Table EnsembleFrame @@ -777,68 +779,40 @@ def calc_nobs(self, by_band=False, label="nobs", temporary=True): """ if by_band: + band_counts = ( + self._source.groupby([self._id_col])[self._band_col] # group by each object + .value_counts() # count occurence of each band + .to_frame() # convert series to dataframe + .reset_index() # break up the multiindex + .categorize(columns=[self._band_col]) # retype the band labels as categories + .pivot_table(values=self._band_col, index=self._id_col, columns=self._band_col, aggfunc="sum") + ) # the pivot_table call makes each band_count a column of the id_col row + # repartition the result to align with object if self._object.known_divisions: - # Grab these up front to help out the task graph - id_col = self._id_col - band_col = self._band_col - - # Get the band metadata - unq_bands = np.unique(self._source[band_col]) - meta = {band: float for band in unq_bands} - - # Map the groupby to each partition - band_counts = self._source.map_partitions( - lambda x: x.groupby(id_col)[[band_col]] - .value_counts() - .to_frame() - .reset_index() - .pivot_table(values=band_col, index=id_col, columns=band_col, aggfunc="sum"), - meta=meta, - ).repartition(divisions=self._object.divisions) + self._object.divisions = tuple([None for i in range(self._object.npartitions + 1)]) + band_counts = band_counts.repartition(npartitions=self._object.npartitions) else: - band_counts = ( - self._source.groupby([self._id_col])[self._band_col] # group by each object - .value_counts() # count occurence of each band - .to_frame() # convert series to dataframe - .rename(columns={self._band_col: "counts"}) # rename column - .reset_index() # break up the multiindex - .categorize(columns=[self._band_col]) # retype the band labels as categories - .pivot_table( - values=self._band_col, index=self._id_col, columns=self._band_col, aggfunc="sum" - ) - ) # the pivot_table call makes each band_count a column of the id_col row - band_counts = band_counts.repartition(npartitions=self._object.npartitions) # short-hand for calculating nobs_total band_counts["total"] = band_counts[list(band_counts.columns)].sum(axis=1) bands = band_counts.columns.values - self._object = self._object.assign( - **{label + "_" + str(band): band_counts[band] for band in bands} - ) + self._object = self._object.assign(**{label + "_" + band: band_counts[band] for band in bands}) if temporary: - self._object_temp.extend(label + "_" + str(band) for band in bands) + self._object_temp.extend(label + "_" + band for band in bands) else: - if self._object.known_divisions and self._source.known_divisions: - # Grab these up front to help out the task graph - id_col = self._id_col - band_col = self._band_col - - # Map the groupby to each partition - counts = self._source.map_partitions( - lambda x: x.groupby([id_col])[[band_col]].aggregate("count") - ).repartition(divisions=self._object.divisions) + counts = self._source.groupby([self._id_col])[[self._band_col]].aggregate("count") + + # repartition the result to align with object + if self._object.known_divisions: + self._object.divisions = tuple([None for i in range(self._object.npartitions + 1)]) + counts = counts.repartition(npartitions=self._object.npartitions) else: - # Just do a groupby on all source - counts = ( - self._source.groupby([self._id_col])[[self._band_col]] - .aggregate("count") - .repartition(npartitions=self._object.npartitions) - ) + counts = counts.repartition(npartitions=self._object.npartitions) self._object = self._object.assign(**{label + "_total": counts[self._band_col]}) @@ -875,7 +849,8 @@ def prune(self, threshold=50, col_name=None): col_name = "nobs_total" # Mask on object table - self = self.query(f"{col_name} >= {threshold}", table="object") + mask = self._object[col_name] >= threshold + self.update_frame(self._object[mask]) self._object.set_dirty(True) # Object table is now dirty @@ -1159,18 +1134,12 @@ def s2n_inter_quartile_range(flux, err): meta=meta, ) - # Inherit divisions if known from source and the resulting index is the id - # Groupby on index should always return a subset that adheres to the same divisions criteria - if self._source.known_divisions and batch.index.name == self._id_col: - batch.divisions = self._source.divisions - if label is not None: if label == "": label = self._generate_frame_label() print(f"Using generated label, {label}, for a batch result.") # Track the result frame under the provided label self.add_frame(batch, label) - if compute: return batch.compute() else: @@ -1274,6 +1243,8 @@ def from_dask_dataframe( The ensemble object with the Dask dataframe data loaded. """ self._load_column_mapper(column_mapper, **kwargs) + + # TODO(wbeebe@uw.edu): Determine most efficient way to convert to SourceFrame/ObjectFrame source_frame = SourceFrame.from_dask_dataframe(source_frame, self) # Set the index of the source frame and save the resulting table @@ -1284,6 +1255,7 @@ def from_dask_dataframe( self.update_frame(self._generate_object_table()) else: + # TODO(wbeebe@uw.edu): Determine most efficient way to convert to SourceFrame/ObjectFrame self.update_frame(ObjectFrame.from_dask_dataframe(object_frame, ensemble=self)) self.update_frame(self._object.set_index(self._id_col, sorted=sorted, sort=sort)) @@ -1298,12 +1270,6 @@ def from_dask_dataframe( elif partition_size: self._source = self._source.repartition(partition_size=partition_size) - # Check that Divisions are established, warn if not. - for name, table in [("object", self._object), ("source", self._source)]: - if not table.known_divisions: - warnings.warn( - f"Divisions for {name} are not set, certain downstream dask operations may fail as a result. We recommend setting the `sort` or `sorted` flags when loading data to establish division information." - ) return self def from_hipscat(self, dir, source_subdir="source", object_subdir="object", column_mapper=None, **kwargs): @@ -1498,10 +1464,7 @@ def from_parquet( columns.append(self._provenance_col) # Read in the source parquet file(s) - # Index is set False so that we can set it with a future set_index call - # This has the advantage of letting Dask set partition boundaries based - # on the divisions between the sources of different objects. - source = SourceFrame.from_parquet(source_file, index=False, columns=columns, ensemble=self) + source = SourceFrame.from_parquet(source_file, index=self._id_col, columns=columns, ensemble=self) # Generate a provenance column if not provided if self._provenance_col is None: @@ -1511,9 +1474,7 @@ def from_parquet( object = None if object_file: # Read in the object file(s) - # Index is False so that we can set it with a future set_index call - # More meaningful for source than object but parity seems good here - object = ObjectFrame.from_parquet(object_file, index=False, ensemble=self) + object = ObjectFrame.from_parquet(object_file, index=self._id_col, ensemble=self) return self.from_dask_dataframe( source_frame=source, object_frame=object, @@ -1699,7 +1660,13 @@ def convert_flux_to_mag(self, zero_point, zp_form="mag", out_col_name=None, flux def _generate_object_table(self): """Generate an empty object table from the source table.""" - res = self._source.map_partitions(lambda x: TapeObjectFrame(index=x.index.unique())) + sor_idx = self._source.index.unique() + obj_df = pd.DataFrame(index=sor_idx) + + # Convert the resulting dataframe into an ObjectFrame + # TODO(wbeebe): Switch for a cleaner loading fucnction + res = ObjectFrame.from_dask_dataframe( + dd.from_pandas(obj_df, npartitions=int(np.ceil(self._source.npartitions / 100))), ensemble=self) return res @@ -1752,20 +1719,9 @@ def _sync_tables(self): if self._object.is_dirty(): # Sync Object to Source; remove any missing objects from source - - if self._object.known_divisions and self._source.known_divisions: - # Lazily Create an empty object table (just index) for joining - empty_obj = self._object.map_partitions(lambda x: TapeObjectFrame(index=x.index)) - if type(empty_obj) != type(self._object): - raise ValueError("Bad type for empty_obj: " + str(type(empty_obj))) - - # Join source onto the empty object table to align - self.update_frame(self._source.join(empty_obj, how="inner")) - else: - warnings.warn("Divisions are not known, syncing using a non-lazy method.") - obj_idx = list(self._object.index.compute()) - self.update_frame(self._source.map_partitions(lambda x: x[x.index.isin(obj_idx)])) - self.update_frame(self._source.persist()) # persist the source frame + obj_idx = list(self._object.index.compute()) + self.update_frame(self._source.map_partitions(lambda x: x[x.index.isin(obj_idx)])) + self.update_frame(self._source.persist()) # persist the source frame # Drop Temporary Source Columns on Sync if len(self._source_temp): @@ -1775,20 +1731,10 @@ def _sync_tables(self): if self._source.is_dirty(): # not elif if not self.keep_empty_objects: - if self._object.known_divisions and self._source.known_divisions: - # Lazily Create an empty source table (just unique indexes) for joining - empty_src = self._source.map_partitions(lambda x: TapeSourceFrame(index=x.index.unique())) - if type(empty_src) != type(self._source): - raise ValueError("Bad type for empty_src: " + str(type(empty_src))) - - # Join object onto the empty unique source table to align - self.update_frame(self._object.join(empty_src, how="inner")) - else: - warnings.warn("Divisions are not known, syncing using a non-lazy method.") - # Sync Source to Object; remove any objects that do not have sources - sor_idx = list(self._source.index.unique().compute()) - self.update_frame(self._object.map_partitions(lambda x: x[x.index.isin(sor_idx)])) - self.update_frame(self._object.persist()) # persist the object frame + # Sync Source to Object; remove any objects that do not have sources + sor_idx = list(self._source.index.unique().compute()) + self.update_frame(self._object.map_partitions(lambda x: x[x.index.isin(sor_idx)])) + self.update_frame(self._object.persist()) # persist the object frame # Drop Temporary Object Columns on Sync if len(self._object_temp): @@ -1888,7 +1834,7 @@ def _build_index(self, obj_id, band): index = pd.MultiIndex.from_tuples(tuples, names=["object_id", "band", "index"]) return index - def sf2(self, sf_method="basic", argument_container=None, use_map=True, compute=True): + def sf2(self, sf_method="basic", argument_container=None, use_map=True): """Wrapper interface for calling structurefunction2 on the ensemble Parameters @@ -1930,17 +1876,11 @@ def sf2(self, sf_method="basic", argument_container=None, use_map=True, compute= self._source.index, argument_container=argument_container, ) - + return result else: - result = self.batch( - calc_sf2, use_map=use_map, argument_container=argument_container, compute=compute - ) + result = self.batch(calc_sf2, use_map=use_map, argument_container=argument_container) - # Inherit divisions information if known - if self._source.known_divisions and self._object.known_divisions: - result.divisions = self._source.divisions - - return result + return result def _translate_meta(self, meta): """Translates Dask-style meta into a TapeFrame or TapeSeries object. diff --git a/tests/tape_tests/conftest.py b/tests/tape_tests/conftest.py index c0af84c3..e416a04a 100644 --- a/tests/tape_tests/conftest.py +++ b/tests/tape_tests/conftest.py @@ -270,25 +270,6 @@ def parquet_ensemble(dask_client): return ens -# pylint: disable=redefined-outer-name -@pytest.fixture -def parquet_ensemble_with_divisions(dask_client): - """Create an Ensemble from parquet data.""" - ens = Ensemble(client=dask_client) - ens.from_parquet( - "tests/tape_tests/data/source/test_source.parquet", - "tests/tape_tests/data/object/test_object.parquet", - id_col="ps1_objid", - time_col="midPointTai", - band_col="filterName", - flux_col="psFlux", - err_col="psFluxErr", - sort=True, - ) - - return ens - - # pylint: disable=redefined-outer-name @pytest.fixture def parquet_ensemble_from_source(dask_client): diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index c36d5dd9..89fb2dbc 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -32,7 +32,6 @@ def test_with_client(): "data_fixture", [ "parquet_ensemble", - "parquet_ensemble_with_divisions", "parquet_ensemble_without_client", "parquet_ensemble_from_source", "parquet_ensemble_from_hipscat", @@ -62,11 +61,6 @@ def test_parquet_construction(data_fixture, request): assert parquet_ensemble._source is not None assert parquet_ensemble._object is not None - # Make sure divisions are set - if data_fixture == "parquet_ensemble_with_divisions": - assert parquet_ensemble._source.known_divisions - assert parquet_ensemble._object.known_divisions - # Check that the data is not empty. obj, source = parquet_ensemble.compute() assert len(source) == 2000 @@ -729,21 +723,12 @@ def test_update_column_map(dask_client): assert cmap_2.map["provenance_col"] == "p" -@pytest.mark.parametrize( - "data_fixture", - [ - "parquet_ensemble", - "parquet_ensemble_with_divisions", - ], -) @pytest.mark.parametrize("legacy", [True, False]) -def test_sync_tables(data_fixture, request, legacy): +def test_sync_tables(parquet_ensemble, legacy): """ Test that _sync_tables works as expected, using Ensemble-level APIs when `legacy` is `True`, and EsnembleFrame APIs when `legacy` is `False`. """ - parquet_ensemble = request.getfixturevalue(data_fixture) - if legacy: assert len(parquet_ensemble.compute("object")) == 15 assert len(parquet_ensemble.compute("source")) == 2000 @@ -759,16 +744,24 @@ def test_sync_tables(data_fixture, request, legacy): else: assert len(parquet_ensemble.object.compute()) == 5 + # Replace the maximum flux value with a NaN so that we will have a row to drop. + max_flux = max(parquet_ensemble.source[parquet_ensemble._flux_col]) + parquet_ensemble.source[parquet_ensemble._flux_col] = parquet_ensemble.source[ + parquet_ensemble._flux_col].apply( + lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float) + ) if legacy: parquet_ensemble.dropna(table="source") else: parquet_ensemble.source.dropna().update_ensemble() assert parquet_ensemble.source.is_dirty() # Dropna should set the source dirty flag - # Drop a whole object from Source to test that the object is dropped in the object table - dropped_obj_id = 88472935274829959 + # Drop a whole object to test that the object is dropped in the object table if legacy: - parquet_ensemble.query(f"{parquet_ensemble._id_col} != {dropped_obj_id}", table="source") + parquet_ensemble.query(f"{parquet_ensemble._id_col} != 88472935274829959", table="source") + assert parquet_ensemble.source.is_dirty() + parquet_ensemble.compute() + assert not parquet_ensemble.source.is_dirty() else: filtered_src = parquet_ensemble.source.query(f"{parquet_ensemble._id_col} != 88472935274829959") @@ -778,16 +771,12 @@ def test_sync_tables(data_fixture, request, legacy): filtered_src.compute() assert parquet_ensemble.source.is_dirty() - # Update the ensemble to use the filtered source. + # After updating the ensemble validate that a sync occurred and the table is no longer dirty. filtered_src.update_ensemble() + filtered_src.compute() # Now equivalent to parquet_ensemble.source.compute() + assert not parquet_ensemble.source.is_dirty() - # Verify that the object ID we removed from the source table is present in the object table - assert dropped_obj_id in parquet_ensemble._object.index.compute().values - - # Perform an operation which should trigger syncing both tables. - parquet_ensemble.compute() - - # Both tables should have the expected number of rows after a sync + # both tables should have the expected number of rows after a sync if legacy: assert len(parquet_ensemble.compute("object")) == 4 assert len(parquet_ensemble.compute("source")) == 1063 @@ -795,18 +784,9 @@ def test_sync_tables(data_fixture, request, legacy): assert len(parquet_ensemble.object.compute()) == 4 assert len(parquet_ensemble.source.compute()) == 1063 - # Validate that the filtered object has been removed from both tables. - assert dropped_obj_id not in parquet_ensemble.source.index.compute().values - assert dropped_obj_id not in parquet_ensemble.object.index.compute().values - - # Dirty flags should be unset after sync - assert not parquet_ensemble.object_dirty - assert not parquet_ensemble.source_dirty - - # Make sure that divisions are preserved - if data_fixture == "parquet_ensemble_with_divisions": - assert parquet_ensemble.source.known_divisions - assert parquet_ensemble.object.known_divisions + # dirty flags should be unset after sync + assert not parquet_ensemble._object.is_dirty() + assert not parquet_ensemble._source.is_dirty() @pytest.mark.parametrize("legacy", [True, False]) @@ -1046,19 +1026,10 @@ def test_temporary_cols(parquet_ensemble): assert "f2" not in ens._source.columns -@pytest.mark.parametrize( - "data_fixture", - [ - "parquet_ensemble", - "parquet_ensemble_with_divisions", - ], -) @pytest.mark.parametrize("legacy", [True, False]) -def test_dropna(data_fixture, request, legacy): +def test_dropna(parquet_ensemble, legacy): """Tests dropna, using Ensemble.dropna when `legacy` is `True`, and EnsembleFrame.dropna when `legacy` is `False`.""" - parquet_ensemble = request.getfixturevalue(data_fixture) - # Try passing in an unrecognized 'table' parameter and verify an exception is thrown with pytest.raises(ValueError): parquet_ensemble.dropna(table="banana") @@ -1091,10 +1062,6 @@ def test_dropna(data_fixture, request, legacy): parquet_ensemble.source.dropna().update_ensemble() assert len(parquet_ensemble._source.compute().index) == source_length - occurrences_source - if data_fixture == "parquet_ensemble_with_divisions": - # divisions should be preserved - assert parquet_ensemble._source.known_divisions - # Now test dropping na from 'object' table # Sync the tables parquet_ensemble._sync_tables() @@ -1110,8 +1077,10 @@ def test_dropna(data_fixture, request, legacy): parquet_ensemble.object.dropna().update_ensemble() assert len(parquet_ensemble.object.compute().index) == object_length - # select an id from the object table + # get a valid object id and set at least two occurences of that id in the object table valid_object_id = object_pdf.index.values[1] + object_pdf.index.values[0] = valid_object_id + occurrences_object = len(object_pdf.loc[valid_object_id].values) # Set the nobs_g values for one object to NaN so we can drop it. # We do this on the instantiated object (pdf) and convert it back into a @@ -1119,19 +1088,14 @@ def test_dropna(data_fixture, request, legacy): object_pdf.loc[valid_object_id, parquet_ensemble._object.columns[0]] = pd.NA parquet_ensemble.update_frame(ObjectFrame.from_tapeframe(TapeObjectFrame(object_pdf), label="object", npartitions=1)) - # Try dropping NaNs from object and confirm that we dropped a row + # Try dropping NaNs from object and confirm that we did. if legacy: parquet_ensemble.dropna(table="object") else: parquet_ensemble.object.dropna().update_ensemble() - assert len(parquet_ensemble.object.compute().index) == object_length - 1 - - if data_fixture == "parquet_ensemble_with_divisions": - # divisions should be preserved - assert parquet_ensemble._object.known_divisions - + assert len(parquet_ensemble.object.compute().index) == object_length - occurrences_object new_objects_pdf = parquet_ensemble.object.compute() - assert len(new_objects_pdf.index) == len(object_pdf.index) - 1 + assert len(new_objects_pdf.index) == len(object_pdf.index) - occurrences_object # Assert the filtered ID is no longer in the objects. assert valid_source_id not in new_objects_pdf.index.values @@ -1172,29 +1136,18 @@ def test_keep_zeros(parquet_ensemble, legacy): assert parquet_ensemble._object.npartitions == prev_npartitions -@pytest.mark.parametrize( - "data_fixture", - [ - "parquet_ensemble", - "parquet_ensemble_with_divisions", - ], -) @pytest.mark.parametrize("by_band", [True, False]) -@pytest.mark.parametrize("multi_partition", [True, False]) -def test_calc_nobs(data_fixture, request, by_band, multi_partition): - # Get the Ensemble from a fixture - ens = request.getfixturevalue(data_fixture) - - if multi_partition: - ens._source = ens._source.repartition(3) - - # Drop the existing nobs columns +@pytest.mark.parametrize("know_divisions", [True, False]) +def test_calc_nobs(parquet_ensemble, by_band, know_divisions): + ens = parquet_ensemble ens._object = ens._object.drop(["nobs_g", "nobs_r", "nobs_total"], axis=1) - # Calculate nobs + if know_divisions: + ens._object = ens._object.reset_index().set_index(ens._id_col) + assert ens._object.known_divisions + ens.calc_nobs(by_band) - # Check that things turned out as we expect lc = ens._object.loc[88472935274829959].compute() if by_band: @@ -1205,46 +1158,16 @@ def test_calc_nobs(data_fixture, request, by_band, multi_partition): assert "nobs_total" in ens._object.columns assert lc["nobs_total"].values[0] == 499 - # Make sure that if divisions were set previously, they are preserved - if data_fixture == "parquet_ensemble_with_divisions": - assert ens._object.known_divisions - assert ens._source.known_divisions - -@pytest.mark.parametrize( - "data_fixture", - [ - "parquet_ensemble", - "parquet_ensemble_with_divisions", - ], -) -@pytest.mark.parametrize("generate_nobs", [False, True]) -def test_prune(data_fixture, request, generate_nobs): +def test_prune(parquet_ensemble): """ Test that ensemble.prune() appropriately filters the dataframe """ - - # Get the Ensemble from a fixture - parquet_ensemble = request.getfixturevalue(data_fixture) - threshold = 10 - # Generate the nobs cols from within prune - if generate_nobs: - # Drop the existing nobs columns - parquet_ensemble._object = parquet_ensemble._object.drop(["nobs_g", "nobs_r", "nobs_total"], axis=1) - parquet_ensemble.prune(threshold) - - # Use an existing column - else: - parquet_ensemble.prune(threshold, col_name="nobs_total") + parquet_ensemble.prune(threshold) assert not np.any(parquet_ensemble._object["nobs_total"].values < threshold) - # Make sure that if divisions were set previously, they are preserved - if data_fixture == "parquet_ensemble_with_divisions": - assert parquet_ensemble._source.known_divisions - assert parquet_ensemble._object.known_divisions - def test_query(dask_client): ens = Ensemble(client=dask_client) @@ -1594,7 +1517,6 @@ def test_bin_sources_two_days(dask_client): "data_fixture", [ "parquet_ensemble", - "parquet_ensemble_with_divisions", "parquet_ensemble_without_client", ], ) @@ -1625,10 +1547,6 @@ def test_batch(data_fixture, request, use_map, on): assert isinstance(tracked_result, EnsembleSeries) assert result is tracked_result - # Make sure that divisions information is propagated if known - if parquet_ensemble._source.known_divisions and parquet_ensemble._object.known_divisions: - assert result.known_divisions - result = result.compute() if on is None: @@ -1763,41 +1681,25 @@ def test_build_index(dask_client): assert result_ids == target -@pytest.mark.parametrize( - "data_fixture", - [ - "parquet_ensemble", - "parquet_ensemble_with_divisions", - ], -) @pytest.mark.parametrize("method", ["size", "length", "loglength"]) @pytest.mark.parametrize("combine", [True, False]) @pytest.mark.parametrize("sthresh", [50, 100]) -def test_sf2(data_fixture, request, method, combine, sthresh, use_map=False): +def test_sf2(parquet_ensemble, method, combine, sthresh, use_map=False): """ Test calling sf2 from the ensemble """ - parquet_ensemble = request.getfixturevalue(data_fixture) arg_container = StructureFunctionArgumentContainer() arg_container.bin_method = method arg_container.combine = combine arg_container.bin_count_target = sthresh - if not combine: - res_sf2 = parquet_ensemble.sf2(argument_container=arg_container, use_map=use_map, compute=False) - else: - res_sf2 = parquet_ensemble.sf2(argument_container=arg_container, use_map=use_map) + res_sf2 = parquet_ensemble.sf2(argument_container=arg_container, use_map=use_map) res_batch = parquet_ensemble.batch(calc_sf2, use_map=use_map, argument_container=arg_container) - if parquet_ensemble._source.known_divisions and parquet_ensemble._object.known_divisions: - if not combine: - assert res_sf2.known_divisions - if combine: assert not res_sf2.equals(res_batch) # output should be different else: - res_sf2 = res_sf2.compute() assert res_sf2.equals(res_batch) # output should be identical diff --git a/tests/tape_tests/test_packaging.py b/tests/tape_tests/test_packaging.py deleted file mode 100644 index ef36cc82..00000000 --- a/tests/tape_tests/test_packaging.py +++ /dev/null @@ -1,6 +0,0 @@ -import tape - - -def test_version(): - """Check to see that the version property returns something""" - assert tape.__version__ is not None From 494eefc2347f0c3ef2df23024135d7f8c5ebeeb0 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Mon, 4 Dec 2023 11:37:59 -0800 Subject: [PATCH 29/35] Fix linting --- src/tape/__init__.py | 2 +- src/tape/ensemble.py | 125 ++++++++++----------- src/tape/ensemble_frame.py | 215 +++++++++++++++++++++---------------- 3 files changed, 189 insertions(+), 153 deletions(-) diff --git a/src/tape/__init__.py b/src/tape/__init__.py index 1e9471fa..46eba57c 100644 --- a/src/tape/__init__.py +++ b/src/tape/__init__.py @@ -1,6 +1,6 @@ from .analysis import * # noqa from .ensemble import * # noqa -from .ensemble_frame import * # noqa +from .ensemble_frame import * # noqa from .timeseries import * # noqa from .ensemble_readers import * # noqa from ._version import __version__ # noqa diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index 1002dff3..f0cb4540 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -13,14 +13,24 @@ from .analysis.feature_extractor import BaseLightCurveFeature, FeatureExtractor from .analysis.structure_function import SF_METHODS from .analysis.structurefunction2 import calc_sf2 -from .ensemble_frame import EnsembleFrame, EnsembleSeries, ObjectFrame, SourceFrame, TapeFrame, TapeObjectFrame, TapeSourceFrame, TapeSeries +from .ensemble_frame import ( + EnsembleFrame, + EnsembleSeries, + ObjectFrame, + SourceFrame, + TapeFrame, + TapeObjectFrame, + TapeSourceFrame, + TapeSeries, +) from .timeseries import TimeSeries from .utils import ColumnMapper SOURCE_FRAME_LABEL = "source" OBJECT_FRAME_LABEL = "object" -DEFAULT_FRAME_LABEL = "result" # A base default label for an Ensemble's result frames. +DEFAULT_FRAME_LABEL = "result" # A base default label for an Ensemble's result frames. + class Ensemble: """Ensemble object is a collection of light curve ids""" @@ -42,13 +52,13 @@ def __init__(self, client=True, **kwargs): self._source = None # Source Table self._object = None # Object Table - self.frames = {} # Frames managed by this Ensemble, keyed by label + self.frames = {} # Frames managed by this Ensemble, keyed by label # A unique ID to allocate new result frame labels. self.default_frame_id = 1 - self.source = None # Source Table EnsembleFrame - self.object = None # Object Table EnsembleFrame + self.source = None # Source Table EnsembleFrame + self.object = None # Object Table EnsembleFrame self._source_temp = [] # List of temporary columns in Source self._object_temp = [] # List of temporary columns in Object @@ -99,7 +109,7 @@ def add_frame(self, frame, label): frame: `tape.ensemble.EnsembleFrame` The frame object for the Ensemble to track. label: `str` - | The label for the Ensemble to use to track the frame. + | The label for the Ensemble to use to track the frame. Returns ------- @@ -110,13 +120,9 @@ def add_frame(self, frame, label): ValueError if the label is "source", "object", or already tracked by the Ensemble. """ if label == SOURCE_FRAME_LABEL or label == OBJECT_FRAME_LABEL: - raise ValueError( - f"Unable to add frame with reserved label " f"'{label}'" - ) + raise ValueError(f"Unable to add frame with reserved label " f"'{label}'") if label in self.frames: - raise ValueError( - f"Unable to add frame: a frame with label " f"'{label}'" f"is in the Ensemble." - ) + raise ValueError(f"Unable to add frame: a frame with label " f"'{label}'" f"is in the Ensemble.") # Assign the frame to the requested tracking label. frame.label = label # Update the ensemble to track this labeled frame. @@ -142,14 +148,11 @@ def update_frame(self, frame): but uses the reserved labels. """ if frame.label is None: - raise ValueError( - f"Unable to update frame with no populated `EnsembleFrame.label`." - ) + raise ValueError(f"Unable to update frame with no populated `EnsembleFrame.label`.") if isinstance(frame, SourceFrame) or isinstance(frame, ObjectFrame): expected_label = SOURCE_FRAME_LABEL if isinstance(frame, SourceFrame) else OBJECT_FRAME_LABEL if frame.label != expected_label: - raise ValueError(f"Unable to update frame with reserved label " f"'{frame.label}'" - ) + raise ValueError(f"Unable to update frame with reserved label " f"'{frame.label}'") if isinstance(frame, SourceFrame): self._source = frame self.source = frame @@ -161,7 +164,7 @@ def update_frame(self, frame): frame.ensemble = self self.frames[frame.label] = frame return self - + def drop_frame(self, label): """Drops a frame tracked by the Ensemble. @@ -180,13 +183,9 @@ def drop_frame(self, label): KeyError if the label is not tracked by the Ensemble. """ if label == SOURCE_FRAME_LABEL or label == OBJECT_FRAME_LABEL: - raise ValueError( - f"Unable to drop frame with reserved label " f"'{label}'" - ) + raise ValueError(f"Unable to drop frame with reserved label " f"'{label}'") if label not in self.frames: - raise KeyError( - f"Unable to drop frame: no frame with label " f"'{label}'" f"is in the Ensemble." - ) + raise KeyError(f"Unable to drop frame: no frame with label " f"'{label}'" f"is in the Ensemble.") del self.frames[label] return self @@ -209,9 +208,9 @@ def select_frame(self, label): if label not in self.frames: raise KeyError( f"Unable to select frame: no frame with label" f"'{label}'" f" is in the Ensemble." - ) + ) return self.frames[label] - + def frame_info(self, labels=None, verbose=True, memory_usage=True, **kwargs): """Wrapper for calling dask.dataframe.DataFrame.info() on frames tracked by the Ensemble. @@ -242,14 +241,14 @@ def frame_info(self, labels=None, verbose=True, memory_usage=True, **kwargs): if label not in self.frames: raise KeyError( f"Unable to get frame info: no frame with label " f"'{label}'" f" is in the Ensemble." - ) + ) print(label, "Frame") print(self.frames[label].info(verbose=verbose, memory_usage=memory_usage, **kwargs)) def _generate_frame_label(self): - """ Generates a new unique label for a result frame. """ + """Generates a new unique label for a result frame.""" result = DEFAULT_FRAME_LABEL + "_" + str(self.default_frame_id) - self.default_frame_id += 1 # increment to guarantee uniqueness + self.default_frame_id += 1 # increment to guarantee uniqueness while result in self.frames: # If the generated label has been taken by a user, increment again. # In most workflows, we expect the number of frames to be O(100) so it's unlikely for @@ -349,7 +348,7 @@ def insert_sources( self.update_frame(self._source.repartition(divisions=prev_div)) elif self._source.npartitions != prev_num: self._source = self._source.repartition(npartitions=prev_num) - + return self def client_info(self): @@ -877,7 +876,7 @@ def prune(self, threshold=50, col_name=None): # Mask on object table self = self.query(f"{col_name} >= {threshold}", table="object") - self._object.set_dirty(True) # Object table is now dirty + self._object.set_dirty(True) # Object table is now dirty return self @@ -1016,7 +1015,9 @@ def bin_sources( aggr_funs[key] = custom_aggr[key] # Group the columns by id, band, and time bucket and aggregate. - self.update_frame(self._source.groupby([self._id_col, self._band_col, tmp_time_col]).aggregate(aggr_funs)) + self.update_frame( + self._source.groupby([self._id_col, self._band_col, tmp_time_col]).aggregate(aggr_funs) + ) # Fix the indices and remove the temporary column. self.update_frame(self._source.reset_index().set_index(self._id_col).drop(tmp_time_col, axis=1)) @@ -1064,10 +1065,10 @@ def batch(self, func, *args, meta=None, use_map=True, compute=True, on=None, lab source or object tables. For TAPE and `light-curve` functions this is populated automatically. label: 'str', optional - If provided the ensemble will use this label to track the result + If provided the ensemble will use this label to track the result dataframe. If not provided, a label of the from "result_{x}" where x is a monotonically increasing integer is generated. If `None`, - the result frame will not be tracked. + the result frame will not be tracked. **kwargs: Additional optional parameters passed for the selected function @@ -1165,7 +1166,7 @@ def s2n_inter_quartile_range(flux, err): batch.divisions = self._source.divisions if label is not None: - if label == "": + if label == "": label = self._generate_frame_label() print(f"Using generated label, {label}, for a batch result.") # Track the result frame under the provided label @@ -1277,8 +1278,7 @@ def from_dask_dataframe( source_frame = SourceFrame.from_dask_dataframe(source_frame, self) # Set the index of the source frame and save the resulting table - self.update_frame( - source_frame.set_index(self._id_col, drop=True, sorted=sorted, sort=sort)) + self.update_frame(source_frame.set_index(self._id_col, drop=True, sorted=sorted, sort=sort)) if object_frame is None: # generate an indexed object table from source self.update_frame(self._generate_object_table()) @@ -1669,34 +1669,40 @@ def convert_flux_to_mag(self, zero_point, zp_form="mag", out_col_name=None, flux if zp_form == "flux": # mag = -2.5*np.log10(flux/zp) if isinstance(zero_point, str): - self.update_frame(self._source.assign( - **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / x[zero_point])} - )) + self.update_frame( + self._source.assign( + **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / x[zero_point])} + ) + ) else: - self.update_frame(self._source.assign( - **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / zero_point)} - )) + self.update_frame( + self._source.assign(**{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / zero_point)}) + ) elif zp_form == "magnitude" or zp_form == "mag": # mag = -2.5*np.log10(flux) + zp if isinstance(zero_point, str): - self.update_frame(self._source.assign( - **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + x[zero_point]} - )) + self.update_frame( + self._source.assign( + **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + x[zero_point]} + ) + ) else: - self.update_frame(self._source.assign( - **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + zero_point} - )) + self.update_frame( + self._source.assign(**{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + zero_point}) + ) else: raise ValueError(f"{zp_form} is not a valid zero_point format.") # Calculate Errors if err_col is not None: - self.update_frame(self._source.assign( - **{out_col_name + "_err": lambda x: (2.5 / np.log(10)) * (x[err_col] / x[flux_col])} - )) + self.update_frame( + self._source.assign( + **{out_col_name + "_err": lambda x: (2.5 / np.log(10)) * (x[err_col] / x[flux_col])} + ) + ) return self - + def _generate_object_table(self): """Generate an empty object table from the source table.""" res = self._source.map_partitions(lambda x: TapeObjectFrame(index=x.index.unique())) @@ -1704,7 +1710,7 @@ def _generate_object_table(self): return res def _lazy_sync_tables_from_frame(self, frame): - """Call the sync operation for the frame only if the + """Call the sync operation for the frame only if the table being modified (`frame`) needs to be synced. Does nothing in the case that only the table to be modified is dirty or if it is not the object or source frame for this @@ -1957,20 +1963,17 @@ def _translate_meta(self, meta): """ if isinstance(meta, TapeFrame) or isinstance(meta, TapeSeries): return meta - + # If the meta is not a DataFrame or Series, have Dask attempt translate the meta into an # appropriate Pandas object. - meta_object = meta + meta_object = meta if not (isinstance(meta_object, pd.DataFrame) or isinstance(meta_object, pd.Series)): meta_object = dd.backends.make_meta_object(meta_object) - + # Convert meta_object into the appropriate TAPE extension. if isinstance(meta_object, pd.DataFrame): return TapeFrame(meta_object) elif isinstance(meta_object, pd.Series): return TapeSeries(meta_object) else: - raise ValueError( - "Unsupported Meta: " + str(meta) + "\nTry a Pandas DataFrame or Series instead." - ) - \ No newline at end of file + raise ValueError("Unsupported Meta: " + str(meta) + "\nTry a Pandas DataFrame or Series instead.") diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index 38a57074..58129c7a 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -17,11 +17,12 @@ from functools import partial from dask.dataframe.io.parquet.arrow import ( - ArrowDatasetEngine as DaskArrowDatasetEngine, - ) + ArrowDatasetEngine as DaskArrowDatasetEngine, +) + +SOURCE_FRAME_LABEL = "source" # Reserved label for source table +OBJECT_FRAME_LABEL = "object" # Reserved label for object table. -SOURCE_FRAME_LABEL = "source" # Reserved label for source table -OBJECT_FRAME_LABEL = "object" # Reserved label for object table. class TapeArrowEngine(DaskArrowDatasetEngine): """ @@ -52,10 +53,11 @@ def _create_dd_meta(cls, dataset_info, use_nullable_dtypes=False): meta = cls._creates_meta(meta, schema) return meta + class TapeSourceArrowEngine(TapeArrowEngine): """ Barebones subclass of TapeArrowEngine for assigning the meta when loading from a parquet file - of source data. + of source data. """ @classmethod @@ -65,10 +67,11 @@ def _creates_meta(cls, meta, schema): """ return TapeSourceFrame(meta) + class TapeObjectArrowEngine(TapeArrowEngine): """ Barebones subclass of TapeArrowEngine for assigning the meta when loading from a parquet file - of object data. + of object data. """ @classmethod @@ -78,21 +81,22 @@ def _creates_meta(cls, meta, schema): """ return TapeObjectFrame(meta) + class _Frame(dd.core._Frame): """Base class for extensions of Dask Dataframes that track additional Ensemble-related metadata.""" def __init__(self, dsk, name, meta, divisions, label=None, ensemble=None): # We define relevant object fields before super().__init__ since that call may lead to a # map_partitions call which will assume these fields exist. - self.label = label # A label used by the Ensemble to identify this frame. - self.ensemble = ensemble # The Ensemble object containing this frame. - self.dirty = False # True if the underlying data is out of sync with the Ensemble + self.label = label # A label used by the Ensemble to identify this frame. + self.ensemble = ensemble # The Ensemble object containing this frame. + self.dirty = False # True if the underlying data is out of sync with the Ensemble super().__init__(dsk, name, meta, divisions) def is_dirty(self): return self.dirty - + def set_dirty(self, dirty): self.dirty = dirty @@ -123,7 +127,7 @@ def _propagate_metadata(self, new_frame): def copy(self): self_copy = super().copy() return self._propagate_metadata(self_copy) - + def assign(self, **kwargs): """Assign new columns to a DataFrame. @@ -150,7 +154,7 @@ def assign(self, **kwargs): result = self._propagate_metadata(super().assign(**kwargs)) result.set_dirty(True) return result - + def query(self, expr, **kwargs): """Filter dataframe with complex expression @@ -174,7 +178,7 @@ def query(self, expr, **kwargs): ---------- result: `tape._Frame` The modifed frame - + Notes ----- This is like the sequential version except that this will also happen @@ -190,7 +194,7 @@ def query(self, expr, **kwargs): result = self._propagate_metadata(super().query(expr, **kwargs)) result.set_dirty(True) return result - + def merge(self, right, **kwargs): """Merge the Dataframe with another DataFrame @@ -198,7 +202,7 @@ def merge(self, right, **kwargs): This will merge the two datasets, either on the indices, a certain column in each dataset or the index in one dataset and the column in another. - + Parameters ---------- right: dask.dataframe.DataFrame @@ -285,7 +289,7 @@ def merge(self, right, **kwargs): """ result = super().merge(right, **kwargs) return self._propagate_metadata(result) - + def join(self, other, **kwargs): """Join columns of another DataFrame. Note that if `other` is a different type, we expect the result to have the type of this object regardless of the value @@ -347,12 +351,12 @@ def join(self, other, **kwargs): """ result = super().join(other, **kwargs) return self._propagate_metadata(result) - + def drop(self, labels=None, axis=0, columns=None, errors="raise"): """Drop specified labels from rows or columns. Doc string below derived from dask.dataframe.core - + Remove rows or columns by specifying label names and corresponding axis, or by directly specifying index or column names. When using a multi-index, labels on different levels can be removed by specifying @@ -381,10 +385,12 @@ def drop(self, labels=None, axis=0, columns=None, errors="raise"): Returns the frame or None with the specified index or column labels removed or None if inplace=True. """ - result = self._propagate_metadata(super().drop(labels=labels, axis=axis, columns=columns, errors=errors)) + result = self._propagate_metadata( + super().drop(labels=labels, axis=axis, columns=columns, errors=errors) + ) result.set_dirty(True) return result - + def dropna(self, **kwargs): """ Remove missing values. @@ -420,7 +426,7 @@ def persist(self, **kwargs): """Persist this dask collection into memory Doc string below derived from dask.base - + This turns a lazy Dask collection into a Dask collection with the same metadata, but now with the results fully computed or actively computing in the background. @@ -449,7 +455,7 @@ def persist(self, **kwargs): """ result = super().persist(**kwargs) return self._propagate_metadata(result) - + def set_index( self, other: str | pd.Series, @@ -461,7 +467,6 @@ def set_index( sort: bool = True, **kwargs, ): - """Set the DataFrame index (row labels) using an existing column. Doc string below derived from dask.dataframe.core @@ -532,7 +537,7 @@ def set_index( partition_size: int, optional Desired size of each partitions in bytes. Only used when ``npartitions='auto'`` - + Returns ---------- result: `tape._Frame` @@ -540,7 +545,7 @@ def set_index( """ result = super().set_index(other, drop, sorted, npartitions, divisions, inplace, sort, **kwargs) return self._propagate_metadata(result) - + def map_partitions(self, func, *args, **kwargs): """Apply Python function on each DataFrame partition. @@ -618,7 +623,7 @@ def map_partitions(self, func, *args, **kwargs): # If the output of func is another _Frame, let's propagate any metadata. return self._propagate_metadata(result) return result - + def compute(self, **kwargs): """Compute this Dask collection, returning the underlying dataframe or series. If tracked by an `Ensemble`, the `Ensemble` is informed of this operation and @@ -627,7 +632,7 @@ def compute(self, **kwargs): Doc string below derived from dask.dataframe.DataFrame.compute - This turns a lazy Dask collection into its in-memory equivalent. For example + This turns a lazy Dask collection into its in-memory equivalent. For example a Dask array turns into a NumPy array and a Dask dataframe turns into a Pandas dataframe. The entire dataset must fit into memory before calling this operation. @@ -635,11 +640,11 @@ def compute(self, **kwargs): Parameters ---------- scheduler: `string`, optional - Which scheduler to use like “threads”, “synchronous” or “processes”. + Which scheduler to use like “threads”, “synchronous” or “processes”. If not provided, the default is to check the global settings first, and then fall back to the collection defaults. optimize_graph: `bool`, optional - If True [default], the graph is optimized before computation. + If True [default], the graph is optimized before computation. Otherwise the graph is run as is. This can be useful for debugging. **kwargs: `dict`, optional Extra keywords to forward to the scheduler function. @@ -648,37 +653,42 @@ def compute(self, **kwargs): self.ensemble._lazy_sync_tables_from_frame(self) return super().compute(**kwargs) + class TapeSeries(pd.Series): """A barebones extension of a Pandas series to be used for underlying Ensemble data. - + See https://pandas.pydata.org/docs/development/extending.html#subclassing-pandas-data-structures """ + @property def _constructor(self): return TapeSeries - + @property def _constructor_sliced(self): return TapeSeries - + + class TapeFrame(pd.DataFrame): """A barebones extension of a Pandas frame to be used for underlying Ensemble data. - + See https://pandas.pydata.org/docs/development/extending.html#subclassing-pandas-data-structures """ + @property def _constructor(self): return TapeFrame - + @property def _constructor_expanddim(self): return TapeFrame - - + + class EnsembleSeries(_Frame, dd.core.Series): - """A barebones extension of a Dask Series for Ensemble data. - """ - _partition_type = TapeSeries # Tracks the underlying data type + """A barebones extension of a Dask Series for Ensemble data.""" + + _partition_type = TapeSeries # Tracks the underlying data type + class EnsembleFrame(_Frame, dd.core.DataFrame): """An extension for a Dask Dataframe for data used by a lightcurve Ensemble. @@ -692,7 +702,8 @@ class EnsembleFrame(_Frame, dd.core.DataFrame): data = {...} # Some data you want tracked by the Ensemble ensemble_frame = tape.EnsembleFrame.from_dict(data, label="my_frame", ensemble=ens) """ - _partition_type = TapeFrame # Tracks the underlying data type + + _partition_type = TapeFrame # Tracks the underlying data type def __getitem__(self, key): result = super().__getitem__(key) @@ -702,10 +713,8 @@ def __getitem__(self, key): return result @classmethod - def from_tapeframe( - cls, data, npartitions=None, chunksize=None, sort=True, label=None, ensemble=None - ): - """ Returns an EnsembleFrame constructed from a TapeFrame. + def from_tapeframe(cls, data, npartitions=None, chunksize=None, sort=True, label=None, ensemble=None): + """Returns an EnsembleFrame constructed from a TapeFrame. Parameters ---------- data: `TapeFrame` @@ -730,10 +739,10 @@ def from_tapeframe( result.label = label result.ensemble = ensemble return result - + @classmethod def from_dask_dataframe(cl, df, ensemble=None, label=None): - """ Returns an EnsembleFrame constructed from a Dask dataframe. + """Returns an EnsembleFrame constructed from a Dask dataframe. Parameters ---------- df: `dask.dataframe.DataFrame` or `list` @@ -748,13 +757,13 @@ def from_dask_dataframe(cl, df, ensemble=None, label=None): """ # Create a EnsembleFrame by mapping the partitions to the appropriate meta, TapeFrame # TODO(wbeebe@uw.edu): Determine if there is a better method - result = df.map_partitions(TapeFrame) + result = df.map_partitions(TapeFrame) result.ensemble = ensemble result.label = label return result def update_ensemble(self): - """ Updates the Ensemble linked by the `EnsembelFrame.ensemble` property to track this frame. + """Updates the Ensemble linked by the `EnsembelFrame.ensemble` property to track this frame. Returns result: `tape.Ensemble` @@ -764,14 +773,15 @@ def update_ensemble(self): return None # Update the Ensemble to track this frame and return the ensemble. return self.ensemble.update_frame(self) - - def convert_flux_to_mag(self, - flux_col, - zero_point, - err_col=None, - zp_form="mag", - out_col_name=None, - ): + + def convert_flux_to_mag( + self, + flux_col, + zero_point, + err_col=None, + zp_form="mag", + out_col_name=None, + ): """Converts this EnsembleFrame's flux column into a magnitude column, returning a new EnsembleFrame. @@ -807,14 +817,10 @@ def convert_flux_to_mag(self, result = None if zp_form == "flux": # mag = -2.5*np.log10(flux/zp) - result = self.assign( - **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / x[zero_point])} - ) + result = self.assign(**{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / x[zero_point])}) elif zp_form == "magnitude" or zp_form == "mag": # mag = -2.5*np.log10(flux) + zp - result = self.assign( - **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + x[zero_point]} - ) + result = self.assign(**{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + x[zero_point]}) else: raise ValueError(f"{zp_form} is not a valid zero_point format.") @@ -834,7 +840,7 @@ def from_parquet( columns=None, ensemble=None, ): - """ Returns an EnsembleFrame constructed from loading a parquet file. + """Returns an EnsembleFrame constructed from loading a parquet file. Parameters ---------- path: `str` or `list` @@ -859,47 +865,56 @@ def from_parquet( # Read the parquet file with an engine that will assume the meta is a TapeFrame which Dask will # instantiate as EnsembleFrame via its dispatcher. result = dd.read_parquet( - path, index=index, columns=columns, split_row_groups=True, engine=TapeArrowEngine, + path, + index=index, + columns=columns, + split_row_groups=True, + engine=TapeArrowEngine, ) - result.ensemble=ensemble + result.ensemble = ensemble return result + class TapeSourceFrame(TapeFrame): """A barebones extension of a Pandas frame to be used for underlying Ensemble source data - + See https://pandas.pydata.org/docs/development/extending.html#subclassing-pandas-data-structures """ + @property def _constructor(self): return TapeSourceFrame - + @property def _constructor_expanddim(self): return TapeSourceFrame - + + class TapeObjectFrame(TapeFrame): """A barebones extension of a Pandas frame to be used for underlying Ensemble object data. - + See https://pandas.pydata.org/docs/development/extending.html#subclassing-pandas-data-structures """ + @property def _constructor(self): return TapeObjectFrame - + @property def _constructor_expanddim(self): return TapeObjectFrame + class SourceFrame(EnsembleFrame): - """ A subclass of EnsembleFrame for Source data. """ + """A subclass of EnsembleFrame for Source data.""" - _partition_type = TapeSourceFrame # Tracks the underlying data type + _partition_type = TapeSourceFrame # Tracks the underlying data type def __init__(self, dsk, name, meta, divisions, ensemble=None): super().__init__(dsk, name, meta, divisions) - self.label = SOURCE_FRAME_LABEL # A label used by the Ensemble to identify this frame. - self.ensemble = ensemble # The Ensemble object containing this frame. + self.label = SOURCE_FRAME_LABEL # A label used by the Ensemble to identify this frame. + self.ensemble = ensemble # The Ensemble object containing this frame. def __getitem__(self, key): result = super().__getitem__(key) @@ -916,7 +931,7 @@ def from_parquet( columns=None, ensemble=None, ): - """ Returns a SourceFrame constructed from loading a parquet file. + """Returns a SourceFrame constructed from loading a parquet file. Parameters ---------- path: `str` or `list` @@ -938,20 +953,24 @@ def from_parquet( result: `tape.EnsembleFrame` The constructed EnsembleFrame object. """ - # Read the source parquet file with an engine that will assume the meta is a + # Read the source parquet file with an engine that will assume the meta is a # TapeSourceFrame which tells Dask to instantiate a SourceFrame via its # dispatcher. result = dd.read_parquet( - path, index=index, columns=columns, split_row_groups=True, engine=TapeSourceArrowEngine, + path, + index=index, + columns=columns, + split_row_groups=True, + engine=TapeSourceArrowEngine, ) - result.ensemble=ensemble + result.ensemble = ensemble result.label = SOURCE_FRAME_LABEL return result @classmethod def from_dask_dataframe(cl, df, ensemble=None): - """ Returns a SourceFrame constructed from a Dask dataframe.. + """Returns a SourceFrame constructed from a Dask dataframe.. Parameters ---------- df: `dask.dataframe.DataFrame` or `list` @@ -964,20 +983,21 @@ def from_dask_dataframe(cl, df, ensemble=None): """ # Create a SourceFrame by mapping the partitions to the appropriate meta, TapeSourceFrame # TODO(wbeebe@uw.edu): Determine if there is a better method - result = df.map_partitions(TapeSourceFrame) + result = df.map_partitions(TapeSourceFrame) result.ensemble = ensemble result.label = SOURCE_FRAME_LABEL return result - + + class ObjectFrame(EnsembleFrame): - """ A subclass of EnsembleFrame for Object data. """ + """A subclass of EnsembleFrame for Object data.""" - _partition_type = TapeObjectFrame # Tracks the underlying data type + _partition_type = TapeObjectFrame # Tracks the underlying data type def __init__(self, dsk, name, meta, divisions, ensemble=None): super().__init__(dsk, name, meta, divisions) - self.label = OBJECT_FRAME_LABEL # A label used by the Ensemble to identify this frame. - self.ensemble = ensemble # The Ensemble object containing this frame. + self.label = OBJECT_FRAME_LABEL # A label used by the Ensemble to identify this frame. + self.ensemble = ensemble # The Ensemble object containing this frame. @classmethod def from_parquet( @@ -987,7 +1007,7 @@ def from_parquet( columns=None, ensemble=None, ): - """ Returns an ObjectFrame constructed from loading a parquet file. + """Returns an ObjectFrame constructed from loading a parquet file. Parameters ---------- path: `str` or `list` @@ -1011,16 +1031,20 @@ def from_parquet( """ # Read in the object Parquet file result = dd.read_parquet( - path, index=index, columns=columns, split_row_groups=True, engine=TapeObjectArrowEngine, + path, + index=index, + columns=columns, + split_row_groups=True, + engine=TapeObjectArrowEngine, ) result.ensemble = ensemble - result.label= OBJECT_FRAME_LABEL + result.label = OBJECT_FRAME_LABEL return result @classmethod def from_dask_dataframe(cl, df, ensemble=None): - """ Returns an ObjectFrame constructed from a Dask dataframe.. + """Returns an ObjectFrame constructed from a Dask dataframe.. Parameters ---------- df: `dask.dataframe.DataFrame` or `list` @@ -1033,11 +1057,12 @@ def from_dask_dataframe(cl, df, ensemble=None): """ # Create an ObjectFrame by mapping the partitions to the appropriate meta, TapeObjectFrame # TODO(wbeebe@uw.edu): Determine if there is a better method - result = df.map_partitions(TapeObjectFrame) + result = df.map_partitions(TapeObjectFrame) result.ensemble = ensemble result.label = OBJECT_FRAME_LABEL return result + """ Dask Dataframes are constructed indirectly using method dispatching and inference on the underlying data. So to ensure our subclasses behave correctly, we register the methods @@ -1054,50 +1079,58 @@ def from_dask_dataframe(cl, df, ensemble=None): get_parallel_type.register(TapeObjectFrame, lambda _: ObjectFrame) get_parallel_type.register(TapeSourceFrame, lambda _: SourceFrame) + @make_meta_dispatch.register(TapeSeries) def make_meta_series(x, index=None): # Create an empty TapeSeries to use as Dask's underlying object meta. result = x.head(0) return result + @make_meta_dispatch.register(TapeFrame) def make_meta_frame(x, index=None): # Create an empty TapeFrame to use as Dask's underlying object meta. result = x.head(0) return result + @meta_nonempty.register(TapeSeries) def _nonempty_tapeseries(x, index=None): # Construct a new TapeSeries with the same underlying data. data = _nonempty_series(x) return TapeSeries(data) + @meta_nonempty.register(TapeFrame) def _nonempty_tapeseries(x, index=None): # Construct a new TapeFrame with the same underlying data. df = meta_nonempty_dataframe(x) return TapeFrame(df) + @make_meta_dispatch.register(TapeObjectFrame) def make_meta_frame(x, index=None): # Create an empty TapeObjectFrame to use as Dask's underlying object meta. result = x.head(0) return result + @meta_nonempty.register(TapeObjectFrame) def _nonempty_tapesourceframe(x, index=None): # Construct a new TapeObjectFrame with the same underlying data. df = meta_nonempty_dataframe(x) return TapeObjectFrame(df) + @make_meta_dispatch.register(TapeSourceFrame) def make_meta_frame(x, index=None): # Create an empty TapeSourceFrame to use as Dask's underlying object meta. result = x.head(0) return result + @meta_nonempty.register(TapeSourceFrame) def _nonempty_tapesourceframe(x, index=None): # Construct a new TapeSourceFrame with the same underlying data. df = meta_nonempty_dataframe(x) - return TapeSourceFrame(df) \ No newline at end of file + return TapeSourceFrame(df) From 046942be4c78abeff0918a65e2fc84915b19dd98 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Mon, 4 Dec 2023 11:42:31 -0800 Subject: [PATCH 30/35] Remove unsupported type annotations --- src/tape/ensemble_frame.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index 58129c7a..892e47bb 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -458,13 +458,13 @@ def persist(self, **kwargs): def set_index( self, - other: str | pd.Series, - drop: bool = True, - sorted: bool = False, - npartitions: int | Literal["auto"] | None = None, - divisions: Sequence | None = None, - inplace: bool = False, - sort: bool = True, + other, + drop=True, + sorted=False, + npartitions=None, + divisions=None, + inplace=False, + sort=True, **kwargs, ): """Set the DataFrame index (row labels) using an existing column. From dd22e9ef25f3566713b68951d21c1187d5325f9a Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Mon, 4 Dec 2023 14:10:47 -0800 Subject: [PATCH 31/35] Fix merge error --- tests/tape_tests/test_ensemble.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index c36d5dd9..f0118b5c 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -800,8 +800,8 @@ def test_sync_tables(data_fixture, request, legacy): assert dropped_obj_id not in parquet_ensemble.object.index.compute().values # Dirty flags should be unset after sync - assert not parquet_ensemble.object_dirty - assert not parquet_ensemble.source_dirty + assert not parquet_ensemble.object.is_dirty() + assert not parquet_ensemble.source.is_dirty() # Make sure that divisions are preserved if data_fixture == "parquet_ensemble_with_divisions": From 3de618f3714107998c227e778ae5b7be771bff6f Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Mon, 4 Dec 2023 16:06:37 -0800 Subject: [PATCH 32/35] Use client=False in test_analysis --- tests/tape_tests/test_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tape_tests/test_analysis.py b/tests/tape_tests/test_analysis.py index c75a9621..824e4954 100644 --- a/tests/tape_tests/test_analysis.py +++ b/tests/tape_tests/test_analysis.py @@ -28,7 +28,7 @@ def test_analysis_function(cls, dask_client): "flux": [1.0, 2.0, 5.0, 3.0, 1.0, 2.0, 3.0, 4.0, 5.0], } cmap = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="err", band_col="band") - ens = Ensemble(client=dask_client).from_source_dict(rows, column_mapper=cmap) + ens = Ensemble(client=False).from_source_dict(rows, column_mapper=cmap) assert isinstance(obj.cols(ens), list) assert len(obj.cols(ens)) > 0 From d061b3c4f54d0f5d6acfb738f730c53befc5696d Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Tue, 5 Dec 2023 15:42:25 -0800 Subject: [PATCH 33/35] Remove '_object' and '_source' fields --- src/tape/ensemble.py | 236 ++++++++++++++--------------- tests/tape_tests/test_ensemble.py | 238 +++++++++++++++--------------- 2 files changed, 233 insertions(+), 241 deletions(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index f0cb4540..9ea7234b 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -49,9 +49,6 @@ def __init__(self, client=True, **kwargs): """ self.result = None # holds the latest query - self._source = None # Source Table - self._object = None # Object Table - self.frames = {} # Frames managed by this Ensemble, keyed by label # A unique ID to allocate new result frame labels. @@ -63,9 +60,6 @@ def __init__(self, client=True, **kwargs): self._source_temp = [] # List of temporary columns in Source self._object_temp = [] # List of temporary columns in Object - self._source_temp = [] # List of temporary columns in Source - self._object_temp = [] # List of temporary columns in Object - # Default to removing empty objects. self.keep_empty_objects = kwargs.get("keep_empty_objects", False) @@ -154,10 +148,8 @@ def update_frame(self, frame): if frame.label != expected_label: raise ValueError(f"Unable to update frame with reserved label " f"'{frame.label}'") if isinstance(frame, SourceFrame): - self._source = frame self.source = frame elif isinstance(frame, ObjectFrame): - self._object = frame self.object = frame # Ensure this frame is assigned to this Ensemble. @@ -334,20 +326,20 @@ def insert_sources( df2 = df2.set_index(self._id_col, drop=True, sort=True) # Save the divisions and number of partitions. - prev_div = self._source.divisions - prev_num = self._source.npartitions + prev_div = self.source.divisions + prev_num = self.source.npartitions # Append the new rows to the correct divisions. - self.update_frame(dd.concat([self._source, df2], axis=0, interleave_partitions=True)) - self._source.set_dirty(True) + self.update_frame(dd.concat([self.source, df2], axis=0, interleave_partitions=True)) + self.source.set_dirty(True) # Do the repartitioning if requested. If the divisions were set, reuse them. # Otherwise, use the same number of partitions. if force_repartition: if all(prev_div): - self.update_frame(self._source.repartition(divisions=prev_div)) - elif self._source.npartitions != prev_num: - self._source = self._source.repartition(npartitions=prev_num) + self.update_frame(self.source.repartition(divisions=prev_div)) + elif self.source.npartitions != prev_num: + self.source = self.source.repartition(npartitions=prev_num) return self @@ -383,9 +375,9 @@ def info(self, verbose=True, memory_usage=True, **kwargs): self._lazy_sync_tables(table="all") print("Object Table") - self._object.info(verbose=verbose, memory_usage=memory_usage, **kwargs) + self.object.info(verbose=verbose, memory_usage=memory_usage, **kwargs) print("Source Table") - self._source.info(verbose=verbose, memory_usage=memory_usage, **kwargs) + self.source.info(verbose=verbose, memory_usage=memory_usage, **kwargs) def check_sorted(self, table="object"): """Checks to see if an Ensemble Dataframe is sorted (increasing) on @@ -402,9 +394,9 @@ def check_sorted(self, table="object"): or not (False) """ if table == "object": - idx = self._object.index + idx = self.object.index elif table == "source": - idx = self._source.index + idx = self.source.index else: raise ValueError(f"{table} is not one of 'object' or 'source'") @@ -428,7 +420,7 @@ def check_lightcurve_cohesion(self): across multiple partitions (False) """ - idx = self._source.index + idx = self.source.index counts = idx.map_partitions(lambda a: Counter(a.unique())).compute() unq_counter = counts[0] @@ -457,12 +449,12 @@ def compute(self, table=None, **kwargs): if table: self._lazy_sync_tables(table) if table == "object": - return self._object.compute(**kwargs) + return self.object.compute(**kwargs) elif table == "source": - return self._source.compute(**kwargs) + return self.source.compute(**kwargs) else: self._lazy_sync_tables(table="all") - return (self._object.compute(**kwargs), self._source.compute(**kwargs)) + return (self.object.compute(**kwargs), self.source.compute(**kwargs)) def persist(self, **kwargs): """Wrapper for dask.dataframe.DataFrame.persist() @@ -473,15 +465,15 @@ def persist(self, **kwargs): of the computation. """ self._lazy_sync_tables("all") - self.update_frame(self._object.persist(**kwargs)) - self.update_frame(self._source.persist(**kwargs)) + self.update_frame(self.object.persist(**kwargs)) + self.update_frame(self.source.persist(**kwargs)) def columns(self, table="object"): """Retrieve columns from dask dataframe""" if table == "object": - return self._object.columns + return self.object.columns elif table == "source": - return self._source.columns + return self.source.columns else: raise ValueError(f"{table} is not one of 'object' or 'source'") @@ -490,9 +482,9 @@ def head(self, table="object", n=5, **kwargs): self._lazy_sync_tables(table) if table == "object": - return self._object.head(n=n, **kwargs) + return self.object.head(n=n, **kwargs) elif table == "source": - return self._source.head(n=n, **kwargs) + return self.source.head(n=n, **kwargs) else: raise ValueError(f"{table} is not one of 'object' or 'source'") @@ -501,9 +493,9 @@ def tail(self, table="object", n=5, **kwargs): self._lazy_sync_tables(table) if table == "object": - return self._object.tail(n=n, **kwargs) + return self.object.tail(n=n, **kwargs) elif table == "source": - return self._source.tail(n=n, **kwargs) + return self.source.tail(n=n, **kwargs) else: raise ValueError(f"{table} is not one of 'object' or 'source'") @@ -526,9 +518,9 @@ def dropna(self, table="source", **kwargs): scheme """ if table == "object": - self.update_frame(self._object.dropna(**kwargs)) + self.update_frame(self.object.dropna(**kwargs)) elif table == "source": - self.update_frame(self._source.dropna(**kwargs)) + self.update_frame(self.source.dropna(**kwargs)) else: raise ValueError(f"{table} is not one of 'object' or 'source'") @@ -548,11 +540,11 @@ def select(self, columns, table="object"): """ self._lazy_sync_tables(table) if table == "object": - cols_to_drop = [col for col in self._object.columns if col not in columns] - self.update_frame(self._object.drop(cols_to_drop, axis=1)) + cols_to_drop = [col for col in self.object.columns if col not in columns] + self.update_frame(self.object.drop(cols_to_drop, axis=1)) elif table == "source": - cols_to_drop = [col for col in self._source.columns if col not in columns] - self.update_frame(self._source.drop(cols_to_drop, axis=1)) + cols_to_drop = [col for col in self.source.columns if col not in columns] + self.update_frame(self.source.drop(cols_to_drop, axis=1)) else: raise ValueError(f"{table} is not one of 'object' or 'source'") @@ -581,9 +573,9 @@ def query(self, expr, table="object"): """ self._lazy_sync_tables(table) if table == "object": - self.update_frame(self._object.query(expr)) + self.update_frame(self.object.query(expr)) elif table == "source": - self.update_frame(self._source.query(expr)) + self.update_frame(self.source.query(expr)) return self def filter_from_series(self, keep_series, table="object"): @@ -601,10 +593,10 @@ def filter_from_series(self, keep_series, table="object"): """ self._lazy_sync_tables(table) if table == "object": - self.update_frame(self._object[keep_series]) + self.update_frame(self.object[keep_series]) elif table == "source": - self.update_frame(self._source[keep_series]) + self.update_frame(self.source[keep_series]) return self def assign(self, table="object", temporary=False, **kwargs): @@ -642,17 +634,17 @@ def assign(self, table="object", temporary=False, **kwargs): self._lazy_sync_tables(table) if table == "object": - pre_cols = self._object.columns - self.update_frame(self._object.assign(**kwargs)) - post_cols = self._object.columns + pre_cols = self.object.columns + self.update_frame(self.object.assign(**kwargs)) + post_cols = self.object.columns if temporary: self._object_temp.extend(col for col in post_cols if col not in pre_cols) elif table == "source": - pre_cols = self._source.columns - self.update_frame(self._source.assign(**kwargs)) - post_cols = self._source.columns + pre_cols = self.source.columns + self.update_frame(self.source.assign(**kwargs)) + post_cols = self.source.columns if temporary: self._source_temp.extend(col for col in post_cols if col not in pre_cols) @@ -687,9 +679,9 @@ def coalesce(self, input_cols, output_col, table="object", drop_inputs=False): """ # we shouldn't need to sync for this if table == "object": - table_ddf = self._object + table_ddf = self.object elif table == "source": - table_ddf = self._source + table_ddf = self.source else: raise ValueError(f"{table} is not one of 'object' or 'source'") @@ -777,27 +769,27 @@ def calc_nobs(self, by_band=False, label="nobs", temporary=True): if by_band: # repartition the result to align with object - if self._object.known_divisions: + if self.object.known_divisions: # Grab these up front to help out the task graph id_col = self._id_col band_col = self._band_col # Get the band metadata - unq_bands = np.unique(self._source[band_col]) + unq_bands = np.unique(self.source[band_col]) meta = {band: float for band in unq_bands} # Map the groupby to each partition - band_counts = self._source.map_partitions( + band_counts = self.source.map_partitions( lambda x: x.groupby(id_col)[[band_col]] .value_counts() .to_frame() .reset_index() .pivot_table(values=band_col, index=id_col, columns=band_col, aggfunc="sum"), meta=meta, - ).repartition(divisions=self._object.divisions) + ).repartition(divisions=self.object.divisions) else: band_counts = ( - self._source.groupby([self._id_col])[self._band_col] # group by each object + self.source.groupby([self._id_col])[self._band_col] # group by each object .value_counts() # count occurence of each band .to_frame() # convert series to dataframe .rename(columns={self._band_col: "counts"}) # rename column @@ -808,13 +800,13 @@ def calc_nobs(self, by_band=False, label="nobs", temporary=True): ) ) # the pivot_table call makes each band_count a column of the id_col row - band_counts = band_counts.repartition(npartitions=self._object.npartitions) + band_counts = band_counts.repartition(npartitions=self.object.npartitions) # short-hand for calculating nobs_total band_counts["total"] = band_counts[list(band_counts.columns)].sum(axis=1) bands = band_counts.columns.values - self._object = self._object.assign( + self.object = self.object.assign( **{label + "_" + str(band): band_counts[band] for band in bands} ) @@ -822,24 +814,24 @@ def calc_nobs(self, by_band=False, label="nobs", temporary=True): self._object_temp.extend(label + "_" + str(band) for band in bands) else: - if self._object.known_divisions and self._source.known_divisions: + if self.object.known_divisions and self.source.known_divisions: # Grab these up front to help out the task graph id_col = self._id_col band_col = self._band_col # Map the groupby to each partition - counts = self._source.map_partitions( + counts = self.source.map_partitions( lambda x: x.groupby([id_col])[[band_col]].aggregate("count") - ).repartition(divisions=self._object.divisions) + ).repartition(divisions=self.object.divisions) else: # Just do a groupby on all source counts = ( - self._source.groupby([self._id_col])[[self._band_col]] + self.source.groupby([self._id_col])[[self._band_col]] .aggregate("count") - .repartition(npartitions=self._object.npartitions) + .repartition(npartitions=self.object.npartitions) ) - self._object = self._object.assign(**{label + "_total": counts[self._band_col]}) + self.object = self.object.assign(**{label + "_total": counts[self._band_col]}) if temporary: self._object_temp.extend([label + "_total"]) @@ -876,7 +868,7 @@ def prune(self, threshold=50, col_name=None): # Mask on object table self = self.query(f"{col_name} >= {threshold}", table="object") - self._object.set_dirty(True) # Object table is now dirty + self.object.set_dirty(True) # Object table is now dirty return self @@ -902,7 +894,7 @@ def find_day_gap_offset(self): self._lazy_sync_tables(table="source") # Compute a histogram of observations by hour of the day. - hours = self._source[self._time_col].apply( + hours = self.source[self._time_col].apply( lambda x: np.floor(x * 24.0).astype(int) % 24, meta=pd.Series(dtype=int) ) hour_counts = hours.value_counts().compute() @@ -978,9 +970,9 @@ def bin_sources( # Bin the time and add it as a column. We create a temporary column that # truncates the time into increments of `time_window`. tmp_time_col = "tmp_time_for_aggregation" - if tmp_time_col in self._source.columns: + if tmp_time_col in self.source.columns: raise KeyError(f"Column '{tmp_time_col}' already exists in source table.") - self._source[tmp_time_col] = self._source[self._time_col].apply( + self.source[tmp_time_col] = self.source[self._time_col].apply( lambda x: np.floor((x + offset) / time_window) * time_window, meta=pd.Series(dtype=float) ) @@ -988,7 +980,7 @@ def bin_sources( aggr_funs = {self._time_col: "mean", self._flux_col: "mean"} # If the source table has errors then add an aggregation function for it. - if self._err_col in self._source.columns: + if self._err_col in self.source.columns: aggr_funs[self._err_col] = dd.Aggregation( name="err_agg", chunk=lambda x: (x.count(), x.apply(lambda s: np.sum(np.power(s, 2)))), @@ -1000,8 +992,8 @@ def bin_sources( # adding an initial column of all ones if needed. if count_col is not None: self._bin_count_col = count_col - if self._bin_count_col not in self._source.columns: - self._source[self._bin_count_col] = self._source[self._time_col].apply( + if self._bin_count_col not in self.source.columns: + self.source[self._bin_count_col] = self.source[self._time_col].apply( lambda x: 1, meta=pd.Series(dtype=int) ) aggr_funs[self._bin_count_col] = "sum" @@ -1016,14 +1008,14 @@ def bin_sources( # Group the columns by id, band, and time bucket and aggregate. self.update_frame( - self._source.groupby([self._id_col, self._band_col, tmp_time_col]).aggregate(aggr_funs) + self.source.groupby([self._id_col, self._band_col, tmp_time_col]).aggregate(aggr_funs) ) # Fix the indices and remove the temporary column. - self.update_frame(self._source.reset_index().set_index(self._id_col).drop(tmp_time_col, axis=1)) + self.update_frame(self.source.reset_index().set_index(self._id_col).drop(tmp_time_col, axis=1)) # Mark the source table as dirty. - self._source.set_dirty(True) + self.source.set_dirty(True) return self def batch(self, func, *args, meta=None, use_map=True, compute=True, on=None, label="", **kwargs): @@ -1129,15 +1121,15 @@ def s2n_inter_quartile_range(flux, err): on = [on] # Convert to list if only one column is passed # Handle object columns to group on - source_cols = list(self._source.columns) - object_cols = list(self._object.columns) + source_cols = list(self.source.columns) + object_cols = list(self.object.columns) object_group_cols = [col for col in on if (col in object_cols) and (col not in source_cols)] if len(object_group_cols) > 0: - object_col_dd = self._object[object_group_cols] - source_to_batch = self._source.merge(object_col_dd, how="left") + object_col_dd = self.object[object_group_cols] + source_to_batch = self.source.merge(object_col_dd, how="left") else: - source_to_batch = self._source # Can directly use the source table + source_to_batch = self.source # Can directly use the source table id_col = self._id_col # pre-compute needed for dask in lambda function @@ -1162,8 +1154,8 @@ def s2n_inter_quartile_range(flux, err): # Inherit divisions if known from source and the resulting index is the id # Groupby on index should always return a subset that adheres to the same divisions criteria - if self._source.known_divisions and batch.index.name == self._id_col: - batch.divisions = self._source.divisions + if self.source.known_divisions and batch.index.name == self._id_col: + batch.divisions = self.source.divisions if label is not None: if label == "": @@ -1285,21 +1277,21 @@ def from_dask_dataframe( else: self.update_frame(ObjectFrame.from_dask_dataframe(object_frame, ensemble=self)) - self.update_frame(self._object.set_index(self._id_col, sorted=sorted, sort=sort)) + self.update_frame(self.object.set_index(self._id_col, sorted=sorted, sort=sort)) # Optionally sync the tables, recalculates nobs columns if sync_tables: - self._source.set_dirty(True) - self._object.set_dirty(True) + self.source.set_dirty(True) + self.object.set_dirty(True) self._sync_tables() if npartitions and npartitions > 1: - self._source = self._source.repartition(npartitions=npartitions) + self.source = self.source.repartition(npartitions=npartitions) elif partition_size: - self._source = self._source.repartition(partition_size=partition_size) + self.source = self.source.repartition(partition_size=partition_size) # Check that Divisions are established, warn if not. - for name, table in [("object", self._object), ("source", self._source)]: + for name, table in [("object", self.object), ("source", self.source)]: if not table.known_divisions: warnings.warn( f"Divisions for {name} are not set, certain downstream dask operations may fail as a result. We recommend setting the `sort` or `sorted` flags when loading data to establish division information." @@ -1670,25 +1662,25 @@ def convert_flux_to_mag(self, zero_point, zp_form="mag", out_col_name=None, flux if zp_form == "flux": # mag = -2.5*np.log10(flux/zp) if isinstance(zero_point, str): self.update_frame( - self._source.assign( + self.source.assign( **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / x[zero_point])} ) ) else: self.update_frame( - self._source.assign(**{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / zero_point)}) + self.source.assign(**{out_col_name: lambda x: -2.5 * np.log10(x[flux_col] / zero_point)}) ) elif zp_form == "magnitude" or zp_form == "mag": # mag = -2.5*np.log10(flux) + zp if isinstance(zero_point, str): self.update_frame( - self._source.assign( + self.source.assign( **{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + x[zero_point]} ) ) else: self.update_frame( - self._source.assign(**{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + zero_point}) + self.source.assign(**{out_col_name: lambda x: -2.5 * np.log10(x[flux_col]) + zero_point}) ) else: raise ValueError(f"{zp_form} is not a valid zero_point format.") @@ -1696,7 +1688,7 @@ def convert_flux_to_mag(self, zero_point, zp_form="mag", out_col_name=None, flux # Calculate Errors if err_col is not None: self.update_frame( - self._source.assign( + self.source.assign( **{out_col_name + "_err": lambda x: (2.5 / np.log(10)) * (x[err_col] / x[flux_col])} ) ) @@ -1705,7 +1697,7 @@ def convert_flux_to_mag(self, zero_point, zp_form="mag", out_col_name=None, flux def _generate_object_table(self): """Generate an empty object table from the source table.""" - res = self._source.map_partitions(lambda x: TapeObjectFrame(index=x.index.unique())) + res = self.source.map_partitions(lambda x: TapeObjectFrame(index=x.index.unique())) return res @@ -1740,11 +1732,11 @@ def _lazy_sync_tables(self, table="object"): The table being modified. Should be one of "object", "source", or "all" """ - if table == "object" and self._source.is_dirty(): # object table should be updated + if table == "object" and self.source.is_dirty(): # object table should be updated self._sync_tables() - elif table == "source" and self._object.is_dirty(): # source table should be updated + elif table == "source" and self.object.is_dirty(): # source table should be updated self._sync_tables() - elif table == "all" and (self._source.is_dirty() or self._object.is_dirty()): + elif table == "all" and (self.source.is_dirty() or self.object.is_dirty()): self._sync_tables() return self @@ -1756,55 +1748,55 @@ def _sync_tables(self): keep_empty_objects attribute is set to True. """ - if self._object.is_dirty(): + if self.object.is_dirty(): # Sync Object to Source; remove any missing objects from source - if self._object.known_divisions and self._source.known_divisions: + if self.object.known_divisions and self.source.known_divisions: # Lazily Create an empty object table (just index) for joining - empty_obj = self._object.map_partitions(lambda x: TapeObjectFrame(index=x.index)) - if type(empty_obj) != type(self._object): + empty_obj = self.object.map_partitions(lambda x: TapeObjectFrame(index=x.index)) + if type(empty_obj) != type(self.object): raise ValueError("Bad type for empty_obj: " + str(type(empty_obj))) # Join source onto the empty object table to align - self.update_frame(self._source.join(empty_obj, how="inner")) + self.update_frame(self.source.join(empty_obj, how="inner")) else: warnings.warn("Divisions are not known, syncing using a non-lazy method.") - obj_idx = list(self._object.index.compute()) - self.update_frame(self._source.map_partitions(lambda x: x[x.index.isin(obj_idx)])) - self.update_frame(self._source.persist()) # persist the source frame + obj_idx = list(self.object.index.compute()) + self.update_frame(self.source.map_partitions(lambda x: x[x.index.isin(obj_idx)])) + self.update_frame(self.source.persist()) # persist the source frame # Drop Temporary Source Columns on Sync if len(self._source_temp): - self.update_frame(self._source.drop(columns=self._source_temp)) + self.update_frame(self.source.drop(columns=self._source_temp)) print(f"Temporary columns dropped from Source Table: {self._source_temp}") self._source_temp = [] - if self._source.is_dirty(): # not elif + if self.source.is_dirty(): # not elif if not self.keep_empty_objects: - if self._object.known_divisions and self._source.known_divisions: + if self.object.known_divisions and self.source.known_divisions: # Lazily Create an empty source table (just unique indexes) for joining - empty_src = self._source.map_partitions(lambda x: TapeSourceFrame(index=x.index.unique())) - if type(empty_src) != type(self._source): + empty_src = self.source.map_partitions(lambda x: TapeSourceFrame(index=x.index.unique())) + if type(empty_src) != type(self.source): raise ValueError("Bad type for empty_src: " + str(type(empty_src))) # Join object onto the empty unique source table to align - self.update_frame(self._object.join(empty_src, how="inner")) + self.update_frame(self.object.join(empty_src, how="inner")) else: warnings.warn("Divisions are not known, syncing using a non-lazy method.") # Sync Source to Object; remove any objects that do not have sources - sor_idx = list(self._source.index.unique().compute()) - self.update_frame(self._object.map_partitions(lambda x: x[x.index.isin(sor_idx)])) - self.update_frame(self._object.persist()) # persist the object frame + sor_idx = list(self.source.index.unique().compute()) + self.update_frame(self.object.map_partitions(lambda x: x[x.index.isin(sor_idx)])) + self.update_frame(self.object.persist()) # persist the object frame # Drop Temporary Object Columns on Sync if len(self._object_temp): - self.update_frame(self._object.drop(columns=self._object_temp)) + self.update_frame(self.object.drop(columns=self._object_temp)) print(f"Temporary columns dropped from Object Table: {self._object_temp}") self._object_temp = [] # Now synced and clean - self._source.set_dirty(False) - self._object.set_dirty(False) + self.source.set_dirty(False) + self.object.set_dirty(False) return self def to_timeseries( @@ -1857,7 +1849,7 @@ def to_timeseries( if band_col is None: band_col = self._band_col - df = self._source.loc[target].compute() + df = self.source.loc[target].compute() ts = TimeSeries().from_dataframe( data=df, object_id=target, @@ -1929,11 +1921,11 @@ def sf2(self, sf_method="basic", argument_container=None, use_map=True, compute= if argument_container.combine: result = calc_sf2( - self._source[self._time_col], - self._source[self._flux_col], - self._source[self._err_col], - self._source[self._band_col], - self._source.index, + self.source[self._time_col], + self.source[self._flux_col], + self.source[self._err_col], + self.source[self._band_col], + self.source.index, argument_container=argument_container, ) @@ -1943,8 +1935,8 @@ def sf2(self, sf_method="basic", argument_container=None, use_map=True, compute= ) # Inherit divisions information if known - if self._source.known_divisions and self._object.known_divisions: - result.divisions = self._source.divisions + if self.source.known_divisions and self.object.known_divisions: + result.divisions = self.source.divisions return result diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index f0118b5c..ef61d7e0 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -59,13 +59,13 @@ def test_parquet_construction(data_fixture, request): parquet_ensemble = request.getfixturevalue(data_fixture) # Check to make sure the source and object tables were created - assert parquet_ensemble._source is not None - assert parquet_ensemble._object is not None + assert parquet_ensemble.source is not None + assert parquet_ensemble.object is not None # Make sure divisions are set if data_fixture == "parquet_ensemble_with_divisions": - assert parquet_ensemble._source.known_divisions - assert parquet_ensemble._object.known_divisions + assert parquet_ensemble.source.known_divisions + assert parquet_ensemble.object.known_divisions # Check that the data is not empty. obj, source = parquet_ensemble.compute() @@ -84,7 +84,7 @@ def test_parquet_construction(data_fixture, request): parquet_ensemble._provenance_col, ]: # Check to make sure the critical quantity labels are bound to real columns - assert parquet_ensemble._source[col] is not None + assert parquet_ensemble.source[col] is not None @pytest.mark.parametrize( @@ -107,8 +107,8 @@ def test_dataframe_constructors(data_fixture, request): ens = request.getfixturevalue(data_fixture) # Check to make sure the source and object tables were created - assert ens._source is not None - assert ens._object is not None + assert ens.source is not None + assert ens.object is not None # Check that the data is not empty. obj, source = ens.compute() @@ -126,7 +126,7 @@ def test_dataframe_constructors(data_fixture, request): ens._band_col, ]: # Check to make sure the critical quantity labels are bound to real columns - assert ens._source[col] is not None + assert ens.source[col] is not None # Check that we can compute an analysis function on the ensemble. amplitude = ens.batch(calc_stetson_J) @@ -146,33 +146,33 @@ def test_update_ensemble(data_fixture, request): ens = request.getfixturevalue(data_fixture) # Filter the object table and have the ensemble track the updated table. - updated_obj = ens._object.query("nobs_total > 50") - assert updated_obj is not ens._object + updated_obj = ens.object.query("nobs_total > 50") + assert updated_obj is not ens.object assert updated_obj.is_dirty() # Update the ensemble and validate that it marks the object table dirty - assert ens._object.is_dirty() == False + assert ens.object.is_dirty() == False updated_obj.update_ensemble() - assert ens._object.is_dirty() == True - assert updated_obj is ens._object + assert ens.object.is_dirty() == True + assert updated_obj is ens.object # Filter the source table and have the ensemble track the updated table. - updated_src = ens._source.query("psFluxErr > 0.1") - assert updated_src is not ens._source + updated_src = ens.source.query("psFluxErr > 0.1") + assert updated_src is not ens.source # Update the ensemble and validate that it marks the source table dirty - assert ens._source.is_dirty() == False + assert ens.source.is_dirty() == False updated_src.update_ensemble() - assert ens._source.is_dirty() == True - assert updated_src is ens._source + assert ens.source.is_dirty() == True + assert updated_src is ens.source # Compute a result to trigger a table sync obj, src = ens.compute() assert len(obj) > 0 assert len(src) > 0 - assert ens._object.is_dirty() == False - assert ens._source.is_dirty() == False + assert ens.object.is_dirty() == False + assert ens.source.is_dirty() == False # Create an additional result table for the ensemble to track. - cnts = ens._source.groupby([ens._id_col, ens._band_col])[ens._time_col].aggregate("count") + cnts = ens.source.groupby([ens._id_col, ens._band_col])[ens._time_col].aggregate("count") res = ( cnts.to_frame() .reset_index() @@ -464,7 +464,7 @@ def test_read_source_dict(dask_client): def test_insert(parquet_ensemble): - num_partitions = parquet_ensemble._source.npartitions + num_partitions = parquet_ensemble.source.npartitions (old_object, old_source) = parquet_ensemble.compute() old_size = old_source.shape[0] @@ -486,7 +486,7 @@ def test_insert(parquet_ensemble): ) # Check we did not increase the number of partitions. - assert parquet_ensemble._source.npartitions == num_partitions + assert parquet_ensemble.source.npartitions == num_partitions # Check that all the new data points are in there. The order may be different # due to the repartitioning. @@ -515,7 +515,7 @@ def test_insert(parquet_ensemble): ) # Check we *did* increase the number of partitions and the size increased. - assert parquet_ensemble._source.npartitions != num_partitions + assert parquet_ensemble.source.npartitions != num_partitions (new_obj, new_source) = parquet_ensemble.compute() assert new_source.shape[0] == old_size + 10 @@ -544,8 +544,8 @@ def test_insert_paritioned(dask_client): # Save the old data for comparison. old_data = ens.compute("source") - old_div = copy.copy(ens._source.divisions) - old_sizes = [len(ens._source.partitions[i]) for i in range(4)] + old_div = copy.copy(ens.source.divisions) + old_sizes = [len(ens.source.partitions[i]) for i in range(4)] assert old_data.shape[0] == num_points # Test an insertion of 5 observations. @@ -558,12 +558,12 @@ def test_insert_paritioned(dask_client): # Check we did not increase the number of partitions and the points # were placed in the correct partitions. - assert ens._source.npartitions == 4 - assert ens._source.divisions == old_div - assert len(ens._source.partitions[0]) == old_sizes[0] + 3 - assert len(ens._source.partitions[1]) == old_sizes[1] - assert len(ens._source.partitions[2]) == old_sizes[2] + 2 - assert len(ens._source.partitions[3]) == old_sizes[3] + assert ens.source.npartitions == 4 + assert ens.source.divisions == old_div + assert len(ens.source.partitions[0]) == old_sizes[0] + 3 + assert len(ens.source.partitions[1]) == old_sizes[1] + assert len(ens.source.partitions[2]) == old_sizes[2] + 2 + assert len(ens.source.partitions[3]) == old_sizes[3] # Check that all the new data points are in there. The order may be different # due to the repartitioning. @@ -581,12 +581,12 @@ def test_insert_paritioned(dask_client): # Check we did not increase the number of partitions and the points # were placed in the correct partitions. - assert ens._source.npartitions == 4 - assert ens._source.divisions == old_div - assert len(ens._source.partitions[0]) == old_sizes[0] + 3 - assert len(ens._source.partitions[1]) == old_sizes[1] + 5 - assert len(ens._source.partitions[2]) == old_sizes[2] + 2 - assert len(ens._source.partitions[3]) == old_sizes[3] + assert ens.source.npartitions == 4 + assert ens.source.divisions == old_div + assert len(ens.source.partitions[0]) == old_sizes[0] + 3 + assert len(ens.source.partitions[1]) == old_sizes[1] + 5 + assert len(ens.source.partitions[2]) == old_sizes[2] + 2 + assert len(ens.source.partitions[3]) == old_sizes[3] def test_core_wrappers(parquet_ensemble): @@ -677,9 +677,9 @@ def test_persist(dask_client): ens.query("flux <= 1.5", table="source") # Compute the task graph size before and after the persist. - old_graph_size = len(ens._source.dask) + old_graph_size = len(ens.source.dask) ens.persist() - new_graph_size = len(ens._source.dask) + new_graph_size = len(ens.source.dask) assert new_graph_size < old_graph_size @@ -782,7 +782,7 @@ def test_sync_tables(data_fixture, request, legacy): filtered_src.update_ensemble() # Verify that the object ID we removed from the source table is present in the object table - assert dropped_obj_id in parquet_ensemble._object.index.compute().values + assert dropped_obj_id in parquet_ensemble.object.index.compute().values # Perform an operation which should trigger syncing both tables. parquet_ensemble.compute() @@ -824,8 +824,8 @@ def test_lazy_sync_tables(parquet_ensemble, legacy): # Modify only the object table. parquet_ensemble.prune(50, col_name="nobs_r").prune(50, col_name="nobs_g") - assert parquet_ensemble._object.is_dirty() - assert not parquet_ensemble._source.is_dirty() + assert parquet_ensemble.object.is_dirty() + assert not parquet_ensemble.source.is_dirty() # For a lazy sync on the object table, nothing should change, because # it is already dirty. @@ -833,34 +833,34 @@ def test_lazy_sync_tables(parquet_ensemble, legacy): parquet_ensemble.compute("object") else: parquet_ensemble.object.compute() - assert parquet_ensemble._object.is_dirty() - assert not parquet_ensemble._source.is_dirty() + assert parquet_ensemble.object.is_dirty() + assert not parquet_ensemble.source.is_dirty() # For a lazy sync on the source table, the source table should be updated. if legacy: parquet_ensemble.compute("source") else: parquet_ensemble.source.compute() - assert not parquet_ensemble._object.is_dirty() - assert not parquet_ensemble._source.is_dirty() + assert not parquet_ensemble.object.is_dirty() + assert not parquet_ensemble.source.is_dirty() # Modify only the source table. # Replace the maximum flux value with a NaN so that we will have a row to drop. - max_flux = max(parquet_ensemble._source[parquet_ensemble._flux_col]) - parquet_ensemble._source[parquet_ensemble._flux_col] = parquet_ensemble._source[ + max_flux = max(parquet_ensemble.source[parquet_ensemble._flux_col]) + parquet_ensemble.source[parquet_ensemble._flux_col] = parquet_ensemble.source[ parquet_ensemble._flux_col].apply( lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float) ) - assert not parquet_ensemble._object.is_dirty() - assert not parquet_ensemble._source.is_dirty() + assert not parquet_ensemble.object.is_dirty() + assert not parquet_ensemble.source.is_dirty() if legacy: parquet_ensemble.dropna(table="source") else: parquet_ensemble.source.dropna().update_ensemble() - assert not parquet_ensemble._object.is_dirty() - assert parquet_ensemble._source.is_dirty() + assert not parquet_ensemble.object.is_dirty() + assert parquet_ensemble.source.is_dirty() # For a lazy sync on the source table, nothing should change, because # it is already dirty. @@ -868,16 +868,16 @@ def test_lazy_sync_tables(parquet_ensemble, legacy): parquet_ensemble.compute("source") else: parquet_ensemble.source.compute() - assert not parquet_ensemble._object.is_dirty() - assert parquet_ensemble._source.is_dirty() + assert not parquet_ensemble.object.is_dirty() + assert parquet_ensemble.source.is_dirty() # For a lazy sync on the source, the object table should be updated. if legacy: parquet_ensemble.compute("object") else: parquet_ensemble.object.compute() - assert not parquet_ensemble._object.is_dirty() - assert not parquet_ensemble._source.is_dirty() + assert not parquet_ensemble.object.is_dirty() + assert not parquet_ensemble.source.is_dirty() def test_compute_triggers_syncing(parquet_ensemble): @@ -931,7 +931,7 @@ def test_temporary_cols(parquet_ensemble): """ ens = parquet_ensemble - ens.update_frame(ens._object.drop(columns=["nobs_r", "nobs_g", "nobs_total"])) + ens.update_frame(ens.object.drop(columns=["nobs_r", "nobs_g", "nobs_total"])) # Make sure temp lists are available but empty assert not len(ens._source_temp) @@ -941,29 +941,29 @@ def test_temporary_cols(parquet_ensemble): # nobs_total should be a temporary column assert "nobs_total" in ens._object_temp - assert "nobs_total" in ens._object.columns + assert "nobs_total" in ens.object.columns ens.assign(nobs2=lambda x: x["nobs_total"] * 2, table="object", temporary=True) # nobs2 should be a temporary column assert "nobs2" in ens._object_temp - assert "nobs2" in ens._object.columns + assert "nobs2" in ens.object.columns # drop NaNs from source, source should be dirty now ens.dropna(how="any", table="source") - assert ens._source.is_dirty() + assert ens.source.is_dirty() # try a sync ens._sync_tables() # nobs_total should be removed from object assert "nobs_total" not in ens._object_temp - assert "nobs_total" not in ens._object.columns + assert "nobs_total" not in ens.object.columns # nobs2 should be removed from object assert "nobs2" not in ens._object_temp - assert "nobs2" not in ens._object.columns + assert "nobs2" not in ens.object.columns # add a source column that we manually set as dirty, don't have a function # that adds temporary source columns at the moment @@ -972,14 +972,14 @@ def test_temporary_cols(parquet_ensemble): # prune object, object should be dirty ens.prune(threshold=10) - assert ens._object.is_dirty() + assert ens.object.is_dirty() # try a sync ens._sync_tables() # f2 should be removed from source assert "f2" not in ens._source_temp - assert "f2" not in ens._source.columns + assert "f2" not in ens.source.columns def test_temporary_cols(parquet_ensemble): @@ -988,7 +988,7 @@ def test_temporary_cols(parquet_ensemble): """ ens = parquet_ensemble - ens._object = ens._object.drop(columns=["nobs_r", "nobs_g", "nobs_total"]) + ens.object = ens.object.drop(columns=["nobs_r", "nobs_g", "nobs_total"]) # Make sure temp lists are available but empty assert not len(ens._source_temp) @@ -998,17 +998,17 @@ def test_temporary_cols(parquet_ensemble): # nobs_total should be a temporary column assert "nobs_total" in ens._object_temp - assert "nobs_total" in ens._object.columns + assert "nobs_total" in ens.object.columns ens.assign(nobs2=lambda x: x["nobs_total"] * 2, table="object", temporary=True) # nobs2 should be a temporary column assert "nobs2" in ens._object_temp - assert "nobs2" in ens._object.columns + assert "nobs2" in ens.object.columns # Replace the maximum flux value with a NaN so that we will have a row to drop. - max_flux = max(parquet_ensemble._source[parquet_ensemble._flux_col]) - parquet_ensemble._source[parquet_ensemble._flux_col] = parquet_ensemble._source[ + max_flux = max(parquet_ensemble.source[parquet_ensemble._flux_col]) + parquet_ensemble.source[parquet_ensemble._flux_col] = parquet_ensemble.source[ parquet_ensemble._flux_col].apply( lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float) ) @@ -1016,18 +1016,18 @@ def test_temporary_cols(parquet_ensemble): # drop NaNs from source, source should be dirty now ens.dropna(how="any", table="source") - assert ens._source.is_dirty() + assert ens.source.is_dirty() # try a sync ens._sync_tables() # nobs_total should be removed from object assert "nobs_total" not in ens._object_temp - assert "nobs_total" not in ens._object.columns + assert "nobs_total" not in ens.object.columns # nobs2 should be removed from object assert "nobs2" not in ens._object_temp - assert "nobs2" not in ens._object.columns + assert "nobs2" not in ens.object.columns # add a source column that we manually set as dirty, don't have a function # that adds temporary source columns at the moment @@ -1036,14 +1036,14 @@ def test_temporary_cols(parquet_ensemble): # prune object, object should be dirty ens.prune(threshold=10) - assert ens._object.is_dirty() + assert ens.object.is_dirty() # try a sync ens._sync_tables() # f2 should be removed from source assert "f2" not in ens._source_temp - assert "f2" not in ens._source.columns + assert "f2" not in ens.source.columns @pytest.mark.parametrize( @@ -1089,11 +1089,11 @@ def test_dropna(data_fixture, request, legacy): parquet_ensemble.dropna(table="source") else: parquet_ensemble.source.dropna().update_ensemble() - assert len(parquet_ensemble._source.compute().index) == source_length - occurrences_source + assert len(parquet_ensemble.source.compute().index) == source_length - occurrences_source if data_fixture == "parquet_ensemble_with_divisions": # divisions should be preserved - assert parquet_ensemble._source.known_divisions + assert parquet_ensemble.source.known_divisions # Now test dropping na from 'object' table # Sync the tables @@ -1116,7 +1116,7 @@ def test_dropna(data_fixture, request, legacy): # Set the nobs_g values for one object to NaN so we can drop it. # We do this on the instantiated object (pdf) and convert it back into a # ObjectFrame. - object_pdf.loc[valid_object_id, parquet_ensemble._object.columns[0]] = pd.NA + object_pdf.loc[valid_object_id, parquet_ensemble.object.columns[0]] = pd.NA parquet_ensemble.update_frame(ObjectFrame.from_tapeframe(TapeObjectFrame(object_pdf), label="object", npartitions=1)) # Try dropping NaNs from object and confirm that we dropped a row @@ -1128,7 +1128,7 @@ def test_dropna(data_fixture, request, legacy): if data_fixture == "parquet_ensemble_with_divisions": # divisions should be preserved - assert parquet_ensemble._object.known_divisions + assert parquet_ensemble.object.known_divisions new_objects_pdf = parquet_ensemble.object.compute() assert len(new_objects_pdf.index) == len(object_pdf.index) - 1 @@ -1147,16 +1147,16 @@ def test_keep_zeros(parquet_ensemble, legacy): Ensemble.dropna when `legacy` is `True`, and EnsembleFrame.dropna when `legacy` is `False`.""" parquet_ensemble.keep_empty_objects = True - prev_npartitions = parquet_ensemble._object.npartitions - old_objects_pdf = parquet_ensemble._object.compute() - pdf = parquet_ensemble._source.compute() + prev_npartitions = parquet_ensemble.object.npartitions + old_objects_pdf = parquet_ensemble.object.compute() + pdf = parquet_ensemble.source.compute() # Set the psFlux values for one object to NaN so we can drop it. # We do this on the instantiated object (pdf) and convert it back into a # Dask DataFrame. valid_id = pdf.index.values[1] pdf.loc[valid_id, parquet_ensemble._flux_col] = pd.NA - parquet_ensemble._source = dd.from_pandas(pdf, npartitions=1) + parquet_ensemble.source = dd.from_pandas(pdf, npartitions=1) parquet_ensemble.update_frame(SourceFrame.from_tapeframe(TapeSourceFrame(pdf), npartitions=1, label="source")) # Sync the table and check that the number of objects decreased. @@ -1167,9 +1167,9 @@ def test_keep_zeros(parquet_ensemble, legacy): parquet_ensemble._sync_tables() # Check that objects are preserved after sync - new_objects_pdf = parquet_ensemble._object.compute() + new_objects_pdf = parquet_ensemble.object.compute() assert len(new_objects_pdf.index) == len(old_objects_pdf.index) - assert parquet_ensemble._object.npartitions == prev_npartitions + assert parquet_ensemble.object.npartitions == prev_npartitions @pytest.mark.parametrize( @@ -1186,29 +1186,29 @@ def test_calc_nobs(data_fixture, request, by_band, multi_partition): ens = request.getfixturevalue(data_fixture) if multi_partition: - ens._source = ens._source.repartition(3) + ens.source = ens.source.repartition(3) # Drop the existing nobs columns - ens._object = ens._object.drop(["nobs_g", "nobs_r", "nobs_total"], axis=1) + ens.object = ens.object.drop(["nobs_g", "nobs_r", "nobs_total"], axis=1) # Calculate nobs ens.calc_nobs(by_band) # Check that things turned out as we expect - lc = ens._object.loc[88472935274829959].compute() + lc = ens.object.loc[88472935274829959].compute() if by_band: - assert np.all([col in ens._object.columns for col in ["nobs_g", "nobs_r"]]) + assert np.all([col in ens.object.columns for col in ["nobs_g", "nobs_r"]]) assert lc["nobs_g"].values[0] == 98 assert lc["nobs_r"].values[0] == 401 - assert "nobs_total" in ens._object.columns + assert "nobs_total" in ens.object.columns assert lc["nobs_total"].values[0] == 499 # Make sure that if divisions were set previously, they are preserved if data_fixture == "parquet_ensemble_with_divisions": - assert ens._object.known_divisions - assert ens._source.known_divisions + assert ens.object.known_divisions + assert ens.source.known_divisions @pytest.mark.parametrize( @@ -1231,19 +1231,19 @@ def test_prune(data_fixture, request, generate_nobs): # Generate the nobs cols from within prune if generate_nobs: # Drop the existing nobs columns - parquet_ensemble._object = parquet_ensemble._object.drop(["nobs_g", "nobs_r", "nobs_total"], axis=1) + parquet_ensemble.object = parquet_ensemble.object.drop(["nobs_g", "nobs_r", "nobs_total"], axis=1) parquet_ensemble.prune(threshold) # Use an existing column else: parquet_ensemble.prune(threshold, col_name="nobs_total") - assert not np.any(parquet_ensemble._object["nobs_total"].values < threshold) + assert not np.any(parquet_ensemble.object["nobs_total"].values < threshold) # Make sure that if divisions were set previously, they are preserved if data_fixture == "parquet_ensemble_with_divisions": - assert parquet_ensemble._source.known_divisions - assert parquet_ensemble._object.known_divisions + assert parquet_ensemble.source.known_divisions + assert parquet_ensemble.object.known_divisions def test_query(dask_client): @@ -1285,7 +1285,7 @@ def test_filter_from_series(dask_client): ens.from_source_dict(rows, column_mapper=cmap, npartitions=2) # Filter the data set to low flux sources only. - keep_series = ens._source[ens._time_col] >= 250.0 + keep_series = ens.source[ens._time_col] >= 250.0 ens.filter_from_series(keep_series, table="source") # Check that all of the filtered rows are value. @@ -1310,22 +1310,22 @@ def test_select(dask_client): } cmap = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="err", band_col="band") ens.from_source_dict(rows, column_mapper=cmap, npartitions=2) - assert len(ens._source.columns) == 5 - assert "time" in ens._source.columns - assert "flux" in ens._source.columns - assert "band" in ens._source.columns - assert "count" in ens._source.columns - assert "something_else" in ens._source.columns + assert len(ens.source.columns) == 5 + assert "time" in ens.source.columns + assert "flux" in ens.source.columns + assert "band" in ens.source.columns + assert "count" in ens.source.columns + assert "something_else" in ens.source.columns # Select on just time and flux ens.select(["time", "flux"], table="source") - assert len(ens._source.columns) == 2 - assert "time" in ens._source.columns - assert "flux" in ens._source.columns - assert "band" not in ens._source.columns - assert "count" not in ens._source.columns - assert "something_else" not in ens._source.columns + assert len(ens.source.columns) == 2 + assert "time" in ens.source.columns + assert "flux" in ens.source.columns + assert "band" not in ens.source.columns + assert "count" not in ens.source.columns + assert "something_else" not in ens.source.columns @pytest.mark.parametrize("legacy", [True, False]) def test_assign(dask_client, legacy): @@ -1345,7 +1345,7 @@ def test_assign(dask_client, legacy): cmap = ColumnMapper(id_col="id", time_col="time", flux_col="flux", err_col="err", band_col="band") ens.from_source_dict(rows, column_mapper=cmap, npartitions=1) assert len(ens.source.columns) == 4 - assert "lower_bnd" not in ens._source.columns + assert "lower_bnd" not in ens.source.columns # Insert a new column for the "lower bound" computation. if legacy: @@ -1400,7 +1400,7 @@ def test_coalesce(dask_client, drop_inputs): ens.coalesce(["flux1", "flux2", "flux3"], "flux", table="source", drop_inputs=drop_inputs) # Coalesce should return this exact flux array - assert list(ens._source["flux"].values.compute()) == [5.0, 3.0, 4.0, 10.0, 7.0] + assert list(ens.source["flux"].values.compute()) == [5.0, 3.0, 4.0, 10.0, 7.0] if drop_inputs: # The column mapping should be updated @@ -1408,7 +1408,7 @@ def test_coalesce(dask_client, drop_inputs): # The columns to drop should be dropped for col in ["flux1", "flux2", "flux3"]: - assert col not in ens._source.columns + assert col not in ens.source.columns # Test for the drop warning with pytest.warns(UserWarning): @@ -1417,7 +1417,7 @@ def test_coalesce(dask_client, drop_inputs): else: # The input columns should still be present for col in ["flux1", "flux2", "flux3"]: - assert col in ens._source.columns + assert col in ens.source.columns @pytest.mark.parametrize("zero_point", [("zp_mag", "zp_flux"), (25.0, 10**10)]) @@ -1448,19 +1448,19 @@ def test_convert_flux_to_mag(dask_client, zero_point, zp_form, out_col_name): if zp_form == "flux": ens.convert_flux_to_mag(zero_point[1], zp_form, out_col_name) - res_mag = ens._source.compute()[output_column].to_list()[0] + res_mag = ens.source.compute()[output_column].to_list()[0] assert pytest.approx(res_mag, 0.001) == 21.28925 - res_err = ens._source.compute()[output_column + "_err"].to_list()[0] + res_err = ens.source.compute()[output_column + "_err"].to_list()[0] assert pytest.approx(res_err, 0.001) == 0.355979 elif zp_form == "mag" or zp_form == "magnitude": ens.convert_flux_to_mag(zero_point[0], zp_form, out_col_name) - res_mag = ens._source.compute()[output_column].to_list()[0] + res_mag = ens.source.compute()[output_column].to_list()[0] assert pytest.approx(res_mag, 0.001) == 21.28925 - res_err = ens._source.compute()[output_column + "_err"].to_list()[0] + res_err = ens.source.compute()[output_column + "_err"].to_list()[0] assert pytest.approx(res_err, 0.001) == 0.355979 else: @@ -1626,7 +1626,7 @@ def test_batch(data_fixture, request, use_map, on): assert result is tracked_result # Make sure that divisions information is propagated if known - if parquet_ensemble._source.known_divisions and parquet_ensemble._object.known_divisions: + if parquet_ensemble.source.known_divisions and parquet_ensemble.object.known_divisions: assert result.known_divisions result = result.compute() @@ -1790,7 +1790,7 @@ def test_sf2(data_fixture, request, method, combine, sthresh, use_map=False): res_sf2 = parquet_ensemble.sf2(argument_container=arg_container, use_map=use_map) res_batch = parquet_ensemble.batch(calc_sf2, use_map=use_map, argument_container=arg_container) - if parquet_ensemble._source.known_divisions and parquet_ensemble._object.known_divisions: + if parquet_ensemble.source.known_divisions and parquet_ensemble.object.known_divisions: if not combine: assert res_sf2.known_divisions From 81bd28cb8072a06703f033155e575d8593b18489 Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Tue, 5 Dec 2023 15:47:27 -0800 Subject: [PATCH 34/35] Fix linting errors --- src/tape/ensemble.py | 4 +- tests/tape_tests/test_ensemble.py | 149 ++++++++++++++++++------------ 2 files changed, 92 insertions(+), 61 deletions(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index 9ea7234b..d06b9b71 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -806,9 +806,7 @@ def calc_nobs(self, by_band=False, label="nobs", temporary=True): band_counts["total"] = band_counts[list(band_counts.columns)].sum(axis=1) bands = band_counts.columns.values - self.object = self.object.assign( - **{label + "_" + str(band): band_counts[band] for band in bands} - ) + self.object = self.object.assign(**{label + "_" + str(band): band_counts[band] for band in bands}) if temporary: self._object_temp.extend(label + "_" + str(band) for band in bands) diff --git a/tests/tape_tests/test_ensemble.py b/tests/tape_tests/test_ensemble.py index ef61d7e0..40a0264a 100644 --- a/tests/tape_tests/test_ensemble.py +++ b/tests/tape_tests/test_ensemble.py @@ -7,7 +7,17 @@ import pytest import tape -from tape import Ensemble, EnsembleFrame, EnsembleSeries, ObjectFrame, SourceFrame, TapeFrame, TapeSeries, TapeObjectFrame, TapeSourceFrame +from tape import ( + Ensemble, + EnsembleFrame, + EnsembleSeries, + ObjectFrame, + SourceFrame, + TapeFrame, + TapeSeries, + TapeObjectFrame, + TapeSourceFrame, +) from tape.analysis.stetsonj import calc_stetson_J from tape.analysis.structure_function.base_argument_container import StructureFunctionArgumentContainer from tape.analysis.structurefunction2 import calc_sf2 @@ -132,6 +142,7 @@ def test_dataframe_constructors(data_fixture, request): amplitude = ens.batch(calc_stetson_J) assert len(amplitude) == 5 + @pytest.mark.parametrize( "data_fixture", [ @@ -154,7 +165,7 @@ def test_update_ensemble(data_fixture, request): updated_obj.update_ensemble() assert ens.object.is_dirty() == True assert updated_obj is ens.object - + # Filter the source table and have the ensemble track the updated table. updated_src = ens.source.query("psFluxErr > 0.1") assert updated_src is not ens.source @@ -166,7 +177,7 @@ def test_update_ensemble(data_fixture, request): # Compute a result to trigger a table sync obj, src = ens.compute() - assert len(obj) > 0 + assert len(obj) > 0 assert len(src) > 0 assert ens.object.is_dirty() == False assert ens.source.is_dirty() == False @@ -187,7 +198,7 @@ def test_update_ensemble(data_fixture, request): # Test update_ensemble when a frame is unlinked to its parent ensemble. result_frame.ensemble = None - assert result_frame.update_ensemble() is None + assert result_frame.update_ensemble() is None def test_available_datasets(dask_client): @@ -201,6 +212,7 @@ def test_available_datasets(dask_client): assert isinstance(datasets, dict) assert len(datasets) > 0 # Find at least one + @pytest.mark.parametrize( "data_fixture", [ @@ -225,14 +237,16 @@ def test_frame_tracking(data_fixture, request): assert ens.select_frame("object") is ens.object assert isinstance(ens.select_frame("object"), ObjectFrame) - # Construct some result frames for the Ensemble to track. Underlying data is irrelevant for + # Construct some result frames for the Ensemble to track. Underlying data is irrelevant for # this test. num_points = 100 - data = TapeFrame({ - "id": [8000 + 2 * i for i in range(num_points)], - "time": [float(i) for i in range(num_points)], - "flux": [0.5 * float(i % 4) for i in range(num_points)], - }) + data = TapeFrame( + { + "id": [8000 + 2 * i for i in range(num_points)], + "time": [float(i) for i in range(num_points)], + "flux": [0.5 * float(i % 4) for i in range(num_points)], + } + ) # Labels to give the EnsembleFrames label1, label2, label3 = "frame1", "frame2", "frame3" ens_frame1 = EnsembleFrame.from_tapeframe(data, npartitions=1, ensemble=ens, label=label1) @@ -274,7 +288,7 @@ def test_frame_tracking(data_fixture, request): assert len(ens.frames) == 4 with pytest.raises(KeyError): ens.select_frame(label3) - + # Update the ensemble with the dropped frame, and then select the frame assert ens.update_frame(ens_frame3).select_frame(label3) is ens_frame3 assert len(ens.frames) == 5 @@ -293,6 +307,7 @@ def test_frame_tracking(data_fixture, request): assert ens.update_frame(ens_frame4).select_frame(label3) is ens_frame4 assert len(ens.frames) == 6 + def test_from_rrl_dataset(dask_client): """ Test a basic load and analyze workflow from the S82 RR Lyrae Dataset @@ -775,7 +790,7 @@ def test_sync_tables(data_fixture, request, legacy): # Since we have not yet called update_ensemble, the compute call should not trigger # a sync and the source table should remain dirty. assert parquet_ensemble.source.is_dirty() - filtered_src.compute() + filtered_src.compute() assert parquet_ensemble.source.is_dirty() # Update the ensemble to use the filtered source. @@ -844,14 +859,13 @@ def test_lazy_sync_tables(parquet_ensemble, legacy): assert not parquet_ensemble.object.is_dirty() assert not parquet_ensemble.source.is_dirty() - # Modify only the source table. + # Modify only the source table. # Replace the maximum flux value with a NaN so that we will have a row to drop. max_flux = max(parquet_ensemble.source[parquet_ensemble._flux_col]) parquet_ensemble.source[parquet_ensemble._flux_col] = parquet_ensemble.source[ - parquet_ensemble._flux_col].apply( - lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float) - ) - + parquet_ensemble._flux_col + ].apply(lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float)) + assert not parquet_ensemble.object.is_dirty() assert not parquet_ensemble.source.is_dirty() @@ -898,7 +912,7 @@ def test_compute_triggers_syncing(parquet_ensemble): # Update the Ensemble so that computing the object table will trigger # a sync updated_obj.update_ensemble() - updated_obj.compute() # Now equivalent to Ensemble.object.compute() + updated_obj.compute() # Now equivalent to Ensemble.object.compute() assert not parquet_ensemble.source.is_dirty() # Test that an source table can trigger a sync that will clean a dirty @@ -914,12 +928,12 @@ def test_compute_triggers_syncing(parquet_ensemble): # Update the Ensemble so that computing the object table will trigger # a sync updated_src.update_ensemble() - updated_src.compute() # Now equivalent to Ensemble.source.compute() + updated_src.compute() # Now equivalent to Ensemble.source.compute() assert not parquet_ensemble.object.is_dirty() # Generate a new Object frame and set the Ensemble to None to # validate that we return a valid result even for untracked frames - # which cannot be synced. + # which cannot be synced. new_obj_frame = parquet_ensemble.object.dropna() new_obj_frame.ensemble = None assert len(new_obj_frame.compute()) > 0 @@ -1009,9 +1023,8 @@ def test_temporary_cols(parquet_ensemble): # Replace the maximum flux value with a NaN so that we will have a row to drop. max_flux = max(parquet_ensemble.source[parquet_ensemble._flux_col]) parquet_ensemble.source[parquet_ensemble._flux_col] = parquet_ensemble.source[ - parquet_ensemble._flux_col].apply( - lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float) - ) + parquet_ensemble._flux_col + ].apply(lambda x: np.nan if x == max_flux else x, meta=pd.Series(dtype=float)) # drop NaNs from source, source should be dirty now ens.dropna(how="any", table="source") @@ -1055,7 +1068,7 @@ def test_temporary_cols(parquet_ensemble): ) @pytest.mark.parametrize("legacy", [True, False]) def test_dropna(data_fixture, request, legacy): - """Tests dropna, using Ensemble.dropna when `legacy` is `True`, and + """Tests dropna, using Ensemble.dropna when `legacy` is `True`, and EnsembleFrame.dropna when `legacy` is `False`.""" parquet_ensemble = request.getfixturevalue(data_fixture) @@ -1082,7 +1095,9 @@ def test_dropna(data_fixture, request, legacy): # We do this on the instantiated source (pdf) and convert it back into a # SourceFrame. source_pdf.loc[valid_source_id, parquet_ensemble._flux_col] = pd.NA - parquet_ensemble.update_frame(SourceFrame.from_tapeframe(TapeSourceFrame(source_pdf), label="source", npartitions=1)) + parquet_ensemble.update_frame( + SourceFrame.from_tapeframe(TapeSourceFrame(source_pdf), label="source", npartitions=1) + ) # Try dropping NaNs from source and confirm that we did. if legacy: @@ -1117,7 +1132,9 @@ def test_dropna(data_fixture, request, legacy): # We do this on the instantiated object (pdf) and convert it back into a # ObjectFrame. object_pdf.loc[valid_object_id, parquet_ensemble.object.columns[0]] = pd.NA - parquet_ensemble.update_frame(ObjectFrame.from_tapeframe(TapeObjectFrame(object_pdf), label="object", npartitions=1)) + parquet_ensemble.update_frame( + ObjectFrame.from_tapeframe(TapeObjectFrame(object_pdf), label="object", npartitions=1) + ) # Try dropping NaNs from object and confirm that we dropped a row if legacy: @@ -1141,9 +1158,10 @@ def test_dropna(data_fixture, request, legacy): for c in new_objects_pdf.columns.values: assert new_objects_pdf.loc[i, c] == object_pdf.loc[i, c] + @pytest.mark.parametrize("legacy", [True, False]) def test_keep_zeros(parquet_ensemble, legacy): - """Test that we can sync the tables and keep objects with zero sources, using + """Test that we can sync the tables and keep objects with zero sources, using Ensemble.dropna when `legacy` is `True`, and EnsembleFrame.dropna when `legacy` is `False`.""" parquet_ensemble.keep_empty_objects = True @@ -1157,7 +1175,9 @@ def test_keep_zeros(parquet_ensemble, legacy): valid_id = pdf.index.values[1] pdf.loc[valid_id, parquet_ensemble._flux_col] = pd.NA parquet_ensemble.source = dd.from_pandas(pdf, npartitions=1) - parquet_ensemble.update_frame(SourceFrame.from_tapeframe(TapeSourceFrame(pdf), npartitions=1, label="source")) + parquet_ensemble.update_frame( + SourceFrame.from_tapeframe(TapeSourceFrame(pdf), npartitions=1, label="source") + ) # Sync the table and check that the number of objects decreased. if legacy: @@ -1327,6 +1347,7 @@ def test_select(dask_client): assert "count" not in ens.source.columns assert "something_else" not in ens.source.columns + @pytest.mark.parametrize("legacy", [True, False]) def test_assign(dask_client, legacy): """Tests assign for column-manipulation, using Ensemble.assign when `legacy` is `True`, @@ -1610,13 +1631,7 @@ def test_batch(data_fixture, request, use_map, on): result = ( parquet_ensemble.prune(10) .dropna(table="source") - .batch( - calc_stetson_J, - use_map=use_map, - on=on, - band_to_calc=None, - compute=False, - label="stetson_j") + .batch(calc_stetson_J, use_map=use_map, on=on, band_to_calc=None, compute=False, label="stetson_j") ) # Validate that the ensemble is now tracking a new result frame. @@ -1641,6 +1656,7 @@ def test_batch(data_fixture, request, use_map, on): assert pytest.approx(result.values[1]["g"], 0.001) == 1.2208577 assert pytest.approx(result.values[1]["r"], 0.001) == -0.49639028 + def test_batch_labels(parquet_ensemble): """ Test that ensemble.batch() generates unique labels for result frames when none are provided. @@ -1677,6 +1693,7 @@ def test_batch_labels(parquet_ensemble): assert frame_cnt == len(parquet_ensemble.frames) assert len(result) > 0 + def test_batch_with_custom_func(parquet_ensemble): """ Test Ensemble.batch with a custom analysis function @@ -1685,39 +1702,53 @@ def test_batch_with_custom_func(parquet_ensemble): result = parquet_ensemble.prune(10).batch(np.mean, parquet_ensemble._flux_col) assert len(result) > 0 -@pytest.mark.parametrize("custom_meta", [ - ("flux_mean", float), # A tuple representing a series - pd.Series(name="flux_mean_pandas", dtype="float64"), - TapeSeries(name="flux_mean_tape", dtype="float64")]) + +@pytest.mark.parametrize( + "custom_meta", + [ + ("flux_mean", float), # A tuple representing a series + pd.Series(name="flux_mean_pandas", dtype="float64"), + TapeSeries(name="flux_mean_tape", dtype="float64"), + ], +) def test_batch_with_custom_series_meta(parquet_ensemble, custom_meta): """ Test Ensemble.batch with various styles of output meta for a Series-style result. """ num_frames = len(parquet_ensemble.frames) - parquet_ensemble.prune(10).batch( - np.mean, parquet_ensemble._flux_col, meta=custom_meta, label="flux_mean") + parquet_ensemble.prune(10).batch(np.mean, parquet_ensemble._flux_col, meta=custom_meta, label="flux_mean") assert len(parquet_ensemble.frames) == num_frames + 1 assert len(parquet_ensemble.select_frame("flux_mean")) > 0 assert isinstance(parquet_ensemble.select_frame("flux_mean"), EnsembleSeries) -@pytest.mark.parametrize("custom_meta", [ - {"lc_id": int, "band": str, "dt": float, "sf2": float, "1_sigma": float}, - [("lc_id", int), ("band", str), ("dt", float), ("sf2", float), ("1_sigma", float)], - pd.DataFrame({ - "lc_id": pd.Series([], dtype=int), - "band": pd.Series([], dtype=str), - "dt": pd.Series([], dtype=float), - "sf2": pd.Series([], dtype=float), - "1_sigma": pd.Series([], dtype=float)}), - TapeFrame({ - "lc_id": pd.Series([], dtype=int), - "band": pd.Series([], dtype=str), - "dt": pd.Series([], dtype=float), - "sf2": pd.Series([], dtype=float), - "1_sigma": pd.Series([], dtype=float)}), -]) + +@pytest.mark.parametrize( + "custom_meta", + [ + {"lc_id": int, "band": str, "dt": float, "sf2": float, "1_sigma": float}, + [("lc_id", int), ("band", str), ("dt", float), ("sf2", float), ("1_sigma", float)], + pd.DataFrame( + { + "lc_id": pd.Series([], dtype=int), + "band": pd.Series([], dtype=str), + "dt": pd.Series([], dtype=float), + "sf2": pd.Series([], dtype=float), + "1_sigma": pd.Series([], dtype=float), + } + ), + TapeFrame( + { + "lc_id": pd.Series([], dtype=int), + "band": pd.Series([], dtype=str), + "dt": pd.Series([], dtype=float), + "sf2": pd.Series([], dtype=float), + "1_sigma": pd.Series([], dtype=float), + } + ), + ], +) def test_batch_with_custom_frame_meta(parquet_ensemble, custom_meta): """ Test Ensemble.batch with various sytles of output meta for a DataFrame-style result. @@ -1725,12 +1756,14 @@ def test_batch_with_custom_frame_meta(parquet_ensemble, custom_meta): num_frames = len(parquet_ensemble.frames) parquet_ensemble.prune(10).batch( - calc_sf2, parquet_ensemble._flux_col, meta=custom_meta, label="sf2_result") + calc_sf2, parquet_ensemble._flux_col, meta=custom_meta, label="sf2_result" + ) assert len(parquet_ensemble.frames) == num_frames + 1 assert len(parquet_ensemble.select_frame("sf2_result")) > 0 assert isinstance(parquet_ensemble.select_frame("sf2_result"), EnsembleFrame) + def test_to_timeseries(parquet_ensemble): """ Test that ensemble.to_timeseries() runs and assigns the correct metadata From a4150788840a964bb9a0abe9375c6c4c27133e6a Mon Sep 17 00:00:00 2001 From: Wilson Beebe Date: Thu, 7 Dec 2023 17:22:26 -0800 Subject: [PATCH 35/35] Address review comments, add tests --- src/tape/ensemble.py | 2 +- src/tape/ensemble_frame.py | 42 +++++++++----- tests/tape_tests/test_ensemble_frame.py | 73 ++++++++++++++++--------- 3 files changed, 77 insertions(+), 40 deletions(-) diff --git a/src/tape/ensemble.py b/src/tape/ensemble.py index d06b9b71..5d654232 100644 --- a/src/tape/ensemble.py +++ b/src/tape/ensemble.py @@ -191,7 +191,7 @@ def select_frame(self, label): Returns ------- - result: `tape.ensemeble.EnsembleFrame` + result: `tape.ensemble.EnsembleFrame` Raises ------ diff --git a/src/tape/ensemble_frame.py b/src/tape/ensemble_frame.py index 892e47bb..7a910f51 100644 --- a/src/tape/ensemble_frame.py +++ b/src/tape/ensemble_frame.py @@ -23,6 +23,17 @@ SOURCE_FRAME_LABEL = "source" # Reserved label for source table OBJECT_FRAME_LABEL = "object" # Reserved label for object table. +__all__ = [ + "EnsembleFrame", + "EnsembleSeries", + "ObjectFrame", + "SourceFrame", + "TapeFrame", + "TapeObjectFrame", + "TapeSourceFrame", + "TapeSeries", +] + class TapeArrowEngine(DaskArrowDatasetEngine): """ @@ -838,6 +849,7 @@ def from_parquet( path, index=None, columns=None, + label=None, ensemble=None, ): """Returns an EnsembleFrame constructed from loading a parquet file. @@ -848,14 +860,16 @@ def from_parquet( protocol like s3:// to read from alternative filesystems. To read from multiple files you can pass a globstring or a list of paths, with the caveat that they must all have the same protocol. - columns: `str` or `list`, optional - Field name(s) to read in as columns in the output. By default all non-index fields will - be read (as determined by the pandas parquet metadata, if present). Provide a single - field name instead of a list to read in the data as a Series. index: `str`, `list`, `False`, optional Field name(s) to use as the output frame index. Default is None and index will be inferred from the pandas parquet file metadata, if present. Use False to read all fields as columns. + columns: `str` or `list`, optional + Field name(s) to read in as columns in the output. By default all non-index fields will + be read (as determined by the pandas parquet metadata, if present). Provide a single + field name instead of a list to read in the data as a Series. + label: `str`, optional + | The label used to by the Ensemble to identify the frame. ensemble: `tape.ensemble.Ensemble`, optional | A link to the Ensemble object that owns this frame. Returns @@ -871,6 +885,7 @@ def from_parquet( split_row_groups=True, engine=TapeArrowEngine, ) + result.label = label result.ensemble = ensemble return result @@ -1063,17 +1078,16 @@ def from_dask_dataframe(cl, df, ensemble=None): return result -""" -Dask Dataframes are constructed indirectly using method dispatching and inference on the -underlying data. So to ensure our subclasses behave correctly, we register the methods -below. - -For more information, see https://docs.dask.org/en/latest/dataframe-extend.html +# Dask Dataframes are constructed indirectly using method dispatching and inference on the +# underlying data. So to ensure our subclasses behave correctly, we register the methods +# below. +# +# For more information, see https://docs.dask.org/en/latest/dataframe-extend.html +# +# The following should ensure that any Dask Dataframes which use TapeSeries or TapeFrames as their +# underlying data will be resolved as EnsembleFrames or EnsembleSeries as their parrallel +# counterparts. The underlying Dask Dataframe _meta will be a TapeSeries or TapeFrame. -The following should ensure that any Dask Dataframes which use TapeSeries or TapeFrames as their -underlying data will be resolved as EnsembleFrames or EnsembleSeries as their parrallel -counterparts. The underlying Dask Dataframe _meta will be a TapeSeries or TapeFrame. -""" get_parallel_type.register(TapeSeries, lambda _: EnsembleSeries) get_parallel_type.register(TapeFrame, lambda _: EnsembleFrame) get_parallel_type.register(TapeObjectFrame, lambda _: ObjectFrame) diff --git a/tests/tape_tests/test_ensemble_frame.py b/tests/tape_tests/test_ensemble_frame.py index 8f45e69e..1937f457 100644 --- a/tests/tape_tests/test_ensemble_frame.py +++ b/tests/tape_tests/test_ensemble_frame.py @@ -1,7 +1,15 @@ """ Test EnsembleFrame (inherited from Dask.DataFrame) creation and manipulations. """ import numpy as np import pandas as pd -from tape import ColumnMapper, EnsembleFrame, ObjectFrame, SourceFrame, TapeObjectFrame, TapeSourceFrame, TapeFrame +from tape import ( + ColumnMapper, + EnsembleFrame, + ObjectFrame, + SourceFrame, + TapeObjectFrame, + TapeSourceFrame, + TapeFrame, +) import pytest @@ -22,16 +30,16 @@ def test_from_dict(data_fixture, request): Test creating an EnsembleFrame from a dictionary and verify that dask lazy evaluation was appropriately inherited. """ _, data = request.getfixturevalue(data_fixture) - ens_frame = EnsembleFrame.from_dict(data, - npartitions=1) + ens_frame = EnsembleFrame.from_dict(data, npartitions=1) assert isinstance(ens_frame, EnsembleFrame) assert isinstance(ens_frame._meta, TapeFrame) # The calculation for finding the max flux from the data. Note that the - # inherited dask compute method must be called to obtain the result. + # inherited dask compute method must be called to obtain the result. assert ens_frame.flux.max().compute() == 80.6 + @pytest.mark.parametrize( "data_fixture", [ @@ -44,10 +52,7 @@ def test_from_pandas(data_fixture, request): """ ens, data = request.getfixturevalue(data_fixture) frame = TapeFrame(data) - ens_frame = EnsembleFrame.from_tapeframe(frame, - label=TEST_LABEL, - ensemble=ens, - npartitions=1) + ens_frame = EnsembleFrame.from_tapeframe(frame, label=TEST_LABEL, ensemble=ens, npartitions=1) assert isinstance(ens_frame, EnsembleFrame) assert isinstance(ens_frame._meta, TapeFrame) @@ -55,10 +60,26 @@ def test_from_pandas(data_fixture, request): assert ens_frame.ensemble is ens # The calculation for finding the max flux from the data. Note that the - # inherited dask compute method must be called to obtain the result. + # inherited dask compute method must be called to obtain the result. assert ens_frame.flux.max().compute() == 80.6 +def test_from_parquet(): + """ + Test creating an EnsembleFrame from a parquet file. + """ + frame = EnsembleFrame.from_parquet( + "tests/tape_tests/data/source/test_source.parquet", label=TEST_LABEL, ensemble=None + ) + assert isinstance(frame, EnsembleFrame) + assert isinstance(frame._meta, TapeFrame) + assert frame.label == TEST_LABEL + assert frame.ensemble is None + + # Validate that we loaded a non-empty frame. + assert len(frame) > 0 + + @pytest.mark.parametrize( "data_fixture", [ @@ -70,11 +91,10 @@ def test_ensemble_frame_propagation(data_fixture, request): Test ensuring that slices and copies of an EnsembleFrame or still the same class. """ ens, data = request.getfixturevalue(data_fixture) - ens_frame = EnsembleFrame.from_dict(data, - npartitions=1) + ens_frame = EnsembleFrame.from_dict(data, npartitions=1) # Set a label and ensemble for the frame and copies/transformations retain them. ens_frame.label = TEST_LABEL - ens_frame.ensemble=ens + ens_frame.ensemble = ens assert not ens_frame.is_dirty() ens_frame.set_dirty(True) @@ -113,14 +133,14 @@ def test_ensemble_frame_propagation(data_fixture, request): # Test merging two subsets of the dataframe, dropping some columns, and persisting the result. merged_frame = ens_frame.copy()[["id", "time", "error"]].merge( - ens_frame.copy()[["id", "time", "flux"]], on=["id"], suffixes=(None, "_drop_me")) + ens_frame.copy()[["id", "time", "flux"]], on=["id"], suffixes=(None, "_drop_me") + ) cols_to_drop = [col for col in merged_frame.columns if "_drop_me" in col] merged_frame = merged_frame.drop(cols_to_drop, axis=1).persist() assert isinstance(merged_frame, EnsembleFrame) assert merged_frame.label == TEST_LABEL assert merged_frame.ensemble == ens assert merged_frame.is_dirty() - # Test that head returns a subset of the underlying TapeFrame. h = ens_frame.head(5) @@ -128,19 +148,20 @@ def test_ensemble_frame_propagation(data_fixture, request): assert len(h) == 5 # Test that the inherited dask.DataFrame.compute method returns - # the underlying TapeFrame. + # the underlying TapeFrame. assert isinstance(ens_frame.compute(), TapeFrame) assert len(ens_frame) == len(ens_frame.compute()) # Set an index and then group by that index. ens_frame = ens_frame.set_index("id", drop=True) assert ens_frame.label == TEST_LABEL - assert ens_frame.ensemble == ens + assert ens_frame.ensemble == ens group_result = ens_frame.groupby(["id"]).count() assert len(group_result) > 0 assert isinstance(group_result, EnsembleFrame) assert isinstance(group_result._meta, TapeFrame) + @pytest.mark.parametrize( "data_fixture", [ @@ -195,6 +216,7 @@ def test_convert_flux_to_mag(data_fixture, request, err_col, zp_form, out_col_na assert ens_frame.label == TEST_LABEL assert ens_frame.ensemble is ens + @pytest.mark.parametrize( "data_fixture", [ @@ -204,7 +226,7 @@ def test_convert_flux_to_mag(data_fixture, request, err_col, zp_form, out_col_na def test_object_and_source_frame_propagation(data_fixture, request): """ Test that SourceFrame and ObjectFrame metadata and class type is correctly preserved across - typical Pandas operations. + typical Pandas operations. """ ens, source_file, object_file, _ = request.getfixturevalue(data_fixture) @@ -243,8 +265,8 @@ def test_object_and_source_frame_propagation(data_fixture, request): # Set an index and then group by that index. result_source_frame = result_source_frame.set_index("psFlux", drop=True) assert result_source_frame.label == SOURCE_LABEL - assert result_source_frame.ensemble == ens - assert not result_source_frame.is_dirty() # frame is still clean. + assert result_source_frame.ensemble == ens + assert not result_source_frame.is_dirty() # frame is still clean. group_result = result_source_frame.groupby(["psFlux"]).count() assert len(group_result) > 0 assert isinstance(group_result, SourceFrame) @@ -283,7 +305,7 @@ def test_object_and_source_frame_propagation(data_fixture, request): result_object_frame = result_object_frame.set_index("nobs_g", drop=True) assert result_object_frame.label == OBJECT_LABEL assert result_object_frame.ensemble == ens - assert not result_object_frame.is_dirty() # frame is still clean + assert not result_object_frame.is_dirty() # frame is still clean group_result = result_object_frame.groupby(["nobs_g"]).count() assert len(group_result) > 0 assert isinstance(group_result, ObjectFrame) @@ -291,7 +313,8 @@ def test_object_and_source_frame_propagation(data_fixture, request): # Test merging source and object frames, dropping some columns, and persisting the result. merged_frame = source_frame.copy().merge( - object_frame.copy(), on=[ens._id_col], suffixes=(None, "_drop_me")) + object_frame.copy(), on=[ens._id_col], suffixes=(None, "_drop_me") + ) cols_to_drop = [col for col in merged_frame.columns if "_drop_me" in col] merged_frame = merged_frame.drop(cols_to_drop, axis=1).persist() assert isinstance(merged_frame, SourceFrame) @@ -316,22 +339,22 @@ def test_object_and_source_joins(parquet_ensemble): # Join a SourceFrame (left) with an ObjectFrame (right) # Validate that metadata is preserved and the outputted object is a SourceFrame - joined_source = source_frame.join(object_frame, how='left') + joined_source = source_frame.join(object_frame, how="left") assert joined_source.label is SOURCE_LABEL assert type(joined_source) is SourceFrame assert joined_source.ensemble is parquet_ensemble # Now the same form of join (in terms of left/right) but produce an ObjectFrame. This is # because frame1.join(frame2) will yield frame1's type regardless of left vs right. - assert type(object_frame.join(source_frame, how='right')) is ObjectFrame + assert type(object_frame.join(source_frame, how="right")) is ObjectFrame # Join an ObjectFrame (left) with a SourceFrame (right) # Validate that metadata is preserved and the outputted object is an ObjectFrame - joined_object = object_frame.join(source_frame, how='left') + joined_object = object_frame.join(source_frame, how="left") assert joined_object.label is OBJECT_LABEL assert type(joined_object) is ObjectFrame assert joined_object.ensemble is parquet_ensemble # Now the same form of join (in terms of left/right) but produce a SourceFrame. This is # because frame1.join(frame2) will yield frame1's type regardless of left vs right. - assert type(source_frame.join(object_frame, how='right')) is SourceFrame \ No newline at end of file + assert type(source_frame.join(object_frame, how="right")) is SourceFrame