# -- import packages: ---------------------------------------------------------
import ABCParse
import anndata
import dataclasses
import h5py
import logging
import numpy as np
import os
import pandas as pd
import pathlib
import sklearn.decomposition
import sklearn.preprocessing
import warnings
# -- import local dependencies: -----------------------------------------------
from .. import io
from ._figshare_downloader import figshare_downloader, zenodo_file_downloader
# -- set type hints: ----------------------------------------------------------
from typing import Dict, Optional, Tuple, Union
# -- configure logger: --------------------------------------------------------
logger = logging.getLogger(__name__)
# -- variant registry: --------------------------------------------------------
_VARIANTS = {
None: {
"figshare_id": 55415231,
"stem": "larry",
"legacy_fnames": ("larry.h5ad",),
},
"unprocessed": {
"figshare_id": 52612805,
"stem": "larry_unprocessed",
"legacy_fnames": ("larry_fate_prediction.h5ad",),
# Prebuilt "larry_unprocessed.processed.h5ad" exists in Zenodo but
# deliberately not registered here. 2.93 GB vs. 2.16 GB for the raw
# file. Local preprocessing takes ~30s; fetching it costs ~285s of
# extra transfer at ~2.7 MB/s (measured). Set "processed_fname" to
# re-enable.
},
}
_VARIANT_ALIASES = {"fate_prediction": "unprocessed"}
def _resolve_variant(variant: Optional[str]) -> Optional[str]:
"""Canonicalize ``variant``, warning once on a deprecated alias.
Idempotent: re-resolving an already-canonical name never re-warns, which lets
both ``larry()`` and ``LARRYInVitroDataset.__init__`` call it without emitting
the warning twice.
"""
if variant in _VARIANT_ALIASES:
canonical = _VARIANT_ALIASES[variant]
message = (
f"variant={variant!r} is deprecated and will be removed in a future "
f"release; use variant={canonical!r} instead. Note that this variant is "
f"the unprocessed input (25,289 genes, no precomputed X_pca), not the "
f"gene-filtered object its upstream filename suggests."
)
warnings.warn(message, DeprecationWarning, stacklevel=3)
# DeprecationWarning is hidden by default outside __main__, so also log it.
logger.warning(message)
return canonical
if variant not in _VARIANTS:
valid = sorted(
repr(key) for key in list(_VARIANTS) + list(_VARIANT_ALIASES)
)
raise ValueError(
f"Unknown variant: {variant!r}. Valid options: {', '.join(valid)}."
)
return variant
# -- lightweight, read-only h5ad introspection: -------------------------------
@dataclasses.dataclass(frozen=True)
class _H5ADProbe:
"""Metadata-only summary of an ``.h5ad`` file.
Reads HDF5 metadata for a handful of nodes -- a few KB regardless of whether
the file is 80 MB or 5.3 GB. This is what makes it viable to ask "is this
cached file raw or already processed?" without loading it.
"""
path: pathlib.Path
readable: bool = False
n_obs: Optional[int] = None
n_vars: Optional[int] = None
obsm_keys: Tuple[str, ...] = ()
obs_columns: Tuple[str, ...] = ()
pca_dim: Optional[int] = None
error: Optional[str] = None
def _decode(value) -> str:
return value.decode() if isinstance(value, bytes) else str(value)
def _frame_shape_and_columns(node) -> Tuple[Optional[int], Tuple[str, ...]]:
"""Length and column names of an h5ad-encoded DataFrame, reading no column
data."""
if isinstance(node, h5py.Dataset): # legacy structured-array layout
return int(node.shape[0]), tuple(node.dtype.names or ())
index_key = _decode(node.attrs.get("_index", "_index"))
n = None
if index_key in node:
index_node = node[index_key]
if isinstance(index_node, h5py.Dataset):
n = int(index_node.shape[0])
elif "codes" in index_node: # categorical index
n = int(index_node["codes"].shape[0])
columns = node.attrs.get("column-order")
if columns is None:
columns = [key for key in node.keys() if key != index_key]
return n, tuple(_decode(col) for col in columns)
def _probe_h5ad(path: Union[str, pathlib.Path]) -> _H5ADProbe:
"""Open an ``.h5ad`` read-only and report its structure."""
path = pathlib.Path(path)
if (not path.exists()) or path.stat().st_size == 0:
return _H5ADProbe(path=path, error="missing or empty")
try:
with h5py.File(path, "r") as f:
n_obs, obs_columns = _frame_shape_and_columns(f["obs"])
n_vars, _ = _frame_shape_and_columns(f["var"])
obsm = f.get("obsm")
obsm_keys = tuple(obsm.keys()) if obsm is not None else ()
pca_dim = None
if obsm is not None and "X_pca" in obsm:
pca_node = obsm["X_pca"]
if isinstance(pca_node, h5py.Dataset) and pca_node.ndim == 2:
pca_dim = int(pca_node.shape[1])
return _H5ADProbe(
path=path,
readable=True,
n_obs=n_obs,
n_vars=n_vars,
obsm_keys=obsm_keys,
obs_columns=obs_columns,
pca_dim=pca_dim,
)
except (OSError, KeyError, ValueError) as e:
# truncated download, or non-HDF5 payload such as a WAF challenge page
return _H5ADProbe(path=path, error=f"{type(e).__name__}: {e}")
# -- atomic write helpers: ----------------------------------------------------
def _tmp_sibling(path: pathlib.Path, suffix: str) -> pathlib.Path:
"""Hidden sibling path in the same directory, so ``os.replace`` stays atomic."""
return path.with_name(f".{path.name}.{suffix}")
def _write_h5ad_atomic(adata: anndata.AnnData, write_path: pathlib.Path) -> None:
"""Write to a temp sibling, then move into place.
Preprocessing can fail partway -- CytoTRACE annotation pulls two CSVs over the
network -- and without this a half-written file would be left at the processed
path and silently returned by every later call.
"""
tmp = _tmp_sibling(write_path, "tmp.h5ad")
try:
adata.write_h5ad(tmp)
os.replace(tmp, write_path)
except BaseException:
tmp.unlink(missing_ok=True)
raise
# -- CytoTRACE annotation: ----------------------------------------------------
_CYTOTRACE_FILES = {
"obs": (54312011, "larry.ct_obs_df.csv"),
"var": (54312008, "larry.ct_var_df.csv"),
}
# Columns the annotation contributes to ``adata.obs`` (used to validate a cache).
CYTOTRACE_OBS_COLS = ("ct_pseudotime",)
def _coerce_annotation_frame(df: pd.DataFrame) -> pd.DataFrame:
"""String index, but leave numeric columns numeric.
The previous implementation cast *every* column to ``str``, which is why
``ct_pseudotime`` had to be cast back to float on read.
"""
df = df.copy()
df.index = df.index.astype(str)
for col in df.columns:
numeric = pd.to_numeric(df[col], errors="coerce")
df[col] = numeric if numeric.notna().all() else df[col].astype(str)
return df
def _download_cytotrace_annotations(
data_dir: Union[str, pathlib.Path],
force: bool = False,
) -> Dict[str, pd.DataFrame]:
"""Fetch (once) and load the precomputed CytoTRACE obs/var annotations."""
data_dir = pathlib.Path(data_dir)
frames = {}
for key, (figshare_id, fname) in _CYTOTRACE_FILES.items():
path = data_dir.joinpath(fname)
if force or (not path.exists()) or path.stat().st_size == 0:
logger.info(f"Downloading CytoTRACE {key} annotations -> {path}")
figshare_downloader(figshare_id=figshare_id, write_path=path)
else:
logger.debug(f"Using cached CytoTRACE {key} annotations: {path}")
frames[key] = _coerce_annotation_frame(pd.read_csv(path, index_col=0))
return frames
def _merge_annotations(
target: pd.DataFrame,
annotations: pd.DataFrame,
axis_name: str,
) -> pd.DataFrame:
"""Index-safe left-join of annotations onto ``obs``/``var``.
Replaces ``pd.concat(..., axis=1)``, which took the *union* of the two indexes:
any annotation key absent from the target silently lengthened the frame (an
AnnData assignment error), and re-running duplicated column names, after which
``adata.obs['ct_pseudotime']`` returns a DataFrame rather than a Series.
Reindexing to the target keeps its length fixed and leaves NaN wherever an
annotation is unavailable. The var annotations in particular cover only the
gene-filtered subset, so partial coverage is normal and not an error.
"""
target = target.copy()
target.index = target.index.astype(str)
overlap = int(target.index.isin(annotations.index).sum())
if overlap == 0:
logger.warning(
f"No CytoTRACE {axis_name} annotations matched: none of {len(target)} "
f"{axis_name} names are present in the annotation table. The columns "
f"will be added but left empty."
)
elif overlap < len(target):
logger.info(
f"CytoTRACE {axis_name} annotations cover {overlap} of {len(target)} "
f"{axis_name}; the remainder will be NaN."
)
# Left join: never changes len(target), never duplicates columns.
aligned = annotations.reindex(target.index)
for col in aligned.columns:
if col in target.columns:
logger.debug(f"Overwriting existing {axis_name} column {col!r}.")
target[col] = _h5ad_safe_column(aligned[col]).values
return target
def _h5ad_safe_column(values: pd.Series):
"""Coerce a reindexed column into something anndata can serialize.
Partial coverage introduces NaN wherever an annotation was unavailable. A
bool column then upcasts to ``object`` holding True/False/NaN, which h5py
rejects with "Can't implicitly convert non-string objects to strings" -- the
var annotations cover only the gene-filtered subset, so this is the normal
case for the unfiltered variant, not an edge case.
Numeric columns represent gaps as NaN natively and are left alone. Anything
else becomes a categorical of strings, which encodes missingness properly.
"""
if pd.api.types.is_bool_dtype(values):
return values # a real bool dtype cannot contain NaN
if pd.api.types.is_numeric_dtype(values):
return values
present = values.notna()
if present.all():
# Fully covered object column: plain strings write cleanly.
return values.astype(str)
as_str = pd.Series(np.nan, index=values.index, dtype=object)
as_str[present] = values[present].astype(str)
return pd.Series(pd.Categorical(as_str), index=values.index)
def _annotate_larry_cytotrace(
adata: anndata.AnnData,
data_dir: Union[str, pathlib.Path],
force: bool = False,
) -> anndata.AnnData:
"""Annotate ``adata`` with precomputed CytoTRACE obs/var values."""
frames = _download_cytotrace_annotations(data_dir, force=force)
adata.obs = _merge_annotations(adata.obs, frames["obs"], axis_name="obs")
adata.var = _merge_annotations(adata.var, frames["var"], axis_name="var")
return adata
# -- Controller class: --------------------------------------------------------
class LARRYInVitroDataset(ABCParse.ABCParse):
N_PCS = 50
PCA_RANDOM_STATE = 0
VARIANTS = _VARIANTS
VARIANT_ALIASES = _VARIANT_ALIASES
# Retained for backwards compatibility with any external caller.
FIGSHARE_IDS = {
None: 55415231,
"gene_filtered": 52612805,
"fate_prediction": 52612805,
}
def __init__(
self,
data_dir=os.getcwd(),
variant: Optional[str] = None,
filter_genes: bool = True,
reduce_dimensions: bool = True,
cytotrace: bool = True,
force_download: bool = False,
force_preprocess: bool = False,
*args,
**kwargs,
):
# Normalize before __parse__ so self._variant is always canonical.
variant = _resolve_variant(variant)
self.__parse__(locals())
# -- directories: ---------------------------------------------------------
@property
def _scdiffeq_parent_data_dir(self) -> pathlib.Path:
path = pathlib.Path(self._data_dir).joinpath("scdiffeq_data")
path.mkdir(parents=True, exist_ok=True)
return path
@property
def data_dir(self) -> pathlib.Path:
path = self._scdiffeq_parent_data_dir.joinpath("larry")
path.mkdir(parents=True, exist_ok=True)
return path
# -- variant spec: --------------------------------------------------------
@property
def _spec(self) -> Dict:
return self.VARIANTS[self._variant]
@property
def _stem(self) -> str:
return self._spec["stem"]
@property
def _figshare_id(self) -> int:
return self._spec["figshare_id"]
@property
def _DO_PREPROCESSING(self) -> bool:
return any([self._filter_genes, self._reduce_dimensions, self._cytotrace])
@property
def _pp_tag(self) -> str:
"""Filename tag for non-default flag combinations ('' for the default).
Without this, ``larry(reduce_dimensions=False)`` would write a PCA-less
file to the shared processed path and poison the cache for every other
caller.
"""
tags = []
if not self._filter_genes:
tags.append("nofilter")
if not self._reduce_dimensions:
tags.append("nopca")
if not self._cytotrace:
tags.append("noct")
return f".{'-'.join(tags)}" if tags else ""
# -- paths: ---------------------------------------------------------------
@property
def raw_h5ad_path(self) -> pathlib.Path:
return self.data_dir.joinpath(f"_{self._stem}.raw.h5ad")
@property
def processed_h5ad_path(self) -> pathlib.Path:
return self.data_dir.joinpath(f"{self._stem}.processed{self._pp_tag}.h5ad")
@property
def h5ad_path(self) -> pathlib.Path:
"""Path the returned object is read from."""
if self._DO_PREPROCESSING:
return self.processed_h5ad_path
return self.raw_h5ad_path
def _model_path(self, kind: str) -> pathlib.Path:
"""Path for a fitted scaler/PCA.
Default variant keeps the bare ``scaler.pkl`` / ``pca.pkl`` names
that the published quickstart notebook loads. Other variants are
namespaced so they no longer silently overwrite each other's models.
"""
prefix = "" if self._variant is None else f"{self._stem}."
return self.data_dir.joinpath(f"{prefix}{kind}{self._pp_tag}.pkl")
# -- legacy cache migration: ----------------------------------------------
def _migrate_legacy_cache(self) -> None:
"""Adopt a pre-existing ``larry.h5ad`` rather than re-downloading GBs.
Existing installs hold a single file that may be raw *or* processed
depending on how it got there. Classified from HDF5 metadata instead of
assuming. The move is an ``os.replace`` within one directory: instant
and atomic even at 5.3 GB.
"""
if getattr(self, "_migrated", False):
return
self._migrated = True
for legacy_name in self._spec["legacy_fnames"]:
legacy_path = self.data_dir.joinpath(legacy_name)
if not legacy_path.exists():
continue
if legacy_path in (self.raw_h5ad_path, self.processed_h5ad_path):
continue
probe = _probe_h5ad(legacy_path)
if not probe.readable:
logger.warning(
f"Ignoring unreadable legacy cache file {legacy_path} "
f"({probe.error}). Delete it to reclaim disk space."
)
continue
# Width is the discriminator, not mere presence: a raw upload may also
# carry an X_pca of some other width.
looks_processed = probe.pca_dim == self.N_PCS
if looks_processed and self._pp_tag:
logger.warning(
f"Legacy cache {legacy_path.name} appears to be preprocessed with "
f"default settings, but non-default preprocessing was requested; "
f"it cannot be reused. Leaving it in place."
)
continue
target = self.processed_h5ad_path if looks_processed else self.raw_h5ad_path
kind = "processed" if looks_processed else "raw"
if target.exists():
logger.info(
f"{target.name} already exists; leaving legacy file "
f"{legacy_path.name} in place (safe to delete)."
)
continue
logger.info(
f"Migrating legacy cache: {legacy_path.name} -> {target.name} "
f"(detected as {kind}: n_obs={probe.n_obs}, n_vars={probe.n_vars}, "
f"X_pca width={probe.pca_dim})"
)
os.replace(legacy_path, target)
# -- validation: ----------------------------------------------------------
def _validate_processed(self, path: pathlib.Path) -> Tuple[bool, str]:
"""Structural check on a cached processed file. Returns ``(is_valid, reason)``."""
probe = _probe_h5ad(path)
if not probe.readable:
return False, f"unreadable ({probe.error})"
if self._reduce_dimensions:
if probe.pca_dim is None:
return False, "missing obsm['X_pca']"
if probe.pca_dim != self.N_PCS:
return False, (
f"obsm['X_pca'] has {probe.pca_dim} components "
f"(expected {self.N_PCS})"
)
if self._cytotrace:
missing = set(CYTOTRACE_OBS_COLS) - set(probe.obs_columns)
if missing:
return False, f"missing CytoTRACE obs columns: {sorted(missing)}"
# Cross-check against raw only when raw is present, so a valid cache is
# never invalidated merely because the raw file is unavailable.
raw_probe = _probe_h5ad(self.raw_h5ad_path)
if raw_probe.readable and probe.n_obs != raw_probe.n_obs:
return False, (
f"n_obs mismatch (processed={probe.n_obs}, raw={raw_probe.n_obs})"
)
return True, "ok"
# -- raw acquisition: -----------------------------------------------------
def download(self) -> None:
"""Download the raw file for this variant if it is not already cached."""
if self.raw_h5ad_path.exists() and not self._force_download:
return
logger.info(
f"Downloading LARRY (variant={self._variant!r}, "
f"figshare_id={self._figshare_id}) -> {self.raw_h5ad_path}"
)
figshare_downloader(
figshare_id=self._figshare_id,
write_path=self.raw_h5ad_path,
)
# One download per instance, so a second property access does not re-fetch.
self._force_download = False
@property
def raw_adata(self) -> anndata.AnnData:
if not hasattr(self, "_raw_adata"):
self._migrate_legacy_cache()
self.download()
logger.info(f"Reading raw data from {self.raw_h5ad_path}")
self._raw_adata = anndata.read_h5ad(self.raw_h5ad_path)
return self._raw_adata
# -- preprocessing steps: -------------------------------------------------
def _gene_filtering(self, adata: anndata.AnnData) -> anndata.AnnData:
if "use_genes" not in adata.var.columns:
# The gene-filtered variant is already filtered and carries no
# use_genes column. Warn rather than raise, so this stays a no-op
# instead of breaking the call.
logger.warning(
f"var['use_genes'] not found in {self.raw_h5ad_path.name}; skipping "
f"gene filtering (this variant is likely already gene-filtered)."
)
return adata
use_genes = adata.var["use_genes"]
if use_genes.dtype != bool:
use_genes = (
use_genes.astype(str).str.strip().str.lower().isin(["true", "1"])
)
logger.info(f"Filtering genes: {adata.n_vars} -> {int(use_genes.sum())}")
return adata[:, use_genes.values].copy()
def _dimension_reduction(self, adata: anndata.AnnData) -> None:
"""Scale and run PCA, in place on ``adata.obsm``."""
scaler = sklearn.preprocessing.StandardScaler()
pca = sklearn.decomposition.PCA(
n_components=self.N_PCS,
random_state=self.PCA_RANDOM_STATE,
)
n_bytes = adata.n_obs * adata.n_vars * 8
if n_bytes > 8e9:
logger.warning(
f"Dense scaling of {adata.n_obs} x {adata.n_vars} needs roughly "
f"{n_bytes / 1e9:.1f} GB of RAM, and the same again on disk in "
f"obsm['X_scaled']. Consider filter_genes=True."
)
X_raw = adata.X
if not isinstance(X_raw, np.ndarray):
X_raw = X_raw.toarray()
adata.obsm["X_scaled"] = scaler.fit_transform(X_raw)
adata.obsm["X_pca"] = pca.fit_transform(adata.obsm["X_scaled"])
io.write_pickle(obj=scaler, path=self._model_path("scaler"))
io.write_pickle(obj=pca, path=self._model_path("pca"))
def _preprocess(self, adata: anndata.AnnData) -> anndata.AnnData:
"""Run each requested step, then persist the result.
Ordering is load-bearing: CytoTRACE first, so its var-level annotations
survive gene filtering; PCA last, so the scaler and PCA are fit on the
filtered gene space.
"""
logger.info(f"Preprocessing LARRY (variant={self._variant!r})...")
if self._cytotrace:
adata = _annotate_larry_cytotrace(
adata=adata,
data_dir=self.data_dir,
force=self._force_download,
)
if self._filter_genes:
adata = self._gene_filtering(adata)
if self._reduce_dimensions:
self._dimension_reduction(adata)
logger.info(f"Writing processed data to {self.processed_h5ad_path}")
_write_h5ad_atomic(adata, self.processed_h5ad_path)
return adata
def _try_download_processed(self) -> bool:
"""Fetch a prebuilt processed artifact instead of recomputing it.
Only valid when the requested flags match the defaults the artifact was
built with -- otherwise the prebuilt object is not what was asked for and
preprocessing has to run locally. Returns ``False`` whenever the artifact
is unavailable, so this is always an optimization, never a requirement.
"""
processed_fname = self._spec.get("processed_fname")
if not processed_fname or self._pp_tag:
return False
logger.info(f"Checking for a prebuilt {processed_fname}...")
if not zenodo_file_downloader(
filename=processed_fname,
write_path=self.processed_h5ad_path,
):
return False
is_valid, reason = self._validate_processed(self.processed_h5ad_path)
if not is_valid:
logger.warning(
f"Prebuilt {processed_fname} failed validation ({reason}); "
f"falling back to local preprocessing."
)
self.processed_h5ad_path.unlink(missing_ok=True)
return False
return True
def _ensure_processed(self) -> None:
"""Guarantee that ``self.h5ad_path`` exists and is valid."""
self._migrate_legacy_cache()
if not self._DO_PREPROCESSING:
self.download()
return
if self._force_download and self.processed_h5ad_path.exists():
logger.info(
f"force_download=True: discarding stale "
f"{self.processed_h5ad_path.name}"
)
self.processed_h5ad_path.unlink()
if self.processed_h5ad_path.exists() and not self._force_preprocess:
is_valid, reason = self._validate_processed(self.processed_h5ad_path)
if is_valid:
return
logger.warning(
f"Cached {self.processed_h5ad_path.name} failed validation "
f"({reason}); regenerating from raw."
)
self.processed_h5ad_path.unlink()
if self._try_download_processed():
return
adata = self.raw_adata # downloads only if the raw file is absent
self._preprocess(adata=adata)
# Release the large intermediates before re-reading from disk.
del adata
if hasattr(self, "_raw_adata"):
del self._raw_adata
def _safe_read(self, path: Optional[pathlib.Path] = None) -> anndata.AnnData:
path = self.h5ad_path if path is None else path
logger.info(f"Loading data from {path}")
try:
adata = anndata.read_h5ad(path)
except Exception as e:
logger.error(f"Error loading data from {path}: {e}")
raise
# Defensive: files written before the CytoTRACE dtype fix stored these as str.
for col in CYTOTRACE_OBS_COLS:
if col in adata.obs.columns:
adata.obs[col] = pd.to_numeric(adata.obs[col], errors="coerce")
adata.obs.index.name = "index"
return adata
@property
def adata(self) -> anndata.AnnData:
"""Preprocessed LARRY in vitro AnnData for the requested variant."""
if not hasattr(self, "_adata"):
self._ensure_processed()
self._adata = self._safe_read()
return self._adata
[docs]
def larry(
data_dir: str = os.getcwd(),
variant: Optional[str] = None,
filter_genes: bool = True,
reduce_dimensions: bool = True,
cytotrace: bool = True,
force_download: bool = False,
force_preprocess: bool = False,
) -> anndata.AnnData:
"""LARRY in vitro dataset
The raw download and the preprocessed result are cached as separate files, so
a dataset obtained by any means (including a manual download) is still
preprocessed on first use.
Args:
data_dir: str, default=os.getcwd()
Path to the directory where the data will be saved.
variant: Optional[str], default=None
Dataset variant.
``None`` (default) is the biology-rich object: 130,887 x 2,492, already
gene-filtered, shipping a precomputed ``X_pca``, ``X_umap`` and
``X_scaled``.
``"unprocessed"`` is the upstream input: 130,887 x 25,289 with no
``X_pca``, carrying a ``use_genes`` column that preprocessing filters
by (down to 2,447 genes). ``X_pca`` is computed locally, or downloaded
prebuilt when available.
``variant="fate_prediction"`` is a deprecated alias for
``"unprocessed"`` and will be removed in a future release. Despite its
upstream filename (``...in_vitro.gene_filtered.h5ad``), that object is
the unfiltered one.
filter_genes: bool, default=True
Whether to subset to ``adata.var['use_genes']``. A no-op for variants
that are already gene-filtered.
reduce_dimensions: bool, default=True
Whether to scale and run PCA (50 components, ``random_state=0``).
cytotrace: bool, default=True
Whether to annotate with precomputed CytoTRACE values. Applied
independently of the other preprocessing flags.
force_download: bool, default=False
Re-download the raw file and regenerate the processed file.
force_preprocess: bool, default=False
Regenerate the processed file from the cached raw file without
re-downloading it.
Returns:
anndata.AnnData: Preprocessed AnnData object.
"""
variant = _resolve_variant(variant)
data_handler = LARRYInVitroDataset(
data_dir=data_dir,
variant=variant,
filter_genes=filter_genes,
reduce_dimensions=reduce_dimensions,
cytotrace=cytotrace,
force_download=force_download,
force_preprocess=force_preprocess,
)
return data_handler.adata