From 32f61ab1b696e9419d42c1286c76177819856a8c Mon Sep 17 00:00:00 2001 From: Jayaram Kancherla Date: Fri, 25 Oct 2024 15:36:10 -0700 Subject: [PATCH] Switch to pybind11 and a complete rewrite of the package (#33) * Complete overhaul of the codebase using pybind11 * Streamlined readers for R data types * Updated API for all classes and methods * Updated documentation and tests. --- .github/workflows/build-docs.yml | 2 +- .github/workflows/publish-pypi.yml | 2 +- .gitignore | 3 + .pre-commit-config.yaml | 12 +- AUTHORS.md | 2 +- CHANGELOG.md | 8 +- README.md | 107 ++++-- docs/tutorial.md | 72 ++-- lib/CMakeLists.txt | 2 +- lib/src/rdswrapper.cpp | 4 +- setup.cfg | 13 +- setup.py | 5 +- src/rds2py/PyRdsReader.py | 101 ++++- src/rds2py/__init__.py | 12 +- src/rds2py/generics.py | 140 +++++++ src/rds2py/granges.py | 151 -------- src/rds2py/interface.py | 258 ------------- src/rds2py/parser.py | 47 --- src/rds2py/pdf.py | 72 ---- src/rds2py/rdsutils.py | 67 ++++ src/rds2py/read_atomic.py | 115 ++++++ src/rds2py/read_dict.py | 49 +++ src/rds2py/read_factor.py | 50 +++ src/rds2py/read_frame.py | 92 +++++ src/rds2py/read_granges.py | 113 ++++++ src/rds2py/read_mae.py | 80 ++++ src/rds2py/read_matrix.py | 209 +++++++++++ src/rds2py/read_rle.py | 50 +++ src/rds2py/read_sce.py | 86 +++++ src/rds2py/read_se.py | 110 ++++++ tests/data/.Rhistory | 512 -------------------------- tests/data/atomic_ints_with_names.rds | Bin 0 -> 264 bytes tests/data/data.frame.rds | Bin 0 -> 330 bytes tests/data/generate_files.R | 71 ++++ tests/data/ranged_se.rds | Bin 0 -> 10775 bytes tests/data/scalar_int.rds | Bin 0 -> 50 bytes tests/data/simple_factors.rds | Bin 0 -> 107 bytes tests/data/simple_list.rds | Bin 0 -> 161 bytes tests/data/simple_mae.rds | Bin 0 -> 751 bytes tests/data/simple_rle.rds | Bin 0 -> 197 bytes tests/data/simple_sce.rds | Bin 0 -> 86567 bytes tests/data/sumexpt.rds | Bin 0 -> 8966 bytes tests/test_atomic-attr.py | 18 - tests/test_atomic-bool.py | 23 -- tests/test_atomic-double.py | 16 - tests/test_atomic-int.py | 16 - tests/test_atomic-str.py | 23 -- tests/test_atomics.py | 103 ++++++ tests/test_dict.py | 52 +++ tests/test_factors.py | 16 + tests/test_frames.py | 24 ++ tests/test_granges.py | 27 +- tests/test_interface_matrix.py | 34 -- tests/test_list.py | 47 --- tests/test_mae.py | 17 + tests/test_matrices.py | 35 ++ tests/test_rle.py | 18 + tests/test_s4.py | 8 +- tests/test_sce.py | 17 + tests/test_se.py | 25 ++ 60 files changed, 1772 insertions(+), 1364 deletions(-) create mode 100644 src/rds2py/generics.py delete mode 100644 src/rds2py/granges.py delete mode 100644 src/rds2py/interface.py delete mode 100644 src/rds2py/parser.py delete mode 100644 src/rds2py/pdf.py create mode 100644 src/rds2py/rdsutils.py create mode 100644 src/rds2py/read_atomic.py create mode 100644 src/rds2py/read_dict.py create mode 100644 src/rds2py/read_factor.py create mode 100644 src/rds2py/read_frame.py create mode 100644 src/rds2py/read_granges.py create mode 100644 src/rds2py/read_mae.py create mode 100644 src/rds2py/read_matrix.py create mode 100644 src/rds2py/read_rle.py create mode 100644 src/rds2py/read_sce.py create mode 100644 src/rds2py/read_se.py delete mode 100644 tests/data/.Rhistory create mode 100644 tests/data/atomic_ints_with_names.rds create mode 100644 tests/data/data.frame.rds create mode 100644 tests/data/ranged_se.rds create mode 100644 tests/data/scalar_int.rds create mode 100644 tests/data/simple_factors.rds create mode 100644 tests/data/simple_list.rds create mode 100644 tests/data/simple_mae.rds create mode 100644 tests/data/simple_rle.rds create mode 100644 tests/data/simple_sce.rds create mode 100644 tests/data/sumexpt.rds delete mode 100644 tests/test_atomic-attr.py delete mode 100644 tests/test_atomic-bool.py delete mode 100644 tests/test_atomic-double.py delete mode 100644 tests/test_atomic-int.py delete mode 100644 tests/test_atomic-str.py create mode 100644 tests/test_atomics.py create mode 100644 tests/test_dict.py create mode 100644 tests/test_factors.py create mode 100644 tests/test_frames.py delete mode 100644 tests/test_interface_matrix.py delete mode 100644 tests/test_list.py create mode 100644 tests/test_mae.py create mode 100644 tests/test_matrices.py create mode 100644 tests/test_rle.py create mode 100644 tests/test_sce.py create mode 100644 tests/test_se.py diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 2717d14..1381761 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -29,7 +29,7 @@ jobs: - name: Build docs run: | python setup.py build_ext --inplace - cp build/lib*/rds2py/rds_parser* src/rds2py/ + cp build/lib*/rds2py/lib_rds_parser* src/rds2py/ tox -e docs touch ./docs/_build/html/.nojekyll diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index bdbb89c..abc9222 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -28,7 +28,7 @@ jobs: build_macosx_x86_64: name: Build wheels for macosx x86_64 - runs-on: macos-11 + runs-on: macos-13 steps: - name: Check out repository uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index e9e1e9b..6b070ba 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ MANIFEST .venv*/ .conda*/ .python-version + +extern/rds2cpp* +src/rds2py/lib/parser.cpp diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 96cab27..37f69d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,11 +25,11 @@ repos: args: [--in-place, --wrap-descriptions=120, --wrap-summaries=120] # --config, ./pyproject.toml -- repo: https://github.com/psf/black - rev: 24.8.0 - hooks: - - id: black - language_version: python3 +# - repo: https://github.com/psf/black +# rev: 24.8.0 +# hooks: +# - id: black +# language_version: python3 - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. @@ -37,6 +37,8 @@ repos: hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] + # Run the formatter. + - id: ruff-format ## If like to embrace black styles even in the docs: # - repo: https://github.com/asottile/blacken-docs diff --git a/AUTHORS.md b/AUTHORS.md index f635b91..f21a024 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,3 +1,3 @@ # Contributors -* jkanche [jayaram.kancherla@gmail.com](mailto:jayaram.kancherla@gmail.com) +* Jayaram Kancherla [jayaram.kancherla@gmail.com](mailto:jayaram.kancherla@gmail.com) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac57df2..7790bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,17 @@ # Changelog -## Development +## Version 0.5.0 -- Fix github issue with showing incorrect package version on github pages. +- Complete overhaul of the codebase using pybind11 +- Streamlined readers for R data types +- Updated API for all classes and methods +- Updated documentation and tests. ## Version 0.4.5 - Switch to pybind11 to implementing the bindings to rds2cpp. - Update tests, documentation and actions. +- Fix github issue with showing incorrect package version on github pages. ## Version 0.4.4 diff --git a/README.md b/README.md index f0ca25d..968f315 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,39 @@ # rds2py -Parse and construct Python representations for datasets stored in RDS files. `rds2py` supports a few base classes from R and Bioconductor's `SummarizedExperiment` and `SingleCellExperiment` S4 classes. **_This is possible because of [Aaron's rds2cpp library](https://github.com/LTLA/rds2cpp)._** - -The package uses memory views (except for strings) to access the same memory from C++ in Python (through Cython of course). This is especially useful for large datasets so we don't make multiple copies of data. - -## Install +Parse and construct Python representations for datasets stored in RDS files. `rds2py` supports various base classes from R, and Bioconductor's `SummarizedExperiment` and `SingleCellExperiment` S4 classes. ***For more details, check out [rds2cpp library](https://github.com/LTLA/rds2cpp).*** + +> **Important Version Notice** +> +> Version 0.5.0 brings major changes to the package: +> - Complete overhaul of the codebase using pybind11 +> - Streamlined readers for R data types +> - Updated API for all classes and methods +> +> Please refer to the [documentation](https://biocpy.github.io/rds2py/) for the latest usage guidelines. Previous versions may have incompatible APIs. + +The package provides: + +- Efficient parsing of RDS files with *minimal* memory overhead +- Support for R's basic data types and complex S4 objects + - Vectors (numeric, character, logical) + - Factors + - Data frames + - Matrices (dense and sparse) + - Run-length encoded vectors (Rle) +- Conversion to appropriate Python/NumPy/SciPy data structures + - dgCMatrix (sparse column matrix) + - dgRMatrix (sparse row matrix) + - dgTMatrix (sparse triplet matrix) +- Preservation of metadata and attributes from R objects +- Integration with BiocPy ecosystem for Bioconductor classes + - SummarizedExperiment + - RangedSummarizedExperiment + - SingleCellExperiment + - GenomicRanges + - MultiAssayExperiment + +## Installation Package is published to [PyPI](https://pypi.org/project/rds2py/) @@ -16,57 +44,64 @@ Package is published to [PyPI](https://pypi.org/project/rds2py/) pip install rds2py ``` -## Usage - -If you do not have an RDS object handy, feel free to download one from [single-cell-test-files](https://github.com/jkanche/random-test-files/releases). +## Quick Start ```python -from rds2py import as_summarized_experiment, read_rds +from rds2py import read_rds -r_obj = read_rds() +# Read any RDS file +r_obj = read_rds("path/to/file.rds") ``` -This `r_obj` holds a dictionary representation of the RDS file, we can now transform this object into Python representations. - -`rObj` always contains two keys +## Usage -- `data`: If atomic entities, contains the NumPy view of the array. -- `attributes`: Additional properties available for the object. +If you do not have an RDS object handy, feel free to download one from [single-cell-test-files](https://github.com/jkanche/random-test-files/releases). -In addition, the package provides functions to convert parsed R objects into Python representations. +### Basic Usage ```python -from rds2py import as_spase_matrix, as_summarized_experiment - -# to convert an robject to a sparse matrix -sp_mat = as_sparse(rObj) - -# to convert an robject to SCE -sce = as_summarized_experiment(rObj) +from rds2py import read_rds +r_obj = read_rds("path/to/file.rds") ``` -For more examples converting `data.frame`, `dgCMatrix`, `dgRMatrix`, `dgTMatrix` to Python, checkout the [documentation](https://biocpy.github.io/rds2py/). +The returned `r_obj` either returns an appropriate Python class if a parser is already implemented or returns the dictionary containing the data from the RDS file. -## Developer Notes +## Write-your-own-reader -This project uses Cython to provide bindings from C++ to Python. +In addition, the package provides the dictionary representation of the RDS file, allowing users to write their own custom readers into appropriate Python representations. -Steps to setup dependencies - +```python +from rds2py import parse_rds -- git submodules is initialized in `extern/rds2cpp` -- `cmake .` in `extern/rds2cpp` directory to download dependencies, especially the `byteme` library +data = parse_rds("path/to/file.rds") +print(data) +``` -First one needs to build the extern library, this would generate a shared object file to `src/rds2py/core-[*].so` +if you know this RDS file contains an `GenomicRanges` object, you can use or modify the provided list reader, or write your own parser to convert this dictionary. -```shell -python setup.py build_ext --inplace +```python +from rds2py.read_granges import read_genomic_ranges + +gr = read_genomic_ranges(data) ``` -For typical development workflows, run +## Type Conversion Reference -```shell -python setup.py build_ext --inplace && tox -``` +| R Type | Python/NumPy Type | +|--------|------------------| +| numeric | numpy.ndarray (float64) | +| integer | numpy.ndarray (int32) | +| character | list of str | +| logical | numpy.ndarray (bool) | +| factor | list | +| data.frame | BiocFrame | +| matrix | numpy.ndarray or scipy.sparse matrix | +| dgCMatrix | scipy.sparse.csc_matrix | +| dgRMatrix | scipy.sparse.csr_matrix | + +## Developer Notes + +This project uses pybind11 to provide bindings to the rds2cpp library. Please make sure necessary C++ compiler is installed on your system. diff --git a/docs/tutorial.md b/docs/tutorial.md index fdcad60..dbabf6d 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -2,64 +2,40 @@ If you do not have an RDS object handy, feel free to download one from [single-cell-test-files](https://github.com/jkanche/random-test-files/releases). -## Step 1: Read a RDS file in Python - -First we need to read the RDS file that can be easily explored in Python. The `read_rds` parses the R object and returns -a dictionary of the R object. +### Basic Usage ```python from rds2py import read_rds - -rObj = read_rds() +r_obj = read_rds("path/to/file.rds") ``` -Once we have a realized structure, we can now convert this object to useful Python representations. It contains two keys - -- `data`: If atomic entities, contains the numpy view of the memory space. -- `attributes`: Additional properties available for the object. - -The package provides friendly functions to convert some R representations to useful Python representations. - -## Step 2: Python representations +The returned `r_obj` either returns an appropriate Python class if a parser is already implemented or returns the dictionary containing the data from the RDS file. -### Matrices +## Write-your-own-reader -Use these methods if the RDS file contains either a sparse matrix (`dgCMatrix`, `dgRMatrix`, or `dgTMatrix`) or a dense matrix. - -**_Note: If an R object contains `dims` in the `attributes`, we consider this as a matrix._** +In addition, the package provides the dictionary representation of the RDS file, allowing users to write their own custom readers into appropriate Python representations. ```python -from rds2py import as_spase_matrix, as_dense_matrix - -# to convert an robject to a sparse matrix -sp_mat = as_sparse_matrix(rObj) - -# to convert an robject to a sparse matrix -dense_mat = as_dense_matrix(rObj) -``` - -### Pandas DataFrame +from rds2py import parse_rds -Methods are available to construct a pandas `DataFrame` from data stored in an RDS file. The package supports two R classes for this operation - `data.frame` and `DFrame` classes. - -```python -from rds2py import as_pandas - -# to convert an robject to DF -df = as_pandas(rObj) -``` - -### S4 classes: specifically `SingleCellExperiment` or `SummarizedExperiment` - -We also support `SingleCellExperiment` or `SummarizedExperiment` from Bioconductor. the `as_summarized_experiment` method is how we one can do this operation. - -**_Note: This method also serves as an example on how to convert complex R structures into Python representations._** - -```python -from rds2py import as_summarized_experiment +data = parse_rds("path/to/file.rds") +print(data) -# to convert an robject to SCE -sp_mat = as_summarized_experiment(rObj) +# now write your own parser to convert this dictionary. ``` -Well thats it, hack on & create more base representations to encapsulate complex structures. If you want to add more representations, feel free to send a PR! +## Type Conversion Reference + +| R Type | Python/NumPy Type | +|--------|------------------| +| numeric | numpy.ndarray (float64) | +| integer | numpy.ndarray (int32) | +| character | list of str | +| logical | numpy.ndarray (bool) | +| factor | list | +| data.frame | BiocFrame | +| matrix | numpy.ndarray or scipy.sparse matrix | +| dgCMatrix | scipy.sparse.csc_matrix | +| dgRMatrix | scipy.sparse.csr_matrix | + +Check out the module reference for more information on these classes. diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index c38c3b8..c8fc51b 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -31,6 +31,6 @@ set_property(TARGET ${TARGET} PROPERTY CXX_STANDARD 17) target_link_libraries(${TARGET} PRIVATE rds2cpp pybind11::pybind11) set_target_properties(${TARGET} PROPERTIES - OUTPUT_NAME rds_parser + OUTPUT_NAME lib_rds_parser PREFIX "" ) diff --git a/lib/src/rdswrapper.cpp b/lib/src/rdswrapper.cpp index 2c9dcfa..1b52da2 100644 --- a/lib/src/rdswrapper.cpp +++ b/lib/src/rdswrapper.cpp @@ -20,12 +20,12 @@ class RdsReader { if (!ptr) throw std::runtime_error("Null pointer in 'get_rtype'."); // py::print("arg::", static_cast(ptr->type())); switch (ptr->type()) { + case rds2cpp::SEXPType::S4: return "S4"; case rds2cpp::SEXPType::INT: return "integer"; case rds2cpp::SEXPType::REAL: return "double"; case rds2cpp::SEXPType::STR: return "string"; case rds2cpp::SEXPType::LGL: return "boolean"; case rds2cpp::SEXPType::VEC: return "vector"; - case rds2cpp::SEXPType::S4: return "S4"; case rds2cpp::SEXPType::NIL: return "null"; default: return "other"; } @@ -164,7 +164,7 @@ class RdsObject { } }; -PYBIND11_MODULE(rds_parser, m) { +PYBIND11_MODULE(lib_rds_parser, m) { py::register_exception(m, "RdsParserError"); py::class_(m, "RdsObject") diff --git a/setup.cfg b/setup.cfg index 623f78c..09acc5f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ [metadata] name = rds2py -description = Parse and read RDS files as Python representations +description = Parse and construct Python representations for datasets stored in RDS files author = jkanche author_email = jayaram.kancherla@gmail.com license = MIT @@ -50,11 +50,13 @@ python_requires = >=3.8 install_requires = importlib-metadata; python_version<"3.8" numpy - pandas scipy + biocutils>=0.1.5 singlecellexperiment>=0.4.1 summarizedexperiment>=0.4.1 genomicranges>=0.4.9 + biocframe + multiassayexperiment [options.packages.find] where = src @@ -65,17 +67,14 @@ exclude = # Add here additional requirements for extra features, to install with: # `pip install rds2py[PDF]` like: # PDF = ReportLab; RXP +optional = + pandas # Add here test requirements (semicolon/line-separated) testing = setuptools pytest pytest-cov - numpy - pandas - scipy - singlecellexperiment - summarizedexperiment [options.entry_points] # Add here console scripts like: diff --git a/setup.py b/setup.py index 8649f6a..f495462 100644 --- a/setup.py +++ b/setup.py @@ -38,10 +38,7 @@ def build_cmake(self, ext): "lib", "-B", build_temp, - "-Dpybind11_DIR=" - + os.path.join( - os.path.dirname(pybind11.__file__), "share", "cmake", "pybind11" - ), + "-Dpybind11_DIR=" + os.path.join(os.path.dirname(pybind11.__file__), "share", "cmake", "pybind11"), "-DPYTHON_EXECUTABLE=" + sys.executable, ] if os.name != "nt": diff --git a/src/rds2py/PyRdsReader.py b/src/rds2py/PyRdsReader.py index 5219d31..166719b 100644 --- a/src/rds2py/PyRdsReader.py +++ b/src/rds2py/PyRdsReader.py @@ -1,31 +1,75 @@ -from .rds_parser import RdsObject, RdsReader -import numpy as np -from typing import Dict, Any, List, Union +"""Low-level interface for reading RDS file format. + +This module provides the core functionality for parsing RDS files at a binary level and converting them into a +dictionary representation that can be further processed by higher-level functions. +""" + +from typing import Any, Dict, List, Union from warnings import warn +import numpy as np + +from .lib_rds_parser import RdsObject, RdsReader + class PyRdsParserError(Exception): + """Exception raised for errors during RDS parsing.""" + pass class PyRdsParser: - """Python bindings to the rds2cpp interface.""" + """Parser for reading RDS files. + + This class provides low-level access to RDS file contents, handling the binary + format and converting it into Python data structures. It supports various R + data types and handles special R cases like NA values, integer sequences and + range functions. + + Attributes: + R_MIN: + Minimum integer value in R, used for handling NA values. + + rds_object: + Internal representation of the RDS file. + + root_object: + Root object of the parsed RDS file. + """ + + R_MIN: int = -2147483648 def __init__(self, file_path: str): + """Initialize the class. + + Args: + file_path: + Path to the RDS file to be read. + """ try: self.rds_object = RdsObject(file_path) robject = self.rds_object.get_robject() + if not isinstance(robject, RdsReader): raise TypeError(f"Expected 'RdsReader' object, got {type(robject)}") + self.root_object = robject except Exception as e: raise PyRdsParserError(f"Error initializing 'PyRdsParser': {str(e)}") def parse(self) -> Dict[str, Any]: - """Parse the RDS File (recursively). + """Parse the entire RDS file into a dictionary structure. Returns: - A Dictionary with object attributes as keys and the value representing the data from the RDS file. + A dictionary containing the parsed data with keys: + - 'type': The R object type + - 'data': The actual data (if applicable) + - 'attributes': R object attributes (if any) + - 'class_name': The R class name + - Additional keys depending on the object type + + Raises: + PyRdsParserError: If there's an error during parsing. """ try: return self._process_object(self.root_object) @@ -37,44 +81,66 @@ def _process_object(self, obj: RdsReader) -> Dict[str, Any]: rtype = obj.get_rtype() result: Dict[str, Any] = {"type": rtype} - if rtype in ["integer", "boolean", "double"]: - result["data"] = self._get_numeric_data(obj, rtype) + if rtype == "S4": + result["package_name"] = obj.get_package_name() + result["class_name"] = obj.get_class_name() + result["attributes"] = self._process_attributes(obj) + elif rtype in ["integer", "boolean", "double"]: + result["data"] = self._handle_r_special_cases( + self._get_numeric_data(obj, rtype), rtype, obj.get_rsize() + ) result["attributes"] = self._process_attributes(obj) + result["class_name"] = f"{rtype}_vector" elif rtype == "string": result["data"] = obj.get_string_arr() + result["class_name"] = "string_vector" elif rtype == "vector": result["data"] = self._process_vector(obj) result["attributes"] = self._process_attributes(obj) - elif rtype == "S4": - result["package_name"] = obj.get_package_name() - result["class_name"] = obj.get_class_name() - result["attributes"] = self._process_attributes(obj) + result["class_name"] = "vector" elif rtype == "null": pass else: # raise ValueError - warn(f"Unsupported R object type: {rtype}") + warn(f"Unsupported R object type: {rtype}", RuntimeWarning) result["data"] = None result["attributes"] = None + result["class_name"] = None return result except Exception as e: raise PyRdsParserError(f"Error processing object: {str(e)}") + def _handle_r_special_cases(self, data: np.ndarray, rtype: str, size: int) -> Union[np.ndarray, range]: + """Handle special R data representations.""" + try: + # Special handling for R integer containing NA + if size != 2: + if any(data == self.R_MIN): + return np.array([np.nan if x == self.R_MIN else x for x in data]) + + # Special handling for R integer sequences + if rtype == "integer" and size == 2 and data[0] == self.R_MIN and data[1] < 0: + if data[1] == self.R_MIN: + return [None, None] + return range(data[1] * -1) + + return data + except Exception as e: + raise PyRdsParserError(f"Error handling R special cases: {str(e)}") + def _get_numeric_data(self, obj: RdsReader, rtype: str) -> np.ndarray: try: data = obj.get_numeric_data() if rtype == "boolean": return data.astype(bool) + return data except Exception as e: raise PyRdsParserError(f"Error getting numeric data: {str(e)}") def _process_vector(self, obj: RdsReader) -> List[Dict[str, Any]]: - return [ - self._process_object(obj.load_vec_element(i)) - for i in range(obj.get_rsize()) - ] + return [self._process_object(obj.load_vec_element(i)) for i in range(obj.get_rsize())] def _process_attributes(self, obj: RdsReader) -> Dict[str, Dict[str, Any]]: try: @@ -82,6 +148,7 @@ def _process_attributes(self, obj: RdsReader) -> Dict[str, Dict[str, Any]]: for name in obj.get_attribute_names(): attr_obj = obj.load_attribute_by_name(name) attributes[name] = self._process_object(attr_obj) + return attributes except Exception as e: raise PyRdsParserError(f"Error processing attributes: {str(e)}") diff --git a/src/rds2py/__init__.py b/src/rds2py/__init__.py index d16b158..f64e9e8 100644 --- a/src/rds2py/__init__.py +++ b/src/rds2py/__init__.py @@ -15,13 +15,5 @@ finally: del version, PackageNotFoundError -# from .core import * - -from .interface import ( - as_dense_matrix, - as_sparse_matrix, - as_pandas, - as_summarized_experiment, -) - -from .parser import read_rds, get_class +from .generics import read_rds +from .rdsutils import parse_rds diff --git a/src/rds2py/generics.py b/src/rds2py/generics.py new file mode 100644 index 0000000..565d1cd --- /dev/null +++ b/src/rds2py/generics.py @@ -0,0 +1,140 @@ +"""Core functionality for reading RDS files in Python. + +This module provides the main interface for reading RDS files and converting them +to appropriate Python objects. It maintains a registry of supported R object types +and their corresponding Python parser functions. + +The module supports various R object types including vectors, matrices, data frames, +and specialized Bioconductor objects like GenomicRanges and SummarizedExperiment. + +Example: + + .. code-block:: python + + data = read_rds("example.rds") + print(type(data)) +""" + +from importlib import import_module +from warnings import warn + +from .rdsutils import get_class, parse_rds + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + +REGISTRY = { + # typed vectors + "integer_vector": "rds2py.read_atomic.read_integer_vector", + "boolean_vector": "rds2py.read_atomic.read_boolean_vector", + "string_vector": "rds2py.read_atomic.read_string_vector", + "double_vector": "rds2py.read_atomic.read_double_vector", + # dictionary + "vector": "rds2py.read_dict.read_dict", + # factors + "factor": "rds2py.read_factor.read_factor", + # Rle + "Rle": "rds2py.read_rle.read_rle", + # matrices + "dgCMatrix": "rds2py.read_matrix.read_dgcmatrix", + "dgRMatrix": "rds2py.read_matrix.read_dgrmatrix", + "dgTMatrix": "rds2py.read_matrix.read_dgtmatrix", + "ndarray": "rds2py.read_matrix.read_ndarray", + # data frames + "data.frame": "rds2py.read_frame.read_data_frame", + "DFrame": "rds2py.read_frame.read_dframe", + # genomic ranges + "GRanges": "rds2py.read_granges.read_genomic_ranges", + "GenomicRanges": "rds2py.read_granges.read_genomic_ranges", + "CompressedGRangesList": "rds2py.read_granges.read_granges_list", + "GRangesList": "rds2py.read_granges.read_granges_list", + # summarized experiment + "SummarizedExperiment": "rds2py.read_se.read_summarized_experiment", + "RangedSummarizedExperiment": "rds2py.read_se.read_ranged_summarized_experiment", + # single-cell experiment + "SingleCellExperiment": "rds2py.read_sce.read_single_cell_experiment", + "SummarizedExperimentByColumn": "rds2py.read_sce.read_alts_summarized_experiment_by_column", + # multi assay experiment + "MultiAssayExperiment": "rds2py.read_mae.read_multi_assay_experiment", + "ExperimentList": "rds2py.read_dict.read_dict", +} + + +# @singledispatch +# def save_rds(x, path: str): +# """Save a Python object as RDS file. + +# Args: +# x: +# Object to save. + +# path: +# Path to save the object. +# """ +# raise NotImplementedError( +# f"No `save_rds` method implemented for '{type(x).__name__}' objects." +# ) + + +def read_rds(path: str, **kwargs): + """Read an RDS file and convert it to an appropriate Python object. + + Args: + path: + Path to the RDS file to be read. + + **kwargs: + Additional arguments passed to specific parser functions. + + Returns: + A Python object representing the data in the RDS file. The exact type + depends on the contents of the RDS file and the available parsers. + """ + _robj = parse_rds(path=path) + return _dispatcher(_robj, **kwargs) + + +def _dispatcher(robject: dict, **kwargs): + """Internal function to dispatch R objects to appropriate parser functions. + + Args: + robject: + Dictionary containing parsed R object data. + + **kwargs: + Additional arguments passed to specific parser functions. + + Returns: + Parsed Python object corresponding to the R data structure. + Returns the original dictionary if no appropriate parser is found. + """ + _class_name = get_class(robject) + + if _class_name is None: + return None + + # if a class is registered, coerce the object + # to the representation. + if _class_name in REGISTRY: + try: + command = REGISTRY[_class_name] + if isinstance(command, str): + last_period = command.rfind(".") + mod = import_module(command[:last_period]) + command = getattr(mod, command[last_period + 1 :]) + REGISTRY[_class_name] = command + + return command(robject, **kwargs) + except Exception as e: + warn( + f"Failed to coerce RDS object to class: '{_class_name}', returning the dictionary, {str(e)}", + RuntimeWarning, + ) + else: + warn( + f"RDS file contains an unknown class: '{_class_name}', returning the dictionary", + RuntimeWarning, + ) + + return robject diff --git a/src/rds2py/granges.py b/src/rds2py/granges.py deleted file mode 100644 index 7e7081b..0000000 --- a/src/rds2py/granges.py +++ /dev/null @@ -1,151 +0,0 @@ -from biocframe import BiocFrame -from genomicranges import GenomicRanges, GenomicRangesList, SeqInfo -from iranges import IRanges - -from .parser import get_class -from .pdf import as_pandas_from_dframe - -__author__ = "jkanche" -__copyright__ = "jkanche" -__license__ = "MIT" - - -def as_granges(robj): - """Parse an R object as a :py:class:`~genomicranges.GenomicRanges.GenomicRanges`. - - Args: - robj: - Object parsed from the `RDS` file. - - Usually the result of :py:func:`~rds2py.parser.read_rds`. - - Returns: - A ``GenomicRanges`` object. - """ - _cls = get_class(robj) - - if _cls not in ["GenomicRanges", "GRanges"]: - raise TypeError(f"obj is not genomic ranges, but is `{_cls}`.") - - _range_start = robj["attributes"]["ranges"]["attributes"]["start"]["data"] - _range_width = robj["attributes"]["ranges"]["attributes"]["width"]["data"] - _range_names = None - if "NAMES" in robj["attributes"]["ranges"]["attributes"]: - _range_names = robj["attributes"]["ranges"]["attributes"]["NAMES"]["data"] - _ranges = IRanges(_range_start, _range_width, names=_range_names) - - _seqnames = _as_list(robj["attributes"]["seqnames"]) - - _strands = robj["attributes"]["strand"] - _fstrand = None - if "attributes" in _strands: - _lengths = _strands["attributes"]["lengths"]["data"] - _factors = _strands["attributes"]["values"]["data"] - _levels = _strands["attributes"]["values"]["attributes"]["levels"]["data"] - _strds = [_levels[x - 1] for x in _factors] - _fstrand = [] - for i, x in enumerate(_lengths): - _fstrand.extend([_strds[i]] * x) - - _seqinfo_seqnames = robj["attributes"]["seqinfo"]["attributes"]["seqnames"]["data"] - _seqinfo_seqlengths = robj["attributes"]["seqinfo"]["attributes"]["seqlengths"][ - "data" - ] - _seqinfo_is_circular = robj["attributes"]["seqinfo"]["attributes"]["is_circular"][ - "data" - ] - _seqinfo_genome = robj["attributes"]["seqinfo"]["attributes"]["genome"]["data"] - _seqinfo = SeqInfo( - seqnames=_seqinfo_seqnames, - seqlengths=[None if x == -2147483648 else int(x) for x in _seqinfo_seqlengths], - is_circular=[ - None if x == -2147483648 else bool(x) for x in _seqinfo_is_circular - ], - genome=_seqinfo_genome, - ) - - _mcols = BiocFrame.from_pandas( - as_pandas_from_dframe(robj["attributes"]["elementMetadata"]) - ) - - _gr_names = None - if "NAMES" in robj["attributes"]: - _gr_names = robj["attributes"]["NAMES"]["data"] - - return GenomicRanges( - seqnames=_seqnames, - ranges=_ranges, - strand=_fstrand, - names=_gr_names, - mcols=_mcols, - seqinfo=_seqinfo, - ) - - -def _as_list(robj): - """Parse an R object as a :py:class:`~list`. - - Args: - robj: - Object parsed from the `RDS` file. - - Usually the result of :py:func:`~rds2py.parser.read_rds`. - - Returns: - A ``list`` of the Rle class. - """ - _cls = get_class(robj) - - if _cls not in ["Rle"]: - raise TypeError(f"obj is not Rle, but is `{_cls}`.") - - _attr_vals = robj["attributes"] - _data = _attr_vals["values"]["data"].tolist() - if "attributes" in _attr_vals["values"]: - if "levels" in _attr_vals["values"]["attributes"]: - _levels_data = _attr_vals["values"]["attributes"]["levels"]["data"] - _data = [_levels_data[x - 1] for x in _data] - - if "lengths" in _attr_vals: - _final = [] - _lengths = _attr_vals["lengths"]["data"] - - for idx, lg in enumerate(_lengths.tolist()): - _final.extend([_data[idx]] * lg) - - _data = _final - - return _data - - -def as_granges_list(robj): - """Parse an R object as a :py:class:`~genomicranges.GenomicRangesList.GenomicRangesList`. - - Args: - robj: - Object parsed from the `RDS` file. - - Usually the result of :py:func:`~rds2py.parser.read_rds`. - - Returns: - A ``GenomicRangesList`` object. - """ - - _cls = get_class(robj) - - if _cls not in ["CompressedGRangesList", "GRangesList"]: - raise TypeError(f"obj is not genomic ranges list, but is `{_cls}`.") - - _gre = as_granges(robj["attributes"]["unlistData"]) - - _groups = robj["attributes"]["partitioning"]["attributes"]["NAMES"]["data"] - _partitionends = robj["attributes"]["partitioning"]["attributes"]["end"]["data"] - - _grelist = [] - - current = 0 - for _pend in _partitionends: - _grelist.append(_gre[current:_pend]) - current = _pend - - return GenomicRangesList(ranges=_grelist, names=_groups) diff --git a/src/rds2py/interface.py b/src/rds2py/interface.py deleted file mode 100644 index 8f8ad93..0000000 --- a/src/rds2py/interface.py +++ /dev/null @@ -1,258 +0,0 @@ -from typing import Literal - -from numpy import ndarray -from singlecellexperiment import SingleCellExperiment -from summarizedexperiment import SummarizedExperiment -from biocframe import BiocFrame - -from .parser import get_class -from .pdf import as_pandas_from_data_frame, as_pandas_from_dframe - -__author__ = "jkanche" -__copyright__ = "jkanche" -__license__ = "MIT" - - -def as_pandas(robj): - """Parse an R object as a :py:class:`~pandas.DataFrame`. - - Currently supports ``DFrame`` or ``data.frame`` class objects from R. - - Args: - robj: - Object parsed from the `RDS` file. - - Usually the result of :py:func:`~rds2py.parser.read_rds`. - - Returns: - A :py:class:`~pandas.DataFrame` containing the data from the R Object. - """ - _cls = get_class(robj) - - if _cls == "DFrame": - return as_pandas_from_dframe(robj) - elif _cls == "data.frame": - return as_pandas_from_data_frame(robj) - else: - raise TypeError( - f"`robj` must be either a 'DFrame' or 'data.frame' but is {_cls}." - ) - - -def as_sparse_matrix(robj): - """Parse an R object as a sparse matrix. - - Only supports reading of `dgCMatrix`, `dgRMatrix`, `dgTMatrix` marices. - - Args: - robj: - Object parsed from the `RDS` file. - - Usually the result of :py:func:`~rds2py.parser.read_rds`. - - Returns: - A sparse matrix of the R object. - """ - from scipy.sparse import csc_matrix, csr_matrix - - _cls = get_class(robj) - - if _cls not in ["dgCMatrix", "dgRMatrix", "dgTMatrix"]: - raise TypeError( - f"`robj` does not contain not a supported sparse matrix format, contains `{_cls}`." - ) - - if _cls == "dgCMatrix": - return csc_matrix( - ( - robj["attributes"]["x"]["data"], - robj["attributes"]["i"]["data"], - robj["attributes"]["p"]["data"], - ), - shape=tuple(robj["attributes"]["Dim"]["data"].tolist()), - ) - - if _cls == "dgRMatrix": - return csr_matrix( - ( - robj["attributes"]["x"]["data"], - robj["attributes"]["i"]["data"], - robj["attributes"]["p"]["data"], - ), - shape=tuple(robj["attributes"]["Dim"]["data"].tolist()), - ) - - if _cls == "dgTMatrix": - return csr_matrix( - ( - robj["attributes"]["x"]["data"], - ( - robj["attributes"]["i"]["data"], - robj["attributes"]["j"]["data"], - ), - ), - shape=tuple(robj["attributes"]["Dim"]["data"].tolist()), - ) - - -def as_dense_matrix(robj, order: Literal["C", "F"] = "F") -> ndarray: - """Parse an R object as a :py:class:`~numpy.ndarray`. - - Args: - robj: - Object parsed from the `RDS` file. - - Usually the result of :py:func:`~rds2py.parser.read_rds`. - - order: - Row-major (**C**-style) or Column-major (**F**ortran-style) - order. - - Defaults to "F". - - Returns: - An ``ndarray`` of the R object. - """ - _cls = get_class(robj) - - if order not in ["C", "F"]: - raise ValueError("order must be either 'C' or 'F'.") - - if _cls not in ["densematrix"]: - raise TypeError(f"obj is not a supported dense matrix format, but is `{_cls}`.") - - return ndarray( - shape=tuple(robj["attributes"]["dim"]["data"].tolist()), - dtype=robj["data"].dtype, - buffer=robj["data"], - order=order, - ) - - -def as_summarized_experiment(robj): - """Parse an R object as a :py:class:`~singlecellexperiment.SingleCellExperiment.SingleCellExperiment` or - :py:class:`~summarizedexperiment.SummarizedExperiment.SummarizedExperiment`. - - Note: This function demonstrates how to parse a complex RDS objects in Python and may not work for all - scenarios. - - Args: - robj: - Object parsed from the `RDS` file. - - Usually the result of :py:func:`~rds2py.parser.read_rds`. - - order: - Row-major (**C**-style) or Column-major (**F**ortran-style) - order. Only used if the ``robj`` contains a :py:class:`~numpy.ndarray`. - - Defaults to "F". - - Returns: - A `SummarizedExperiment` or - `SingleCellExperiment` from the R object. - """ - - _cls = get_class(robj) - - if _cls not in ["SingleCellExperiment", "SummarizedExperiment"]: - raise TypeError( - "`robj` does not contain a `SingleCellExperiment` or `SummarizedExperiment`." - ) - - # parse assays names - robj_asys = {} - assay_dims = None - asy_names = robj["attributes"]["assays"]["attributes"]["data"]["attributes"][ - "listData" - ]["attributes"]["names"]["data"] - for idx in range(len(asy_names)): - idx_asy = robj["attributes"]["assays"]["attributes"]["data"]["attributes"][ - "listData" - ]["data"][idx] - - asy_class = get_class(idx_asy) - - if asy_class in ["dgCMatrix", "dgRMatrix", "dgTMatrix"]: - robj_asys[asy_names[idx]] = as_sparse_matrix(idx_asy) - if assay_dims is None: - assay_dims = robj_asys[asy_names[idx]].shape - elif asy_class == "densematrix": - robj_asys[asy_names[idx]] = as_dense_matrix(idx_asy) - if assay_dims is None: - assay_dims = robj_asys[asy_names[idx]].shape - else: - robj_asys[asy_names[idx]] = None - - # parse coldata - robj_coldata = as_pandas_from_dframe(robj["attributes"]["colData"]) - if robj_coldata.empty: - robj_coldata = BiocFrame({"_cols": range(assay_dims[1])}) - - # parse rowRanges - robj_rowdata = None - if "rowRanges" in robj["attributes"]: - robj_rowdata = as_pandas_from_dframe( - robj["attributes"]["rowRanges"]["attributes"]["elementMetadata"] - ) - else: - robj_rowdata = BiocFrame({"_rows": range(assay_dims[0])}) - - # check red. dims, alternative expts - robj_reduced_dims = None - robj_altExps = None - if _cls == "SingleCellExperiment": - col_attrs = robj["attributes"]["int_colData"]["attributes"]["listData"][ - "attributes" - ]["names"]["data"] - - for idx in range(len(col_attrs)): - idx_col = col_attrs[idx] - idx_value = robj["attributes"]["int_colData"]["attributes"]["listData"][ - "data" - ][idx] - - if idx_col == "reducedDims" and idx_value["data"] is not None: - robj_reduced_dims = as_dense_matrix( - robj["attributes"]["int_colData"]["attributes"]["listData"]["data"] - ) - - if idx_col == "altExps": - alt_names = idx_value["attributes"]["listData"]["attributes"]["names"][ - "data" - ] - robj_altExps = {} - for idx_alt_names in range(len(alt_names)): - altn = alt_names[idx_alt_names] - - alt_key = list( - idx_value["attributes"]["listData"]["data"][idx_alt_names][ - "attributes" - ].keys() - )[0] - - robj_altExps[altn] = as_summarized_experiment( - idx_value["attributes"]["listData"]["data"][idx_alt_names][ - "attributes" - ][alt_key] - ) - - # ignore colpairs for now, does anyone even use this ? - # if col == "colPairs": - - if _cls == "SummarizedExperiment": - return SummarizedExperiment( - assays=robj_asys, row_data=robj_rowdata, column_data=robj_coldata - ) - elif _cls == "SingleCellExperiment": - return SingleCellExperiment( - assays=robj_asys, - row_data=robj_rowdata, - column_data=robj_coldata, - alternative_experiments=robj_altExps, - reduced_dims=robj_reduced_dims, - ) - else: - raise TypeError( - "`robj` is neither a `SummarizedExperiment` nor `SingleCellExperiment`." - ) diff --git a/src/rds2py/parser.py b/src/rds2py/parser.py deleted file mode 100644 index e8aad9c..0000000 --- a/src/rds2py/parser.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Dict, MutableMapping - -from .PyRdsReader import PyRdsParser - -__author__ = "jkanche" -__copyright__ = "jkanche" -__license__ = "MIT" - - -def read_rds(file: str) -> Dict: - """Read an RDS file as a :py:class:`~dict`. - - Args: - file (str): Path to RDS file. - - Returns: - MutableMapping: R object as a python dictionary. - """ - parsed_obj = PyRdsParser(file) - realized = parsed_obj.parse() - - return realized - - -def get_class(robj: MutableMapping) -> str: - """Generic method to get the class information of the R object. - - Args: - robj (MutableMapping): Object parsed from the `RDS` file. - Usually the result of :py:func:`~rds2py.parser.read_rds`. - - Returns: - str: Class name. - """ - if "class_name" in robj: - return robj["class_name"] - - if "attributes" in robj and len(robj["attributes"].keys()) > 0: - obj_attr = robj["attributes"] - if "class" in obj_attr: - return obj_attr["class"]["data"][0] - - # kind of making this assumption, if we ever see a dim, its a matrix - if "dim" in obj_attr: - return "densematrix" - - return None diff --git a/src/rds2py/pdf.py b/src/rds2py/pdf.py deleted file mode 100644 index a12f015..0000000 --- a/src/rds2py/pdf.py +++ /dev/null @@ -1,72 +0,0 @@ -from .parser import get_class - -__author__ = "jkanche" -__copyright__ = "jkanche" -__license__ = "MIT" - - -def as_pandas_from_data_frame(robj): - """Read an R object to a :py:class:`~pandas.DataFrame`. - - Args: - robj: - Object parsed from the `RDS` file. - - Usually the result of :py:func:`~rds2py.parser.read_rds`. - - Returns: - A `DataFrame` from the R Object. - """ - from pandas import DataFrame - - cls = get_class(robj) - - if cls != "data.frame": - raise TypeError("`robj` does not contain a 'data.frame'.") - - df = DataFrame( - robj["data"], - columns=robj["attributes"]["names"]["data"], - index=robj["attributes"]["row.names"]["data"], - ) - - return df - - -def as_pandas_from_dframe(robj): - """Convert a realized R object to a pandas data frame representation. - - Args: - robj: - Object parsed from the `RDS` file. - - Usually the result of :py:func:`~rds2py.parser.read_rds`. - - Returns: - A `DataFrame` from the R Object. - """ - from pandas import DataFrame - - cls = get_class(robj) - - if cls != "DFrame": - raise Exception("`robj` does not contain a 'DFrame'.") - - data = {} - col_names = robj["attributes"]["listData"]["attributes"]["names"]["data"] - for idx in range(len(col_names)): - idx_asy = robj["attributes"]["listData"]["data"][idx] - - data[col_names[idx]] = idx_asy["data"] - - index = None - if robj["attributes"]["rownames"]["data"]: - index = robj["attributes"]["rownames"]["data"] - - df = DataFrame( - data, - columns=col_names, - index=index, - ) - - return df diff --git a/src/rds2py/rdsutils.py b/src/rds2py/rdsutils.py new file mode 100644 index 0000000..82f52a3 --- /dev/null +++ b/src/rds2py/rdsutils.py @@ -0,0 +1,67 @@ +"""Utility functions for RDS file parsing and class inference. + +This module provides helper functions for parsing RDS files and inferring the appropriate R class information from +parsed objects. +""" + +from .PyRdsReader import PyRdsParser + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def parse_rds(path: str) -> dict: + """Parse an RDS file into a dictionary representation. + + Args: + path: + Path to the RDS file to be parsed. + + Returns: + A dictionary containing the parsed contents of the RDS file. + The structure depends on the type of R object stored in the file. + """ + parsed_obj = PyRdsParser(path) + realized = parsed_obj.parse() + + return realized + + +def get_class(robj: dict) -> str: + """Infer the R class name from a parsed RDS object. + + Notes: + - Handles both S4 and non-S4 R objects + - Special handling for vectors and matrices + - Checks for class information in object attributes + + Args: + robj: + Dictionary containing parsed RDS data, typically + the output of :py:func:`~.parse_rds`. + + Returns: + The inferred R class name, or None if no class can be determined. + """ + _inferred_cls_name = None + if robj["type"] != "S4": + if "class_name" in robj: + _inferred_cls_name = robj["class_name"] + if _inferred_cls_name is not None and ( + "integer" in _inferred_cls_name or "double" in _inferred_cls_name or _inferred_cls_name == "vector" + ): + if "attributes" in robj: + obj_attr = robj["attributes"] + + # kind of making this assumption, if we ever see a dim, its a matrix + if obj_attr is not None: + if "dim" in obj_attr: + _inferred_cls_name = "ndarray" + elif "class" in obj_attr: + _inferred_cls_name = obj_attr["class"]["data"][0] + + else: + _inferred_cls_name = robj["class_name"] + + return _inferred_cls_name diff --git a/src/rds2py/read_atomic.py b/src/rds2py/read_atomic.py new file mode 100644 index 0000000..bd831c5 --- /dev/null +++ b/src/rds2py/read_atomic.py @@ -0,0 +1,115 @@ +"""Functions for parsing atomic R vector types into Python objects. + +This module provides parser functions for converting R's atomic vector types (boolean, integer, string, and double) into +appropriate Python objects using the biocutils package's specialized list classes. +""" + +from biocutils import BooleanList, FloatList, IntegerList, StringList + +from .generics import _dispatcher + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def _extract_names(robject: dict, **kwargs): + """Extract names attribute from an R object if present. + + Args: + robject: + Dictionary containing parsed R object data. + + **kwargs: + Additional arguments. + + Returns: + List of names if present in the object's attributes, + None otherwise. + """ + _names = None + if "attributes" in robject and robject["attributes"] is not None: + if "names" in robject["attributes"]: + _names = _dispatcher(robject["attributes"]["names"]) + + return _names + + +def read_boolean_vector(robject: dict, **kwargs) -> BooleanList: + """Convert an R boolean vector to a Python :py:class:`~biocutils.BooleanList`. + + Args: + robject: + Dictionary containing parsed R boolean vector data. + + **kwargs: + Additional arguments. + + Returns: + A `BooleanList` object containing the vector data + and any associated names. + """ + _names = _extract_names(robject, **kwargs) + + obj = BooleanList(robject["data"], names=_names) + return obj + + +def read_integer_vector(robject: dict, **kwargs) -> IntegerList: + """Convert an R integer vector to a Python :py:class:`~biocutils.IntegerList`. + + Args: + robject: + Dictionary containing parsed R integer vector data. + + **kwargs: + Additional arguments. + + Returns: + A `IntegerList` object containing the vector data + and any associated names. + """ + _names = _extract_names(robject, **kwargs) + + obj = IntegerList(robject["data"], names=_names) + return obj + + +def read_string_vector(robject: dict, **kwargs) -> StringList: + """Convert an R string vector to a Python :py:class:`~biocutils.StringList`. + + Args: + robject: + Dictionary containing parsed R string vector data. + + **kwargs: + Additional arguments. + + Returns: + A `StringList` object containing the vector data + and any associated names. + """ + _names = _extract_names(robject, **kwargs) + + obj = StringList(robject["data"], names=_names) + return obj + + +def read_double_vector(robject: dict, **kwargs) -> FloatList: + """Convert an R double vector to a Python :py:class:`~biocutils.FloatList`. + + Args: + robject: + Dictionary containing parsed R double vector data. + + **kwargs: + Additional arguments. + + Returns: + A `FloatList` object containing the vector data + and any associated names. + """ + _names = _extract_names(robject, **kwargs) + + obj = FloatList(robject["data"], names=_names) + return obj diff --git a/src/rds2py/read_dict.py b/src/rds2py/read_dict.py new file mode 100644 index 0000000..004ac43 --- /dev/null +++ b/src/rds2py/read_dict.py @@ -0,0 +1,49 @@ +"""Functions for parsing R vector and dictionary-like objects. + +This module provides functionality to convert R named vectors and list objects into Python dictionaries or lists, +maintaining the structure and names of the original R objects. +""" + +from .generics import _dispatcher +from .rdsutils import get_class + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def read_dict(robject: dict, **kwargs) -> dict: + """Convert an R named vector or list to a Python dictionary or list. + + Args: + robject: + Dictionary containing parsed R vector/list data. + + **kwargs: + Additional arguments. + + Returns: + If the R object has names, returns a dictionary mapping + names to values. Otherwise, returns a list of parsed values. + + Example: + >>> # For a named R vector c(a=1, b=2) + >>> result = read_dict(robject) + >>> print(result) + {'a': 1, 'b': 2} + """ + _cls = get_class(robject) + + if _cls not in ["vector"]: + raise RuntimeError(f"`robject` does not contain not a vector/dictionary object, contains `{_cls}`.") + + if "names" not in robject["attributes"]: + return [_dispatcher(x, **kwargs) for x in robject["data"]] + + dict_keys = list(_dispatcher(robject["attributes"]["names"], **kwargs)) + + final_vec = {} + for idx, dkey in enumerate(dict_keys): + final_vec[dkey] = _dispatcher(robject["data"][idx], **kwargs) + + return final_vec diff --git a/src/rds2py/read_factor.py b/src/rds2py/read_factor.py new file mode 100644 index 0000000..1339dba --- /dev/null +++ b/src/rds2py/read_factor.py @@ -0,0 +1,50 @@ +"""Functions for parsing R factor objects. + +This module handles the conversion of R factors (categorical variables) into Python lists, preserving the levels and +maintaining the order of the factor levels. +""" + +from .generics import _dispatcher +from .rdsutils import get_class + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def read_factor(robject: dict, **kwargs) -> list: + """Convert an R factor to a Python list. + + Args: + robject: + Dictionary containing parsed R factor data. + + **kwargs: + Additional arguments. + + Returns: + A list containing the factor values, with each value repeated + according to its length if specified. + """ + _cls = get_class(robject) + + if _cls not in ["factor"]: + raise RuntimeError(f"`robject` does not contain not a factor object, contains `{_cls}`.") + + data = robject["data"] + + levels = None + if "levels" in robject["attributes"]: + levels = _dispatcher(robject["attributes"]["levels"], **kwargs) + level_vec = [levels[x - 1] for x in data] + + if "lengths" in robject["attributes"]: + lengths = _dispatcher(robject["attributes"]["lengths"], **kwargs) + else: + lengths = [1] * len(data) + + final_vec = [] + for i, x in enumerate(lengths): + final_vec.extend([level_vec[i]] * x) + + return final_vec diff --git a/src/rds2py/read_frame.py b/src/rds2py/read_frame.py new file mode 100644 index 0000000..bc61922 --- /dev/null +++ b/src/rds2py/read_frame.py @@ -0,0 +1,92 @@ +"""Functions for parsing R data frame objects. + +This module provides parsers for converting both base R `data.frame` objects +and Bioconductor `DataFrame` objects into Python `BiocFrame` objects, preserving +row names, column names, and data types. +""" + +from biocframe import BiocFrame + +from .generics import _dispatcher +from .rdsutils import get_class + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def read_data_frame(robject: dict, **kwargs): + """Convert an R data.frame to a :py:class:`~biocframe.BiocFrame` object. + + Args: + robject: + Dictionary containing parsed R `data.frame` object. + + **kwargs: + Additional arguments. + + Returns: + A BiocFrame object containing the data frame's contents, + with preserved column and row names. + """ + cls = get_class(robject) + + if cls != "data.frame": + raise RuntimeError("`robject` does not contain a 'data.frame'.") + + col_names = _dispatcher(robject["attributes"]["names"], **kwargs) + + bframe_obj = {} + for idx, rd in enumerate(robject["data"]): + bframe_obj[col_names[idx]] = _dispatcher(rd, **kwargs) + + df = BiocFrame( + bframe_obj, + row_names=_dispatcher(robject["attributes"]["row.names"], **kwargs), + ) + + return df + + +def read_dframe(robject: dict, **kwargs): + """Convert an R DFrame (Bioconductor's `DataFrame`) to a `BiocFrame` object. + + Args: + robject: + Dictionary containing parsed R `DFrame` object. + + **kwargs: + Additional arguments. + + Returns: + A BiocFrame object containing the DataFrame's contents, + with preserved metadata and structure. + """ + from biocframe import BiocFrame + + cls = get_class(robject) + + if cls != "DFrame": + raise RuntimeError("`robject` does not contain a 'DFrame'.") + + data = {} + col_names = _dispatcher(robject["attributes"]["listData"]["attributes"]["names"], **kwargs) + for idx, colname in enumerate(col_names): + data[colname] = _dispatcher(robject["attributes"]["listData"]["data"][idx], **kwargs) + + index = None + if robject["attributes"]["rownames"]["data"]: + index = _dispatcher(robject["attributes"]["rownames"], **kwargs) + + nrows = None + if robject["attributes"]["nrows"]["data"]: + nrows = list(_dispatcher(robject["attributes"]["nrows"]), **kwargs)[0] + + df = BiocFrame( + data, + # column_names=col_names, + row_names=index, + number_of_rows=nrows, + ) + + return df diff --git a/src/rds2py/read_granges.py b/src/rds2py/read_granges.py new file mode 100644 index 0000000..5a39520 --- /dev/null +++ b/src/rds2py/read_granges.py @@ -0,0 +1,113 @@ +"""Functions for parsing Bioconductor GenomicRanges objects. + +This module provides parsers for converting Bioconductor's GenomicRanges and GenomicRangesList objects into their Python +equivalents, preserving all genomic coordinates and associated metadata. +""" + +from genomicranges import GenomicRanges, GenomicRangesList, SeqInfo +from iranges import IRanges + +from .generics import _dispatcher +from .rdsutils import get_class + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def read_genomic_ranges(robject: dict, **kwargs) -> GenomicRanges: + """Convert an R `GenomicRanges` object to a Python :py:class:`~genomicranges.GenomicRanges` object. + + Args: + robject: + Dictionary containing parsed `GenomicRanges` data. + + **kwargs: + Additional arguments. + + Returns: + A Python `GenomicRanges` object containing genomic intervals + with associated annotations. + """ + _cls = get_class(robject) + + if _cls not in ["GenomicRanges", "GRanges"]: + raise TypeError(f"obj is not 'GenomicRanges', but is `{_cls}`.") + + _range_start = _dispatcher(robject["attributes"]["ranges"]["attributes"]["start"], **kwargs) + _range_width = _dispatcher(robject["attributes"]["ranges"]["attributes"]["width"], **kwargs) + _range_names = None + if "NAMES" in robject["attributes"]["ranges"]["attributes"]: + _tmp_names = robject["attributes"]["ranges"]["attributes"]["NAMES"] + _range_names = _dispatcher(_tmp_names, **kwargs) + if _range_names is not None: + _range_names = list(_range_names) + + _ranges = IRanges(_range_start, _range_width, names=_range_names) + + _strands = _dispatcher(robject["attributes"]["strand"], **kwargs) + _seqnames = _dispatcher(robject["attributes"]["seqnames"], **kwargs) + _seqinfo_seqnames = _dispatcher(robject["attributes"]["seqinfo"]["attributes"]["seqnames"], **kwargs) + _seqinfo_seqlengths = _dispatcher(robject["attributes"]["seqinfo"]["attributes"]["seqlengths"], **kwargs) + _seqinfo_is_circular = _dispatcher(robject["attributes"]["seqinfo"]["attributes"]["is_circular"], **kwargs) + _seqinfo_genome = _dispatcher(robject["attributes"]["seqinfo"]["attributes"]["genome"], **kwargs) + _seqinfo = SeqInfo( + seqnames=_seqinfo_seqnames, + seqlengths=_seqinfo_seqlengths, + is_circular=_seqinfo_is_circular, + genome=_seqinfo_genome, + ) + _mcols = _dispatcher(robject["attributes"]["elementMetadata"], **kwargs) + + _gr_names = None + if "NAMES" in robject["attributes"]: + _tmp_names = robject["attributes"]["NAMES"] + _gr_names = None if _tmp_names is None else _dispatcher(_tmp_names, **kwargs) + + return GenomicRanges( + seqnames=_seqnames, + ranges=_ranges, + strand=_strands, + names=_gr_names, + mcols=_mcols, + seqinfo=_seqinfo, + ) + + +def read_granges_list(robject: dict, **kwargs) -> GenomicRangesList: + """Convert an R `GenomicRangesList` object to a Python :py:class:`~genomicranges.GenomicRangesList`. + + Args: + robject: + Dictionary containing parsed GenomicRangesList data. + + **kwargs: + Additional arguments. + + Returns: + A Python `GenomicRangesList` object containing containing multiple + `GenomicRanges` objects. + """ + + _cls = get_class(robject) + + if _cls not in ["CompressedGRangesList", "GRangesList"]: + raise TypeError(f"`robject` is not genomic ranges list, but is `{_cls}`.") + + _gre = _dispatcher(robject["attributes"]["unlistData"], **kwargs) + + _groups = None + if "NAMES" in robject["attributes"]["partitioning"]["attributes"]: + _tmp_names = robject["attributes"]["partitioning"]["attributes"]["NAMES"] + _groups = None if _tmp_names is None else _dispatcher(_tmp_names, **kwargs) + + _partitionends = _dispatcher(robject["attributes"]["partitioning"]["attributes"]["end"], **kwargs) + + _grelist = [] + + current = 0 + for _pend in _partitionends: + _grelist.append(_gre[current:_pend]) + current = _pend + + return GenomicRangesList(ranges=_grelist, names=_groups) diff --git a/src/rds2py/read_mae.py b/src/rds2py/read_mae.py new file mode 100644 index 0000000..98d0650 --- /dev/null +++ b/src/rds2py/read_mae.py @@ -0,0 +1,80 @@ +"""Functions for parsing Bioconductor MultiAssayExperiment objects. + +This module handles the conversion of Bioconductor's MultiAssayExperiment container format into its Python equivalent, +preserving the complex relationships between multiple experimental assays and sample metadata. +""" + +from multiassayexperiment import MultiAssayExperiment + +from .generics import _dispatcher +from .rdsutils import get_class +from .read_matrix import MatrixWrapper + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def _sanitize_expts(expts, **kwargs): + """Convert raw experiment objects into SummarizedExperiment format. + + Args: + expts: + Dictionary of experiment objects. + + Returns: + Dictionary of converted experiments, with matrix-like objects + wrapped in SummarizedExperiment containers. + """ + from biocframe import BiocFrame + from summarizedexperiment import SummarizedExperiment + + res = {} + for k, v in expts.items(): + if isinstance(v, MatrixWrapper): + res[k] = SummarizedExperiment( + assays={"matrix": v.matrix}, + row_data=BiocFrame(row_names=v.dimnames[0]), + column_data=BiocFrame(row_names=v.dimnames[1]), + ) + else: + res[k] = v + + return res + + +def read_multi_assay_experiment(robject: dict, **kwargs) -> MultiAssayExperiment: + """Convert an R `MultiAssayExperiment` to a Python :py:class:`~multiassayexperiment.MultiAssayExperiment` object. + + Args: + robject: + Dictionary containing parsed MultiAssayExperiment data. + + **kwargs: + Additional arguments. + + Returns: + A Python `MultiAssayExperiment` object containing + multiple experimental assays with associated metadata. + """ + + _cls = get_class(robject) + + if _cls not in ["MultiAssayExperiment"]: + raise RuntimeError(f"`robject` does not contain a 'MultiAssayExperiment' object, contains `{_cls}`.") + + # parse experiment names + _expt_obj = robject["attributes"]["ExperimentList"]["attributes"]["listData"] + robj_expts = _dispatcher(_expt_obj, **kwargs) + + # parse sample_map + robj_samplemap = _dispatcher(robject["attributes"]["sampleMap"], **kwargs) + + # parse coldata + robj_coldata = _dispatcher(robject["attributes"]["colData"], **kwargs) + + return MultiAssayExperiment( + experiments=_sanitize_expts(robj_expts), + sample_map=robj_samplemap, + column_data=robj_coldata, + ) diff --git a/src/rds2py/read_matrix.py b/src/rds2py/read_matrix.py new file mode 100644 index 0000000..79d6e5b --- /dev/null +++ b/src/rds2py/read_matrix.py @@ -0,0 +1,209 @@ +"""Functions and classes for parsing R matrix objects. + +This module provides functionality to convert R matrix objects (both dense and sparse) into their Python equivalents +using NumPy and SciPy sparse matrix formats. It handles various R matrix types including dgCMatrix, dgRMatrix, and +dgTMatrix. +""" + +from typing import Literal + +from numpy import ndarray +from scipy.sparse import csc_matrix, csr_matrix, spmatrix + +from .generics import _dispatcher +from .rdsutils import get_class + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +class MatrixWrapper: + """A simple wrapper class for matrices that preserves dimension names. + + This class bundles a matrix (dense or sparse) with its dimension names, + maintaining the R-style naming of rows and columns. + + Attributes: + matrix: + The underlying matrix object (numpy.ndarray or scipy.sparse matrix). + + dimnames: + A tuple of (row_names, column_names), each being a list of strings or None. + """ + + def __init__(self, matrix, dimnames=None) -> None: + self.matrix = matrix + self.dimnames = dimnames + + +def _as_sparse_matrix(robject: dict, **kwargs) -> spmatrix: + """Convert an R sparse matrix to a SciPy sparse matrix. + + Notes: + - Supports dgCMatrix (column-sparse) + - Supports dgRMatrix (row-sparse) + - Supports dgTMatrix (triplet format) + - Preserves dimension names if present + + Args: + robject: + Dictionary containing parsed R sparse matrix data. + + **kwargs: + Additional arguments. + + Returns: + A SciPy sparse matrix or wrapped matrix if dimension names exist. + """ + + _cls = get_class(robject) + + if _cls not in ["dgCMatrix", "dgRMatrix", "dgTMatrix"]: + raise RuntimeError(f"`robject` does not contain not a supported sparse matrix format, contains `{_cls}`.") + + if _cls == "dgCMatrix": + mat = csc_matrix( + ( + robject["attributes"]["x"]["data"], + robject["attributes"]["i"]["data"], + robject["attributes"]["p"]["data"], + ), + shape=tuple(robject["attributes"]["Dim"]["data"].tolist()), + ) + elif _cls == "dgRMatrix": + mat = csr_matrix( + ( + robject["attributes"]["x"]["data"], + robject["attributes"]["i"]["data"], + robject["attributes"]["p"]["data"], + ), + shape=tuple(robject["attributes"]["Dim"]["data"].tolist()), + ) + elif _cls == "dgTMatrix": + mat = csr_matrix( + ( + robject["attributes"]["x"]["data"], + ( + robject["attributes"]["i"]["data"], + robject["attributes"]["j"]["data"], + ), + ), + shape=tuple(robject["attributes"]["Dim"]["data"].tolist()), + ) + + names = None + if "dimnames" in robject["attributes"]: + names = _dispatcher(robject["attributes"]["dimnames"], **kwargs) + if names is not None and len(names) > 0: + return MatrixWrapper(mat, names) + + return mat + + +def _as_dense_matrix(robject, order: Literal["C", "F"] = "F", **kwargs) -> ndarray: + """Convert an R matrix to a `NumPy` array. + + Args: + robject: + Dictionary containing parsed R matrix data. + + order: + Memory layout for the array. + 'C' for row-major, 'F' for column-major (default). + + **kwargs: + Additional arguments. + + Returns: + A NumPy array or wrapped array if dimension names exist. + """ + _cls = get_class(robject) + + if order not in ["C", "F"]: + raise ValueError("order must be either 'C' or 'F'.") + + if _cls not in ["ndarray"]: + raise TypeError(f"obj is not a supported dense matrix format, but is `{_cls}`.") + + mat = ndarray( + shape=tuple(robject["attributes"]["dim"]["data"].tolist()), + dtype=robject["data"].dtype, + buffer=robject["data"], + order=order, + ) + + names = None + if "dimnames" in robject["attributes"]: + names = _dispatcher(robject["attributes"]["dimnames"], **kwargs) + if names is not None and len(names) > 0: + return MatrixWrapper(mat, names) + + return mat + + +def read_dgcmatrix(robject: dict, **kwargs) -> spmatrix: + """Parse an R dgCMatrix (sparse column matrix). + + Args: + robject: + Dictionary containing parsed dgCMatrix data. + + **kwargs: + Additional arguments. + + Returns: + Parsed sparse column matrix. + """ + return _as_sparse_matrix(robject, **kwargs) + + +def read_dgrmatrix(robject: dict, **kwargs) -> spmatrix: + """Parse an R dgRMatrix (sparse row matrix). + + Args: + robject: + Dictionary containing parsed dgRMatrix data. + + **kwargs: + Additional arguments. + + Returns: + Parsed sparse row matrix. + """ + return _as_sparse_matrix(robject, **kwargs) + + +def read_dgtmatrix(robject: dict, **kwargs) -> spmatrix: + """Parse an R dgTMatrix (sparse triplet matrix).. + + Args: + robject: + Dictionary containing parsed dgTMatrix data. + + **kwargs: + Additional arguments. + + Returns: + Parsed sparse matrix. + """ + return _as_sparse_matrix(robject, **kwargs) + + +def read_ndarray(robject: dict, order: Literal["C", "F"] = "F", **kwargs) -> ndarray: + """Parse an R matrix as a NumPy array. + + Args: + robject: + Dictionary containing parsed dgCMatrix data. + + order: + Memory layout for the array. + + **kwargs: + Additional arguments. + + Returns: + Parsed dense array. + """ + return _as_dense_matrix(robject, order=order, **kwargs) diff --git a/src/rds2py/read_rle.py b/src/rds2py/read_rle.py new file mode 100644 index 0000000..9a33716 --- /dev/null +++ b/src/rds2py/read_rle.py @@ -0,0 +1,50 @@ +"""Functions for parsing R's Rle (Run-length encoding) objects. + +This module provides functionality to convert R's Rle (Run-length encoding) objects into Python lists, expanding the +compressed representation into its full form. +""" + +from .generics import _dispatcher +from .rdsutils import get_class + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def read_rle(robject: dict, **kwargs) -> list: + """Convert an R Rle object to a Python list. + + Args: + robject: + Dictionary containing parsed Rle data. + + **kwargs: + Additional arguments. + + Returns: + Expanded list where each value is repeated according to its run length. + + Example: + >>> # For Rle with values=[1,2] and lengths=[3,2] + >>> result = read_rle(robject) + >>> print(result) + [1, 1, 1, 2, 2] + """ + _cls = get_class(robject) + + if _cls != "Rle": + raise RuntimeError(f"`robject` does not contain a 'Rle' object, contains `{_cls}`.") + + data = list(_dispatcher(robject["attributes"]["values"], **kwargs)) + + if "lengths" in robject["attributes"]: + lengths = _dispatcher(robject["attributes"]["lengths"], **kwargs) + else: + lengths = [1] * len(data) + + final_vec = [] + for i, x in enumerate(lengths): + final_vec.extend([data[i]] * x) + + return final_vec diff --git a/src/rds2py/read_sce.py b/src/rds2py/read_sce.py new file mode 100644 index 0000000..763cbe4 --- /dev/null +++ b/src/rds2py/read_sce.py @@ -0,0 +1,86 @@ +"""Functions for parsing Bioconductor `SingleCellExperiment` objects. + +This module provides parsers for converting Bioconductor's `SingleCellExperiment` +objects into their Python equivalents, handling the complex structure of single-cell +data including multiple assays, reduced dimensions, and alternative experiments. +""" + +from singlecellexperiment import SingleCellExperiment + +from .generics import _dispatcher +from .rdsutils import get_class + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def read_alts_summarized_experiment_by_column(robject: dict, **kwargs): + """Parse alternative experiments in a SingleCellExperiment.""" + _cls = get_class(robject) + + if _cls not in ["SummarizedExperimentByColumn"]: + raise RuntimeError(f"`robject` does not contain a 'SummarizedExperimentByColumn' object, contains `{_cls}`.") + + objs = {} + + for key, val in robject["attributes"].items(): + objs[key] = _dispatcher(val, **kwargs) + + return objs + + +def read_single_cell_experiment(robject: dict, **kwargs) -> SingleCellExperiment: + """Convert an R SingleCellExperiment to Python SingleCellExperiment. + + Args: + robject: + Dictionary containing parsed SingleCellExperiment data. + + **kwargs: + Additional arguments. + + Returns: + A Python SingleCellExperiment object containing + the assay data and associated metadata. + """ + + _cls = get_class(robject) + + if _cls not in ["SingleCellExperiment"]: + raise RuntimeError(f"`robject` does not contain a 'SingleCellExperiment' object, contains `{_cls}`.") + + robject["class_name"] = "RangedSummarizedExperiment" + _rse = _dispatcher(robject, **kwargs) + + # check red. dims, alternative expts + robj_reduced_dims = None + robj_alt_exps = None + col_attrs = list( + _dispatcher(robject["attributes"]["int_colData"]["attributes"]["listData"]["attributes"]["names"], **kwargs) + ) + + for idx in range(len(col_attrs)): + idx_col = col_attrs[idx] + idx_value = robject["attributes"]["int_colData"]["attributes"]["listData"]["data"][idx] + + if idx_col == "reducedDims" and idx_value.get("data", None) is not None: + robj_reduced_dims = _dispatcher(idx_value, **kwargs) + + if idx_col == "altExps": + alt_names = list(_dispatcher(idx_value["attributes"]["listData"]["attributes"]["names"], **kwargs)) + robj_alt_exps = {} + for idx, altn in enumerate(alt_names): + robj_alt_exps[altn] = _dispatcher(idx_value["attributes"]["listData"]["data"][idx], **kwargs)["se"] + + # ignore colpairs for now, does anyone even use this ? + # if col == "colPairs": + + return SingleCellExperiment( + assays=_rse.assays, + row_data=_rse.row_data, + column_data=_rse.column_data, + row_ranges=_rse.row_ranges, + alternative_experiments=robj_alt_exps, + reduced_dims=robj_reduced_dims, + ) diff --git a/src/rds2py/read_se.py b/src/rds2py/read_se.py new file mode 100644 index 0000000..8da0a09 --- /dev/null +++ b/src/rds2py/read_se.py @@ -0,0 +1,110 @@ +from summarizedexperiment import RangedSummarizedExperiment, SummarizedExperiment + +from .generics import _dispatcher +from .rdsutils import get_class +from .read_matrix import MatrixWrapper + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def _sanitize_empty_frame(frame, nrows): + if frame.shape == (0, 0): + from biocframe import BiocFrame + + return BiocFrame(number_of_rows=nrows) + + +def _sanitize_assays(assays): + res = {} + for k, v in assays.items(): + if isinstance(v, MatrixWrapper): + res[k] = v.matrix + else: + res[k] = v + + return res + + +def read_summarized_experiment(robject: dict, **kwargs) -> SummarizedExperiment: + """Convert an R SummarizedExperiment to Python + :py:class:`~summarizedexperiment.SummarizedExperiment.SummarizedExperiment`. + + Args: + robject: + Dictionary containing parsed SummarizedExperiment data. + + **kwargs: + Additional arguments. + + Returns: + A `SummarizedExperiment` from the R object. + """ + + _cls = get_class(robject) + + if _cls not in ["SummarizedExperiment"]: + raise RuntimeError(f"`robject` does not contain a 'SummarizedExperiment' object, contains `{_cls}`.") + # parse assays names + robj_asys = {} + assay_dims = None + asy_names = list( + _dispatcher( + robject["attributes"]["assays"]["attributes"]["data"]["attributes"]["listData"]["attributes"]["names"], + **kwargs, + ) + ) + for idx, asyname in enumerate(asy_names): + idx_asy = robject["attributes"]["assays"]["attributes"]["data"]["attributes"]["listData"]["data"][idx] + + robj_asys[asyname] = _dispatcher(idx_asy, **kwargs) + if assay_dims is None: + assay_dims = robj_asys[asyname].shape + + # parse coldata + robj_coldata = _sanitize_empty_frame(_dispatcher(robject["attributes"]["colData"], **kwargs), assay_dims[1]) + + # parse rowdata + robj_rowdata = _sanitize_empty_frame(_dispatcher(robject["attributes"]["elementMetadata"], **kwargs), assay_dims[0]) + + return SummarizedExperiment( + assays=_sanitize_assays(robj_asys), + row_data=robj_rowdata, + column_data=robj_coldata, + ) + + +def read_ranged_summarized_experiment(robject: dict, **kwargs) -> RangedSummarizedExperiment: + """Convert an R RangedSummarizedExperiment to its Python equivalent. + + Args: + robject: + Dictionary containing parsed SummarizedExperiment data. + + **kwargs: + Additional arguments. + + Returns: + A Python RangedSummarizedExperiment object. + """ + + _cls = get_class(robject) + + if _cls not in ["RangedSummarizedExperiment"]: + raise RuntimeError(f"`robject` does not contain a 'RangedSummarizedExperiment' object, contains `{_cls}`.") + + robject["class_name"] = "SummarizedExperiment" + _se = _dispatcher(robject, **kwargs) + + # parse rowRanges + row_ranges_data = None + if "rowRanges" in robject["attributes"]: + row_ranges_data = _dispatcher(robject["attributes"]["rowRanges"], **kwargs) + + return RangedSummarizedExperiment( + assays=_se.assays, + row_data=_se.row_data, + column_data=_se.column_data, + row_ranges=row_ranges_data, + ) diff --git a/tests/data/.Rhistory b/tests/data/.Rhistory deleted file mode 100644 index 4e4f090..0000000 --- a/tests/data/.Rhistory +++ /dev/null @@ -1,512 +0,0 @@ -"', def: '", -defs[[i]] , -"'}) ", -"with o ", -"MATCH (p:OntoTerm { id: '", -multi_parents[j], -"'}) ", -"CREATE (p)<-[:parent]-(o) ", -"CREATE (p)-[:child]->(o) " -) -q_cypher <- gsub("\"", "", query_parents) -call_neo4j(q_cypher, con) -# quotes <- c(quotes, query_parents) -# call_neo4j(query_parents, con) -} -} -} -} -importOntology("~/Projects/work/scSearch/scripts/ontologies/v1/efo_v3.32.0.obo", "3.32", "EFO") -importOntology <- -function(onto_path, -version, -source, -neo4j_user = "neo4j", -neo4j_pass = "test") { -con <- neo4j_api$new(url = "http://localhost:7474", -user = neo4j_user, -password = neo4j_pass) -query_node <- -paste0( -"MERGE (o:OntoSource { source:'", source , "', version: '", -version, -"'})" -) -q_cypher <- gsub("\"", "", query_node) -# quotes <- c(quotes, query_node) -call_neo4j(q_cypher, con) -out <- -ontologyIndex::get_ontology(onto_path, extract_tags = "everything") -ids <- out$id -names <- out$name -parents <- out$parents -ancestors <- out$ancestors -is_as <- out$is_a -defs <- out$def -nss <- out$namespace -quotes <- c() -for (i in 1:length(ids)) { -query_node <- -paste0( -"MERGE (os:OntoSource { source:'", source , "', version: '", -version, -"'}) ", -"MERGE (o:OntoTerm { source:'", source , "', id: '", -ids[[i]] , -"', name: '", -names[[i]], -"', def: '", -defs[[i]] , -"'})", -"with os, o ", -"CREATE (o)-[:source]->(os) " -) -q_cypher <- gsub("\"", "", query_node) -# quotes <- c(quotes, query_node) -call_neo4j(q_cypher, con) -if (length(unname(unlist(nss[i]))) > 0) { -query_namespace <- -paste0( -"MERGE (o:OntoTerm { id: '", -ids[[i]] , -"', name: '", -names[[i]], -"', def: '", -defs[[i]] , -"'}) ", -"MERGE (n:OntoNamespace { id: '", -unlist(unname(nss[i])), -"'}) ", -"with o, n ", -"CREATE (o)-[:namespace]->(n) " -) -q_cypher <- gsub("\"", "", query_namespace) -call_neo4j(q_cypher, con) -# quotes <- c(quotes, query_namespace) -# call_neo4j(query_namespace, con) -} -} -for (i in 1:length(ids)) { -if (length(unname(unlist(parents[i]))) > 0) { -multi_parents <- strsplit(unname(unlist(parents[i])), ";") -for (j in 1:length(multi_parents)) { -query_parents = paste0( -"MERGE (o:OntoTerm { id: '", -ids[[i]] , -"', name: '", -names[[i]], -"', def: '", -defs[[i]] , -"'}) ", -"with o ", -"MATCH (p:OntoTerm { id: '", -multi_parents[j], -"'}) ", -"CREATE (p)<-[:parent]-(o) ", -"CREATE (p)-[:child]->(o) " -) -q_cypher <- gsub("\"", "", query_parents) -call_neo4j(q_cypher, con) -# quotes <- c(quotes, query_parents) -# call_neo4j(query_parents, con) -} -} -} -} -importOntology("~/Projects/work/scSearch/scripts/ontologies/v1/efo_v3.32.0.obo", "3.32", "EFO") -out <- -ontologyIndex::get_ontology("~/Projects/work/scSearch/scripts/ontologies/v1/uberon_7_27_2021.obo", extract_tags = "everything") -ids <- out$id -names <- out$name -parents <- out$parents -ancestors <- out$ancestors -is_as <- out$is_a -defs <- out$def -nss <- out$namespace -i < 1 -i <- 1 -query_node <- -paste0( -"MATCH (os:OntoSource { source:'", source , "', version: '", -version, -"'}) ", -"MERGE (o:OntoTerm { source:'", source , "', id: '", -ids[[i]] , -"', name: '", -names[[i]], -"', def: '", -defs[[i]] , -"'})", -"with os, o ", -"CREATE (o)-[:source]->(os) " -) -q_cypher <- gsub("\"", "", query_node) -source <- "UBERON" -version <- "TEST" -query_node <- -paste0( -"MATCH (os:OntoSource { source:'", source , "', version: '", -version, -"'}) ", -"MERGE (o:OntoTerm { source:'", source , "', id: '", -ids[[i]] , -"', name: '", -names[[i]], -"', def: '", -defs[[i]] , -"'})", -"with os, o ", -"CREATE (o)-[:source]->(os) " -) -q_cypher <- gsub("\"", "", query_node) -print(q_cypher) -library(neo4r) -library(ontologyIndex) -importOntology <- -function(onto_path, -version, -source, -neo4j_user = "neo4j", -neo4j_pass = "test") { -con <- neo4j_api$new(url = "http://localhost:7474", -user = neo4j_user, -password = neo4j_pass) -query_node <- -paste0( -"MERGE (o:OntoSource { source:'", source , "', version: '", -version, -"'})" -) -q_cypher <- gsub("\"", "", query_node) -# print(q_cypher) -call_neo4j(q_cypher, con) -out <- -ontologyIndex::get_ontology(onto_path, extract_tags = "everything") -ids <- out$id -names <- out$name -parents <- out$parents -ancestors <- out$ancestors -is_as <- out$is_a -defs <- out$def -nss <- out$namespace -for (i in 1:length(ids)) { -query_node <- -paste0( -"MATCH (os:OntoSource { source:'", source , "', version: '", -version, -"'}) ", -"MERGE (o:OntoTerm { source:'", source , "', id: '", -ids[[i]] , -"', name: '", -names[[i]], -"', def: '", -defs[[i]] , -"'})", -"with os, o ", -"CREATE (o)-[:source]->(os) " -) -q_cypher <- gsub("\"", "", query_node) -# print(q_cypher) -call_neo4j(q_cypher, con) -if (length(unname(unlist(nss[i]))) > 0) { -query_namespace <- -paste0( -"MERGE (o:OntoTerm { id: '", -ids[[i]] , -"', name: '", -names[[i]], -"', def: '", -defs[[i]] , -"'}) ", -"MERGE (n:OntoNamespace { id: '", -unlist(unname(nss[i])), -"'}) ", -"with o, n ", -"CREATE (o)-[:namespace]->(n) " -) -q_cypher <- gsub("\"", "", query_namespace) -# print(q_cypher) -call_neo4j(q_cypher, con) -} -} -for (i in 1:length(ids)) { -if (length(unname(unlist(parents[i]))) > 0) { -multi_parents <- strsplit(unname(unlist(parents[i])), ";") -for (j in 1:length(multi_parents)) { -query_parents = paste0( -"MERGE (o:OntoTerm { id: '", -ids[[i]] , -"', name: '", -names[[i]], -"', def: '", -defs[[i]] , -"'}) ", -"with o ", -"MATCH (p:OntoTerm { id: '", -multi_parents[j], -"'}) ", -"CREATE (p)<-[:parent]-(o) ", -"CREATE (p)-[:child]->(o) " -) -q_cypher <- gsub("\"", "", query_parents) -# print(q_cypher) -call_neo4j(q_cypher, con) -} -} -} -} -# UBERON -importOntology("~/Projects/work/scSearch/scripts/ontologies/v1/uberon_7_27_2021.obo", "7_27_2021", "UBERON") -importOntology("~/Projects/work/scSearch/scripts/ontologies/v1/uberon_7_27_2021.obo", "7_27_2021", "UBERON") -m <- sparseMatrix( -i = sample(x = 1e4, size = 1e4), -j = sample(x = 1e4, size = 1e4), -x = rnorm(n = 1e4) -) -mm -??sample -library(Matrix) -m <- sparseMatrix( -i = sample(x = 1e4, size = 1e4), -j = sample(x = 1e4, size = 1e4), -x = rnorm(n = 1e4) -) -m -??sparseMatrix -?sparseMatrix -install.packages("viewpoly") -viewpoly::run_app() -viewpoly::run_app() -viewpoly::run_app() -viewpoly::run_app() -suppressPackageStartupMessages(library(scater)) -suppressPackageStartupMessages(library(zellkonverter)) -set.seed(1000) -sce <- mockSCE() -dim(sce) -#> [1] 2000 200 -# Will use `Treatment` as a fake batch variable. -table(sce$Treatment) -writeH5AD(sce, file = "mockSCE.h5ad") -BiocManager::install("HCAData") -library("HCAData") -HCAData() -HCAData() -library("HCAData") -HCAData() -HCAData("ica_bone_marrow") -library(dsassembly) -restUrl("https://dev.cerberus.genomics.roche.com/v2") -ds <- getDataset("DS000020088") -dsassembly::activeUserCache(()) -dsassembly::activeUserCache() -dsassembly::userCache() -library(dsassembly) -library(dsassembly) -restUrl("https://dev.cerberus.genomics.roche.com/v2") -ds <- getDataset("DS000020088") -library(dsassembly) -library(zellkonverter) -library(jsonvalidate) -sce <- zellkonverter::readH5AD("~/Projects/GSM3138367.h5ad") -sce <- annotateExperiment(sce, -title="test_sce", -description="tst_sce_description", -annotation=NULL, -sources=list( -list(name="GEO", id="asbc7") -), -organism="Mus musculus", -namespace=list( -list(type="genome", id="GRCm38") -), -technology=list(name="scRNA-seq",details="10X Genomics") -) -row.names(sce) <- make.unique(row.names(sce)) -library(MultiAssayExperiment) -mae <- MultiAssayExperiment(experiments=list("experiment-1" = sce)) -mae <- annotateDataset(mae, -title="test", -description="test_desc", -authors=c("kancherj")) -library(dsdb.plus) -dsassembly::saveDataset(mae, dir="./Projects/work/test-datasets-upload/", stage.only=TRUE) -ds <- getDataset("DS000020088") -traceback() -library(dsassembly) -restUrl("https://dev.cerberus.genomics.roche.com/v2") -ds <- getDataset("DS000020102") -ds <- getDataset("DS000000267") -ds -rowRanges((ds)) -rowRanges(ds) -rowRanges(rowdata(ds)() -rowRanges(rowdata(ds)) -rowRanges(rowdata(ds)) -rowRanges(rowData(ds)) -experiments(ds) -experiments(ds)[["RNA-Seq_hsa_gene"]] -rowRanges(experiments(ds)[["RNA-Seq_hsa_gene"]]) -as(rowRanges(experiments(ds)[["RNA-Seq_hsa_gene"]]), "data.frame") -View(as(rowRanges(experiments(ds)[["RNA-Seq_hsa_gene"]]), "data.frame")) -View(as(rowRanges(experiments(getDataset("DS000000267"))[["RNA-Seq_hsa_gene"]]), "data.frame")) -library(dsassembly) -restUrl("https://dev.cerberus.genomics.roche.com/v2") -ds <- getDataset("DS000020088") -ds <- getDataset("DS000000264") -library(dsassembly) -restUrl("https://dev.cerberus.genomics.roche.com/v2") -ds <- getDataset("DS000020104") -library(genomitory) -hits <- searchFiles(type="collection", n=Inf) -# Ignore the FeatureDB legacy feature sets. -hits <- hits[hits$project != "GMTY28",] -descriptions <- list() -collections <- list() -all.genes <- all.ids <- integer(0) -counter <- 0L -for (h in seq_len(nrow(hits))) { -cursets <- getFeatureSetCollection(hits$id[h]) -descriptions[[h]] <- mcols(cursets) -descriptions[[h]]$size <- lengths(cursets) -collections[[h]] <- data.frame(id = hits$id[h], number = length(cursets), title=hits$title[h], description=hits$description[h], species=hits$organism[h]) -all.genes <- c(all.genes, unlist(cursets, use.names=FALSE)) -all.ids <- c(all.ids, rep(seq_along(cursets) + counter, lengths(cursets))) -counter <- counter + length(cursets) -} -descriptions <- do.call(rbind, descriptions) -collections <- do.call(rbind, collections) -u.genes <- unique(all.genes) -all.genes <- match(all.genes, u.genes) -by.gene <- split(all.ids, factor(all.genes, seq_along(u.genes))) -by.set <- split(all.genes, factor(all.ids, seq_len(nrow(descriptions)))) -gathered <- list( -list( -id = "GMTY17:GRCm38/GRCm38.IGIS4.0.genes.rds@REVISION-3", -field = "symbol" -), -list( -id = "GMTY17:GRCh38/GRCh38.IGIS4.0.genes.rds@REVISION-3", -field = "symbol" -) -) -found.genes <- found.symbols <- character(0) -for (x in gathered) { -current <- getFeatures(x$id) -found.genes <- c(found.genes, names(current)) -found.symbols <- c(found.symbols, mcols(current)[[x$field]]) -} -keep <- found.genes %in% u.genes & !is.na(found.symbols) -found.symbols <- c(u.genes, found.symbols[keep]) # get the Ensembl ID at the front. -found.genes <- c(u.genes, found.genes[keep]) -symbol.mapping <- split(found.symbols, factor(found.genes, levels=u.genes)) -dir <- "assets" -dir.create(dir) -saveTabbedIndices <- function(y, path) { -x <- vapply(y, function(z) { -z <- sort(z) # convert to diffs to reduce integer size -z <- c(z[1] - 1L, diff(z)) # get to 0-based indexing. -paste(z, collapse="\t") -}, "") -write(x, file=file.path(dir, path)) -handle <- gzfile(file.path(dir, paste0(path, ".ranges.gz"))) -write(nchar(x), file=handle, ncolumns=1) -close(handle) -} -saveTabbedIndices(by.gene, path="gene2set.tsv") -saveTabbedIndices(by.set, path="set2gene.tsv") -collected <- sprintf("%s\t%s\t%s\t%s\t%s", collections$id, collections$number, tolower(collections$title), gsub("\t|\n", " ", tolower(collections$description)), collections$species) -handle <- gzfile(file.path(dir, "collections.tsv.gz")) -write(collected, file=handle) -close(handle) -collected <- sprintf("%s\t%s\t%s", gsub("\t|\n", " ", tolower(descriptions$name)), gsub("\t|\n", " ", tolower(descriptions$description)), descriptions$size) -handle <- gzfile(file.path(dir, "sets.tsv.gz")) -write(collected, file=handle) -close(handle) -collected <- vapply(symbol.mapping, function(x) paste(gsub("\t|\n", " ", tolower(x)), collapse="\t"), "") -handle <- gzfile(file.path(dir, "genes.tsv.gz")) -write(collected, file=handle) -close(handle) -library(dsassembly) -dataset <- "DS000012156" -ds <- getDataset(dataset) -ds <- getDataset(dataset) -library(GenomeInfoDbData) -install.packages("BiocManager") -BiocManager::install("GenomeInfoDbData") -BiocManager::install("GenomeInfoDbData", version = 1.2.7) -BiocManager::install("GenomeInfoDbData", version = "1.2.7") -setwd("~/Projects/public/BiocPy/rds2py/tests/data") -setClass("FOO", slots=c(bar="integer")) -# pairlist -y <- pairlist(runif(10), runif(20), runif(30)) -saveRDS(y, file="pairlist.rds") -y <- pairlist(sample(letters), pairlist(sample(11), runif(12))) -saveRDS(y, file="pairlist_nested.rds") -y <- pairlist(foo=sample(letters), bar=pairlist(whee=sample(11), bum=runif(12))) # with names -saveRDS(y, file="pairlist_names.rds") -y <- pairlist(aaron=sample(letters), bar=list(sample(11), runif(12))) -attr(y, "foo") <- "bar" -saveRDS(y, file="pairlist_attr.rds") -# altrep -scenarios <- 1:15 -saveRDS(y, file="altrep_series.rds") -x <- 1:100 -names(x) <- sprintf("GENE_%s", seq_along(x)) -saveRDS(x, file="altrep_attr.rds") -x <- as.character(1:100) -saveRDS(x, file="altrep_strings_deferred.rds") -x <- c(NA_integer_, 1:10, NA_integer_) -x <- as.character(x) -saveRDS(x, file="altrep_strings_wNA.rds") -x <- as.character(1:100 * 2) -saveRDS(x, file="altrep_double_deferred.rds") -x <- c(NaN, 1:10, Inf, -Inf, NA) -x <- as.character(x) -saveRDS(x, file="altrep_double_wNA.rds") -# atomic -y <- rpois(112, lambda=8) -saveRDS(y, file="atomic_ints.rds") -y <- rbinom(55, 1, 0.5) == 0 -saveRDS(y, file="atomic_logical.rds") -y <- rbinom(999, 1, 0.5) == 0 -y[sample(length(y), 10)] <- NA -saveRDS(y, file="atomic_logical_wNA.rds") -y <- rnorm(99) -saveRDS(y, file="atomic_double.rds") -y <- as.raw(sample(256, 99, replace=TRUE) - 1) -saveRDS(y, file="atomic_raw.rds") -y <- rnorm(99) + rnorm(99) * 1i -saveRDS(y, file="atomic_complex.rds") -y <- sample(LETTERS) -saveRDS(y, file="atomic_chars.rds") -y <- c("α-globin", "😀😀😀", "fußball", "Hervé Pagès") -saveRDS(y, file="atomic_chars_unicode.rds") -vals <- sample(.Machine$integer.max, 1000) -names(vals) <- sprintf("GENE_%i", seq_along(vals)) -attr(vals, "foo") <- c("BAR", "bar", "Bar") -class(vals) <- "frog" -saveRDS(vals, file="atomic_attr.rds") -# lists -y <- list(runif(10), runif(20), runif(30)) -saveRDS(y, file="lists.rds") -y <- list(sample(letters), list(sample(11), runif(12))) -saveRDS(y, file="lists_nested.rds") -y <- list(list(2, 6), list(5, c("cat", "dog", "bouse"), list(sample(99), runif(20)))) -saveRDS(y, file="lists_nested_deep.rds") -df <- data.frame(xxx=runif(19), YYY=sample(letters, 19), ZZZ=rbinom(19, 1, 0.4) == 0) -saveRDS(df, file="lists_df.rds") -rownames(df) <- paste0("FOO-", LETTERS[1:19]) -saveRDS(df, file="lists_df_rownames.rds") -# S4 -y <- Matrix::rsparsematrix(100, 10, 0.05) -saveRDS(y, file="s4_matrix.rds") -setClass("FOO", slots=c(bar="integer")) -y <- new("FOO", bar=2L) -saveRDS(y, file="s4_class.rds") -?.row_data_path -??.row_data_path -showMethods(.row_data_path) -methods(".row_data_path") -methods(print) -methods(.row_data_path) diff --git a/tests/data/atomic_ints_with_names.rds b/tests/data/atomic_ints_with_names.rds new file mode 100644 index 0000000000000000000000000000000000000000..de8b7a30fb1ec21484805e292535aede2b12e473 GIT binary patch literal 264 zcmV+j0r&nNiwFP!0000018q>-4#7YW9o_DBwW@x^yEmSE0f`5R6iYlP+J3kA0_<`o z8I4UQJ2U6Znae6AB1N%^NNMkqUM}Y22~m{$;OZ^$X1o>f1FMf*0)K#bfH(tYk;^_! zz8V1!O(Wo3AHytuBXl{|z!Sl<&`X4#`7@*JWe|rMa?EEiW8e?*XJURWcvjqjYk1xi z^;Yyv!Pnr81kO7ckxOBGN7m)Od=J)FLR*VEAID+Gp05a8U6p6oc;}Ag87TJd;Og4# zH%(igs$&O^w`JqRy?WT;iCkB1xVxOM<*wbFs8dU|?WoU||80tUx9MYiNj@t_6@M43=PE-~(b|`xkNQ zD-swO?5`c{%WeSDZxi3v0wwHUS@Riz`1e{WHiPK>(J!n);xEdrzCHlb=XZp8g2W%p zSY!pFZymO_0?8ZQGX&XY|MXOl3P|0nP2mCyfb{!y=}Hbj`pPCfJCOXD-v4hw`tF{y z*F6BFZ>INO1IfQUQZA9RqsDL?wIWjU5#*B)JLf4&~lUNMX4HM)_Ni0d!ODh5zjHaBkD8F0} cW+5*SGXdj^;s5{tZ-LZ504I7n2w(yL0MfIGl>h($ literal 0 HcmV?d00001 diff --git a/tests/data/generate_files.R b/tests/data/generate_files.R index 0aae82b..31166ce 100644 --- a/tests/data/generate_files.R +++ b/tests/data/generate_files.R @@ -70,6 +70,11 @@ attr(vals, "foo") <- c("BAR", "bar", "Bar") class(vals) <- "frog" saveRDS(vals, file="atomic_attr.rds") +# scalars + +y <- 10 +saveRDS(y, file="scalar_int.rds") + # lists y <- list(runif(10), runif(20), runif(30)) @@ -106,3 +111,69 @@ gr <- GRanges( GC = seq(1, 0, length=10)) saveRDS(gr, file="granges.rds") + +# factors + +f1 <- factor(c("chr1", "chr2", "chr1", "chr3")) +saveRDS(f1, "simple_factors.rds") + +# Rle +x2 <- Rle(LETTERS[c(21:26, 25:26)], 8:1) +saveRDS(x2, "simple_rle.rds") + + +# SummarizedExperiment + +nrows <- 200 +ncols <- 6 +counts <- matrix(runif(nrows * ncols, 1, 1e4), nrows) +rowRanges <- GRanges(rep(c("chr1", "chr2"), c(50, 150)), + IRanges(floor(runif(200, 1e5, 1e6)), width=100), + strand=sample(c("+", "-"), 200, TRUE), + feature_id=sprintf("ID%03d", 1:200)) +rowd <- DataFrame(seqs = rep(c("chr1", "chr2"), c(50, 150))) +colData <- DataFrame(Treatment=rep(c("ChIP", "Input"), 3), + row.names=LETTERS[1:6]) + +se <- SummarizedExperiment(assays=list(counts=counts), + rowData = rowd, colData=colData) + +rse <- SummarizedExperiment(assays=list(counts=counts), + rowRanges = rowRanges, colData=colData) +saveRDS(se, "sumexpt.rds") +saveRDS(rse, "ranged_se.rds") + +# SingleCell Experiment + +library(scRNAseq) +sce <- ReprocessedAllenData("tophat_counts") +sce_subset <- sce[1:100, 1:100] +saveRDS(sce_subset, "simple_sce.rds") + +# lists + +x <- list(github = "jkanche", fullname=c("Kancherla", "Jayaram"), + collab=list(github = "ltla", fullname=c("Lun", "Aaron"))) +saveRDS(x, "simple_list.rds") + +# frames +dframe <- as.data.frame(lists_df) +saveRDS(dframe, "data.frame.rds") + +# MAE +library(MultiAssayExperiment) +patient.data <- data.frame(sex=c("M", "F", "M", "F"), + age=38:41, + row.names=c("Jack", "Jill", "Bob", "Barbara")) + +exprss1 <- matrix(rnorm(16), ncol = 4, + dimnames = list(sprintf("ENST00000%i", sample(288754:290000, 4)), + c("Jack", "Jill", "Bob", "Bobby"))) +exprss2 <- matrix(rnorm(12), ncol = 3, + dimnames = list(sprintf("ENST00000%i", sample(288754:290000, 4)), + c("Jack", "Jane", "Bob"))) +doubleExp <- list("methyl 2k" = exprss1, "methyl 3k" = exprss2) +simpleMultiAssay <- MultiAssayExperiment(experiments=doubleExp) +simpleMultiAssay2 <- MultiAssayExperiment(experiments=doubleExp, + colData=patient.data) +saveRDS(simpleMultiAssay2, "simple_mae.rds") diff --git a/tests/data/ranged_se.rds b/tests/data/ranged_se.rds new file mode 100644 index 0000000000000000000000000000000000000000..badac614eecbd6633a3bae9935f5f779bf5ac6ef GIT binary patch literal 10775 zcmeH}*H;q&)29`sNe2PxN)eUbdyuXoAR?kd=nzq=^b$gqCLk)HQUXXX(xgK|?+}m{ zdW&?DK!7wL`=0mPz1;l^cFuF=;<=mOnKNgElIX7dx2_$A`Mp{zOMi-%8cvR>tUW^WPonC{*ksoTo}S;>i!vGI`!$s&%l{TSPERb0vdCL5Ez zj~~Df{Mi3+W_t?5Ti|`QN3`mne`FOl?Cm@9q7M^eAITi!Q%Q)AKNZurxhbw6ubS3h z@&~tdXe`C=Q3VY5F(2*=N%|GrBU63U_!{Ze$0>X__19{380DM{JeystJoMq-J-gV$ zfcm6VR28NJPocSAYW_5N6OSgK#`CXUwR#5hH&tsdOS^d;zTIz)3|*t;s(d62X*HQ# zSXt;=nDmcqsEAFEt)T8`9PIvQXeg|mg~86{Xbs^Z&HL=DHMGf3-OD;o5udXqB~@M{ z-%y;R|I`*ID1l-}Fs|p(_cgh#Knz9|@N=oNLl&0B%jFTu#X0&hi zJ8jq}@{!A^o!SYaK5Jn4NCfLREwrnx%t@raCAUWyP&40KsLmzNsIzNV8DJ>5`lY}l zyKL@X<5Sz=OCm=Mj3>76)-xP)obw$?72LY%jozuasWZOEV_8d1y%&95E{zkI$L zPLUq#JA@2y$!m*u!Ha3PDGB9bHa1e(K+2;Z7W#Kuh0 zC+Yv>HYhv?#{W516o%Evq}@Cc`Qr9PAJt_dtYRD$zXCSa*U)DBv1KpZw0C#ir1bWO zBS^8+B6U)<@ivi)=2#n(-hUwWvU;YNlHA9r zjx)BE1s?s&8k)4Dw|Bmf+7k4=e33vy(VwRYqu;G!R6^v^n(hlHPMs$dI&h?gRT>m3 zE3w6vS;ZF;f$xq|>@F--wTK^`*c9yTl<70TId0W@;v%#Pprldk_e?tWVELNqqJ zmE*CIl5pUGEE|VrFK#3rRWzW#o}ree#3sIh7m4R@G3{L?vDc~!2!l)0A7w8X+?=*z z+s$!}u~Pj0C?rRmoq;cYXR=8@gu*AxE~)FxV8o_2RH5%k(G&LFzhjQP%*C3Pa^xZy zg?aE9@vHZAk1g3B)4wiQQ}{l(cOu6Aky^jP?US`0TfU^A0;+!y<^B3y`4Tb1?@|_8& z=3wu}vXx2S7q*6ciw}ev^`1SHc_hZ#vVN#v*bp%ocOyRRp;p?n9OJ_N7aTuE-wCe* zQ|{f?Mjdp-A7n}BJ8T9Cr$HjHHW+p{qhpIHK-w%v2 z&q8$9`6D5hr1J)uPn4RiIoRhZGOY9}dICrZjO+XdzjZ20dkNUPaJntV?{gH_m3^*u zit9NGkpA7eN-H**3t5^vB!eN6_IVmU!g@lbQ?$G%k$1s)210MAXw^@;L`nAY{+&+? zS>9e}OpeHBlU1bpXxKSHcZ>I`o{0V1>wAh+{=AXu$q#c3^Grpam9Ujg(M`MkZ%vc@ zFxoIrM`R+8?d=4e;@j)Iid0Ozk^iF~i%b-N~Z}U$z}wS|ISGT4qeDwZ7{GL7qI2 zsiqCvxg^Nr2Qq`<)dlC9%k_i!bQpC1;!xa&;zWC-#>dD-;W05PsrpUnip5x;5NY8u zzJ{YMJjawLy_Arp?!Lb63o9?jzN$+uwgs<(8>*QD89$*s4UO6kI9S!_erRTm5lhjj zg2^5CL##;DueuniG7TtRIHe{luIllk*ll;fe$D2QPWRowEDfn)Xie0&@D;r+?7Sm= z#r}|6Ox2N@CR#P2Mt8n%UH-zGGMlO~ew{7Ath3QL$^r@c2rI0y$C(XGDJRBGM6IL zIV9uCN1sGW_?rEFY8F~)EyZ2p{192Gr^WzwF#1cKtD-=&jl)1ltv_q1&CIBo<+@L4 ze~qkQy;pD`-Y%6>lm0$td69#T5A6!wE;2(lWC7IfW9j$3{NK$QsH_ooByVL$ zZj-GUUt_-@wv=o&+^$k)bCz9I$}?*Es&We~&1x`TXU0A}2y0X)`i|E;)WrW?dwuAF ze2)Qt(Y`g8m5FmoB)Z4;C6^-@)>pyD(s7D(p`+rTnizc$Z@(AJ=O?MwdJ4~~&eG${ z)UQ&ww1=uwd^;~0K2(DnQT1-zIO(k}#Lz(QSjf^NBsgPXKM8PK`e&B%0iC!za%pAz zY@N8*d8%f%Y7f?<>=j|#$i@oK6|M)^?Fp`vB5LFWmU|`Ev>lc@EiJLWM>^pL^;fzL zZGa(vjX4naCD3Ov%jRWvvq=IUg^Y^7wCBf|V3fmFbO`p z>qWLZO~#7EXrX2|H!l$JAeF;KlJ7buTTgieeaCBD@--L(a5!9bO+Jd%#tEgg+u08P zGwA>B^1B)B4h!UN@^tlyFu#hee8JeBcsk4hXj*N5ge29fYxKBc=Uc5)vr;+%Izstg znOA&fgX-lt%Lv(k3?(0bN zn_KGpSN5o8P|_`!T}xa|`2B(~U~k3q{W71s$6}*Oj{f2s)=q9Uk~h!h1SMZBaEDi< z`Q0Bk=#ZIrQ3+E$SKc&fu3nmzS+-d5PcZaR#DT&Qw6A zmcagOpVk8V#}MfTO)35mzWtPJ88D9zf?vM(E&Z89D^77q;PT0f-S{O@=CHU5nPJpA z?E5H>R5>#~wwZ3OT2heHYB!7t zhq;|PTismF6(3SAG`(??R(sCuJC8s|eCqag=Do_!R;xpxxXKl1(@0)jQrK*3$mFM- z(t$lCYVkNejeNvmV0+`3mrA@;?;<FuJi!Awn^)2Cu6%YU49E(!$v|!9~zAX&AXNW;Q2Ds{$e4-g3AkiS|JFHc40_i|IJE0!B&ap zqBUZ6w+fg!a_%^#9sBZ&VVLmQl7)JtW;i_L@vUQ#KU(g?zt*!MDa#>^EI~iExR)dK znkSAg4tF~n3aT?^*#XwO#f#3UB+ZNG+~>|R8#pA)G$Q2YlF10sz8hrB9_{<(CTnJa z_|?|98rxKzWFaVa{=^0^SP^=Ka{42I<0~DX>Pxc!_2bJOUh`{G{~7ZvQv`E0@K|MY zsvPSyoYNMdGR-=FItgBs%fP2&qSIj7oLbU== z-Y0qv3F}Al<|j=BGSU1ZY*u~C?hlTO)d4$Qu2>(J9_NvC*Jg;xz~R5fS0&OtL07!F zpR!s9A+GtnhwKF~@5eF*X1w|o{$-QxG(?J^;-bG@&@>2Dchs7Y&qWyWHIrWET@E)` zC~x;v^N<`aqMwx*{W|r_!v4qr@=dUiZ>W13^M2UhI&9`;S$RU2^4LcQ%*`C?a!6)6 z@_)QE^3z@*&OhqJ`GBl+3X?7gR77UKz`BK#W(V}l1J#1JJDyL|w>5d3%WDwCi#r^j zY2(nT0Af^1+R4D3$A?)>;tiNoa)7$(M(Y zMMY=A-Tn8!EoMYQo*2cvYMRkg(OfJeGZODsK6+1j)emABt2p5pP-nwG2nMeCQs(W9 z_M=M}I6+0pAi7Zd4+BcCvGbGe2I+Z$oeCzFX7u>E)VBv22MmI28p3sW<_*o4O6~x^ zE{AB~SFL>R3a^8`2cPxBwg8X)u4!|zl-gBMQ(m6ChT1WyS2+)DUyYs=*KKNL8lt)^ zZ);7i&THDMiiWCmrMeAqhFT-MTWgSm^ueNpm##H}A>(Em&i?mKW?^W7keE$7mZ4t` zRcmd7m(c=U)jZHsj>UYhM8}SB>nbMxs*!+>_9sYUgP;@Y^W2P*QeZ`r!$9Kl`j~hZ zPHz}PYL2if?h|5|>H-w|J6X^a*ioA>*s5`!!M&TLmBiiK%i62>4t|gA=T;LxSNyX0 zyMj^E>iApSBs8v#=Q!OuU}!M$$3zIc`+4@YOFRn2{hEDn?!kW6uwhM!{Yw=AE^v`9 zz6Ehh16x%_FT?7?{CL6tjAKtk+vD4bFQFvNvMFd$7WH-6r(SKsyt`0xAbqV%-*@Y| zGHGFw;m#2GvrG}E^eiw4kneEbes!YHkjMP)HG5E3O_H&vj$LuPf{ilGxvi^b@IDs_ zY`ajp!krV;K3oD?<8YBpQ%;M#zKmclfMK(gvfd4r_V@F6vgo&kpkQ zV;YWFx%PFnGw|=6na%or=c^X%rit!#;J|t56t5^vIIp{iyXdOkzZ)l>CvLWAV}gkL z$=3s(33Ii9`kCy+36|vT63OV4QB9=D1CFDS@H?(&YizR;Uj_Y`)a50Bsuw-HHg<}D zZLz)iO<(t=5Uh3Ua(ltm!K@__+{Fp@wR?T$WJI_O{g8ZE4fRn4S3s_Q*pzQK`^M*L z|H__7j_AvWXO2gtbi9wR%R1ESZWBmBiL8*B0Wy68-WK7XxgxZ;_7E#=#_AmoI&Bd7Y|M%YYnCL z#5O*nCxVdHT8c`ABr)%1mG(Bx?+C7PV-P{)lHQ7-7CXUHG*%3ddN(*#7i#98Z*)Tg zkQrFrb1}#K!tEs1HdM}IlM@L&fAR*#dnDDCH_Wo(7vK_K_EQeZ1bX2ao3Ab2($6d# zV~Y^8bPX6($Y0Uq`QwbsUf{ztw`!KlZbVLz1Cs&9-j$S4cjIvAY-LVdu8 zhX-2Gf;x5}c$$U5a=!bkgvGvS? z>KlRWt@(3&XG|gH4`RY^l+7{989^$g&3y#YM^EQRw`m5qZ!GetTP~e99ltvpe*(zL z$Lga*k6V@3V1}y4#b`%wlr%wHDNxT){G9I(vGR%PUelv8_%MIi4W@wAq`vl_Dq>;} zd3;9OG`xnau!2zOjF$<3oKJ^ykqm+`PoqWmYRV5xd*ul&A0A`6SU%IXGz!fdB7Zm& z#SWE7PdC_mh$e#?I-UQjk9N3c`0U1UuoQ5J$$1qs+4&W?WtTccK#mMacH+*`0<`P~X|gwFzwogO#&|MQTcA z@($Ua^?aM$uX+O^_Gj$qy#*EA`eYiXbHNQ$L}PQJ{byq{OdkR`{d|XkYBT{iUjoCZ za)H(wi_;i_%Z~svNJ-crGp+M=9{Zelv?wBe=N^uE7EcSfXrpjL)tMDbdyA35WJB`jec!HSp zn(@rTla}OWykyf8+kowp!}UFCjg9pI6CpCSny+u)iyXgEEdwp|aps+^4!3-2VWPkJ{F0DnVdGapF# zECoFzuTbx-SSoDm-$v{=$-Mq`&PjNGw|Lj1uhr5@OH)ofy-^7v6mu>ycykZ35_5Vw zQ)oIm09Fs2>8>ZZN6oPKXU}&5R8x3GuNpvXv?PM?A49eB+gX z7|O(X&0?Tof^)rTi!W@9in&6G?cxpb03BHHAgosdJdSO)KvplEcaK_4)LNJK3Tf+H zrjJ*I_+Rl)tE&u~(#Ix0-0EEPhzC6%JV|^$4c=i7uHeGze+*h4l02E@-MqYdb>c^Q zj|C!m&CllzsYxBJm9X$yf?)?M>LoYBMW}dU8$6=biy%LD{0P`_^5 zrR>*F&HXOnH&YBD_nJ%h6?y%0DEC~4z@y@BGQB^I9;UkXM4VeKOC9Hw;|S%3C-vpEFoQRSVQ&U;+-+cBrKL1HV7WoVAK=V)FH-J?EjvGx7) zHSrp|){38VZp;JU?Js?LU)#$sw8=BKR^vY}h3hpp-_pKk04u^13kzZi86CFPQ}Eo8 zZJ^9b9=zyYlI?D^`nVNEws+uGxc4)4WW=e{7d{37wz>21ZgZgzYXf?GCm+&4V!YAs zt~Cc2jT%3@B3NcD@WQ@g7?d>L3wsHP4A1_vX&Fi9kopvIu$tJa&dYl)MnnA81UG~{ zIF>)6+MPs?c2sATyk`(JuJf1YVT7>}+Hw9{e)VnZVjL*F`Pb{kh6FKxUks=xjWVLe z!yq?pQR|U}RL8!U?DF59wzoU)*6cZ*xZ@)(eVydU{B*v#YTN&$itfp98Yntf1fj*RR!faUj^Kpz3a&gqYnM8q@ByG#X4s+ z{EQ_i)_VJGV(wZ!wep*7iDtY*awwBxS}Hv$amEI=H9sGQz9!J9^zNCq2TErW_ycCv z85&jM%Po+Gs#l^mTZn%IA=!G4@dgu4^M`{D69bZV7{hIka!!{V!9!CRDPo%{)^Wa5hZ^BD4iK&%iBneJOAdp+2JV!6S)ykalm@gu4AFXH9_7 ztxUUopQ*E=0*nc8uvs{FSLR6=+F?~TsVi=T)k}inH%lMp;k$&)k>tV@@= zApvB{g8E!{^1?dmv;0^QKvxTbQ>~|Y;um+$t;T5!vaNrCY3fsD7~zUfN{TZuFfY8_ zI?j;yj$rIv&{J2ZORjfk66yt1!1l(=QL3Gr>zUhiWDTPG#2R2D&K?xJ)tIM|H|%m&wL1hRv=w0l^#BKjT7jPw6_5?-rXzVfHj{1y@(<>#ST0_C2y>xfc4 z{q~g*MPmJ9YGK)_`D{;8PF#TW=WhAxah0!@Ls<=b8eL_d@MjYw-Kk3#n{$0^FR;rhZ(rKH%+yLYR-$|7ynn;d~uP zKLKPKSX2X*zo87V{=`};JUp#27b;+c=*ya2_z1}5O{7+oIz|a!8 zd}jycJE4`pRgVB%P31ld(I|+`;wEo3%_pzdmlQp=~v7vc1jI z*XAU~W_M&(nZiUG6Fob#bLwU`Ly%X{9nrfsTwvAUE`}en$Q-PjHI4z91(E6b5F|6| z!ti8F_@(28XSa-PJf;>;qx_#!lAN^=BGs>MX_Cb&tf#uHB zLZ~_bf86?mUh{(l8F#+-gEkpL0^W7o>Do5_xKFbBGWe~Ib1B;qoFMfOc3oz7<*NY% zkzJPK%^K!cSx;K@LdrCM)l}!~J_`N&>=(WUG5lDsMLbhwo0Mi*lNDIMoSZeMIOlnf zmgO)!FueLEpl5{^c6i+^c00Z7fo_vax>D;arwdf--=C34vs*W7$$WmsTYqSvpuwaG zC66+*P6SVj!XY$S7eGVY^=Rk{_j(uZW#F=#CQZt^fZ)E9l)3`$d{1qb0)*3_ADw0t zvrmW&nSy4sw^u5tX-<-KD6Hs)T}%g6SO99=#d58pUQJR)J08)R9OCzZo1puOc;85n zn0B#;YGOsLo*5LQ5k7!@m$Yd0SqYQi2@Ky}NkF`AjpWvTVIqrhc4wekhGE60X9MY( z)hC`G$A@g4T3>L5?3S9r^B#aJ+q%p=RL;X5nLG;-)MOoOV?B$VlA#f9^e>5=3h(=4 z9v=$)d{X?yhOl3@qcury)4m-G*FWFu53~~a7-GgU22s9p`n^0~$Zh;c9x@*LLDTBe zc|=&GH-8lL&O&VCYsZ)g{swlo-+xq7C=vGtOaKrE^>#C3$mDljpQd%fcAT{r?;C5R zneD2);d?!87D8;`%qK`<8XvmfKA?AuoyzcT*Y?=-1fEo2`&iD*VoZ?=*n`-x>_A`6 z{$&~n>beR3X6$O^1psKFAO3IrCHcGXwNP$GyWcbypS?7U(;`=&@E!7}IteBQs?cS2 zfjL>Au1vlB-yGFQ$0bYz#<_A&!;XWN(zdPpB%Nc9RdpuR?g}3ZnZHwgpz@8l+a1h4 ztzUaq9N_u=4?91YR93CPt+>dkdXJHAzZIY0=g(?mr-11>c9AEG(oO9p!H(eLXhq5J59@ed;P;xp$Ammy90-+TA5fy@HLccLl^`gCqZ@e6ksk)6t`mX# z>k8BSIM0jU`z|bUO`!3lmhUX^wv7I#>Itpv2Y{RT&wD^$18tVwm??Dl_z@Q!#7%^Z zpbw3@q)Rn7Ms4;!^8BUjrf7+V0QZ8b4+Orx+roBk4HvG_p*m*^AM$8bBZWTCd-eBW z@15hP_-~U^u5hRXm5W3HkF++%uiA+iUXC%Ds&hjegjJQ6Wt41$upQ83hXREF_mj>ZQqqTG`0oze>@1 zGw&SJp8PK3i}&W1f?=qI(jIIO=RE7Q_B-n@vhKrO@BK5c5T@LO^vNbjLhv1#wZz@1 zCM+D4z;dcL8CU@Oexe7jc+Ps%D)KftJ-|U~DQvY|serRp>}3nA{Y-VhSE1yj$Htt9dK7wd>tEeQ_3zZQRHMT! zTc?M@lkxP7?qlxn)c~K54Cg*IU`&lP7917WRz|+L zH0ROz&H|O{{j>6fc{IJx)lAc>fcZK-vqN>1+?*u0R5+L;1k`!KqALZ+!mD0}$nU+& z0zu1SCk&|5^{Bed#dmwa|471chJq$&Xi76JjlT911IQyA{x-`xHe!82@F^8`DQ)q$ zS2yci;UzVh{TZllZg$ioVr8%_yCz(F(tI05{{+7Wty(jGRwG zWpnDyZX;J@7TA5zK3d{egP(hZW9mfxD76b)xW6`+m&K||48*Lem17b+3CZ@M=Govy z2dX=Y#iZ0qC&wOVxKo#pas8+{!OK6=R+y{bj20wqlh@7zxHf-Z;r7kwZs%s(n-L znAK-K;C=H;duu>UsYt|3A&&C|@{y<>;67MOL-2~7SEvsxRT=Y~Pdz#MA-@ciWFght zr%`zOn2)D04ZW2I{DXFs?Fb7v1$8*U0=rySanl?bvlajXFDaDUCjH3quA!BM!STM+ z)pPR&ZAmda`FVXfC1IhO+d_V2Z_R$)ecQo9X>M$2VIEx#oxgZxl@?`2G1BWO#X-nr qYh}>zW5e*{J4E=g0u0jP4#&XT{*W3{FTs#Y?QQ+5l+SusuKX_yDbQj7 literal 0 HcmV?d00001 diff --git a/tests/data/scalar_int.rds b/tests/data/scalar_int.rds new file mode 100644 index 0000000000000000000000000000000000000000..ad5b757d625a237b6e3ad9ef18354b92c0a7a6f2 GIT binary patch literal 50 zcmb2|=3oE==I#ec2?+^l35kr8);Op!XJ>TGUdKA&Ap4OXJ}sUE1_tSG6I<1RN&#mx B4z&OP literal 0 HcmV?d00001 diff --git a/tests/data/simple_factors.rds b/tests/data/simple_factors.rds new file mode 100644 index 0000000000000000000000000000000000000000..99b00a80dd8c0de842bd40f12e10035dbd46d77d GIT binary patch literal 107 zcmb2|=3oE==I#ec2?+^l35kr8);Op!XJ>TGUdKA&IQJ6w#Cxz3Yhp~H;O1g-o(#GBiH;!efZBTozB6gUkL3;MI88MUMSyKKoGqjmy J)GYwo0RTMNBwqjk literal 0 HcmV?d00001 diff --git a/tests/data/simple_list.rds b/tests/data/simple_list.rds new file mode 100644 index 0000000000000000000000000000000000000000..50771a1c8632d781189c38036e85eee15345e105 GIT binary patch literal 161 zcmb2|=3oE==I#ec2?+^l35kr8);Op!XJ>TGUdPJJJlP2A7_B(D#@r{FyW7h{}XI N26sbS5lKb{1^{E6Il%w` literal 0 HcmV?d00001 diff --git a/tests/data/simple_mae.rds b/tests/data/simple_mae.rds new file mode 100644 index 0000000000000000000000000000000000000000..8c9b0ec81db19c7fb794d635632fe6c52ce61683 GIT binary patch literal 751 zcmV8dU|?WoU||80tUx9MYiNj@t_1@lvm^rp3lmU)k%5JQ z6G*cs=jXU2mLy`4;wZ{5&r8frEe5g$Am)L^S-cXHv*GN_oE#{d*(pB>%?Q>!pcx?j zykJ3)8EAIoWEPiz?Pp*T2J)HE>|*qVG29R$5aWPGF{lBtIuL7M*o)+DCYbw*Q!8N1 z#Pn1k^$$%OcWO>*ZfaghNM%7PR5MJ9+0ic&O*KDMwQp)kVhY#`a49xMzfd0^Ms&~S zB2#8d)Fos^pZawRLwm9S*W3XHeJ zN_gB9W#%Rp!MVU3mIsbOSZuJNdlHej*pcFb^FL}j0fi0jq`{4DD8aPA=UP#a3Un$k z3HtzZAtFJ*BOH`C_<%Ie{!3ixbHnSh_ur4%&)ar*+x}n2IqC~P8ts2@bv{G!?{xcx zvu98Ebii!??Im81GOgq7pNrfxy~y*${vywl$qx6G?Oz`{vHd-J{Qh&9vp0(dX7B&d zc`p9vuXy`)S*9oF&iI7pGUk-bTxdE33A11;IDkrEnH;&`V1Z>$SHIv80}wE>u(Y%= z!YXWHj7`|s$P$aNg}Iq211SX|D7?X;$Fcvj*jrOC)3f%s1Js;Y_*nKoS(U|gyyMsY z+X-nW)gIrpe>CN%-HWn6`!CoTr4fM2)J&TC=mgbaXI)d^p hLOqV$!kSW)Ur>x{EkS4Cuz+;O004dXouttR001t?afko_ literal 0 HcmV?d00001 diff --git a/tests/data/simple_rle.rds b/tests/data/simple_rle.rds new file mode 100644 index 0000000000000000000000000000000000000000..b5fe2d7ebbd8a2adf38d14fe503aa16437cfb79e GIT binary patch literal 197 zcmV;$06PC4iwFP!0000018tB&4uUWcMW;{%OpGM1JcK(Jp1>8lP(gIp)CndQG*Ugj zaSAj7vB_^=erGbj&Bg%W0_U`Qgy1EI^@_9-eXu+$sW;v%%@=4aa^j)x6F z@vEFcy?XbaRHC9ZPSi2oox)-6hmbdsWBWbU4)@CxrM^GSgFZ%I9<6KlaRC4TwYF5r literal 0 HcmV?d00001 diff --git a/tests/data/simple_sce.rds b/tests/data/simple_sce.rds new file mode 100644 index 0000000000000000000000000000000000000000..e6a015f0d9d96076ec2380aba28f689e9de30d51 GIT binary patch literal 86567 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 0 - assert len(data["attributes"]) > 0 - assert len(data["attributes"]["names"]["data"]) == 1000 diff --git a/tests/test_atomic-bool.py b/tests/test_atomic-bool.py deleted file mode 100644 index e57fe27..0000000 --- a/tests/test_atomic-bool.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest - -from rds2py.PyRdsReader import PyRdsParser - -__author__ = "jkanche" -__copyright__ = "jkanche" -__license__ = "MIT" - - -def test_read_atomic_logical(): - parsed_obj = PyRdsParser("tests/data/atomic_logical.rds") - array = parsed_obj.parse() - - assert array is not None - assert array["data"].shape[0] > 0 - - -def test_read_atomic_logical_na(): - parsed_obj = PyRdsParser("tests/data/atomic_logical_wNA.rds") - array = parsed_obj.parse() - - assert array is not None - assert array["data"].shape[0] > 0 diff --git a/tests/test_atomic-double.py b/tests/test_atomic-double.py deleted file mode 100644 index f92f620..0000000 --- a/tests/test_atomic-double.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest - -from rds2py.PyRdsReader import PyRdsParser - -__author__ = "jkanche" -__copyright__ = "jkanche" -__license__ = "MIT" - - -def test_read_atomic_double(): - parsed_obj = PyRdsParser("tests/data/atomic_double.rds") - array = parsed_obj.parse() - - assert array is not None - print(array) - assert array["data"].shape[0] == 99 diff --git a/tests/test_atomic-int.py b/tests/test_atomic-int.py deleted file mode 100644 index c10c7a6..0000000 --- a/tests/test_atomic-int.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest - -from rds2py.PyRdsReader import PyRdsParser - -__author__ = "jkanche" -__copyright__ = "jkanche" -__license__ = "MIT" - - -def test_read_atomic_ints(): - parsed_obj = PyRdsParser("tests/data/atomic_ints.rds") - array = parsed_obj.parse() - - assert array is not None - print(array) - assert array["data"].shape[0] == 112 diff --git a/tests/test_atomic-str.py b/tests/test_atomic-str.py deleted file mode 100644 index 0b4d062..0000000 --- a/tests/test_atomic-str.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest - -from rds2py.PyRdsReader import PyRdsParser - -__author__ = "jkanche" -__copyright__ = "jkanche" -__license__ = "MIT" - - -def test_read_atomic_chars(): - parsed_obj = PyRdsParser("tests/data/atomic_chars.rds") - array = parsed_obj.parse() - - assert array is not None - assert len(array["data"]) == 26 - - -def test_read_atomic_chars_unicode(): - parsed_obj = PyRdsParser("tests/data/atomic_chars_unicode.rds") - array = parsed_obj.parse() - - assert array is not None - assert len(array["data"]) == 4 diff --git a/tests/test_atomics.py b/tests/test_atomics.py new file mode 100644 index 0000000..2c4dafa --- /dev/null +++ b/tests/test_atomics.py @@ -0,0 +1,103 @@ +import pytest + +from rds2py import read_rds + +from biocutils import BooleanList, FloatList, IntegerList, StringList + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + +## With attributes + + +def test_read_atomic_attrs(): + data = read_rds("tests/data/atomic_attr.rds") + + assert data is not None + assert isinstance(data, dict) + assert data["attributes"]["class"]["data"][0] == "frog" + + +## Booleans + + +def test_read_atomic_logical(): + arr = read_rds("tests/data/atomic_logical.rds") + + assert arr is not None + assert isinstance(arr, BooleanList) + assert len(arr) > 0 + + +def test_read_atomic_logical_na(): + arr = read_rds("tests/data/atomic_logical_wNA.rds") + + assert arr is not None + assert isinstance(arr, BooleanList) + assert len(arr) > 0 + + +## Doubles/Floats + + +def test_read_atomic_double(): + obj = read_rds("tests/data/atomic_double.rds") + + assert obj is not None + assert isinstance(obj, FloatList) + assert len(obj) == 99 + + +## Ints + + +def test_read_atomic_ints(): + arr = read_rds("tests/data/atomic_ints.rds") + + assert arr is not None + assert isinstance(arr, IntegerList) + assert len(arr) == 112 + assert arr.names is None + + +def test_read_atomic_ints_with_names(): + arr = read_rds("tests/data/atomic_ints_with_names.rds") + + assert arr is not None + assert isinstance(arr, IntegerList) + assert arr.names is not None + assert len(arr) == 112 + + +## Strings + + +def test_read_atomic_chars(): + arr = read_rds("tests/data/atomic_chars.rds") + + assert arr is not None + assert isinstance(arr, StringList) + assert len(arr) == 26 + assert arr.names is None + + +def test_read_atomic_chars_unicode(): + arr = read_rds("tests/data/atomic_chars_unicode.rds") + + assert arr is not None + assert isinstance(arr, StringList) + assert len(arr) == 4 + assert arr.names is None + + +## Test scalar values, defaults to a vector + + +def test_read_scalar_float(): + obj = read_rds("tests/data/scalar_int.rds") + + assert obj is not None + assert isinstance(obj, FloatList) + assert len(obj) == 1 + assert obj[0] == 10.0 diff --git a/tests/test_dict.py b/tests/test_dict.py new file mode 100644 index 0000000..52611f3 --- /dev/null +++ b/tests/test_dict.py @@ -0,0 +1,52 @@ +import pytest + +from rds2py import read_rds + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def test_read_simple_lists(): + obj = read_rds("tests/data/simple_list.rds") + + assert obj is not None + assert len(obj) > 0 + + assert "collab" in obj + assert len(obj["collab"]) > 0 + + +def test_read_atomic_lists(): + obj = read_rds("tests/data/lists.rds") + + assert obj is not None + assert len(obj) > 0 + + +def test_read_atomic_lists_nested(): + obj = read_rds("tests/data/lists_nested.rds") + + assert obj is not None + assert len(obj) > 0 + + +def test_read_atomic_lists_nested_deep(): + obj = read_rds("tests/data/lists_nested_deep.rds") + + assert obj is not None + assert len(obj) > 0 + + +def test_read_atomic_lists_df(): + obj = read_rds("tests/data/lists_df.rds") + + assert obj is not None + assert len(obj) > 0 + + +def test_read_atomic_lists_nested_deep_rownames(): + obj = read_rds("tests/data/lists_df_rownames.rds") + + assert obj is not None + assert len(obj) > 0 diff --git a/tests/test_factors.py b/tests/test_factors.py new file mode 100644 index 0000000..96471fc --- /dev/null +++ b/tests/test_factors.py @@ -0,0 +1,16 @@ +import pytest + +from rds2py import read_rds + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + +## With attributes + + +def test_read_simple_factors(): + data = read_rds("tests/data/simple_factors.rds") + + assert data is not None + assert len(data) == 4 diff --git a/tests/test_frames.py b/tests/test_frames.py new file mode 100644 index 0000000..449150f --- /dev/null +++ b/tests/test_frames.py @@ -0,0 +1,24 @@ +import pytest + +from rds2py import read_rds +from biocframe import BiocFrame + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def test_read_atomic_lists_df(): + frame = read_rds("tests/data/lists_df.rds") + + assert frame is not None + assert isinstance(frame, BiocFrame) + assert len(frame) > 0 + + +def test_read_atomic_lists_nested_deep_rownames(): + frame = read_rds("tests/data/lists_df_rownames.rds") + + assert frame is not None + assert isinstance(frame, BiocFrame) + assert len(frame) > 0 diff --git a/tests/test_granges.py b/tests/test_granges.py index f64cf8a..97ee606 100644 --- a/tests/test_granges.py +++ b/tests/test_granges.py @@ -1,9 +1,9 @@ import pytest -from rds2py.granges import as_granges, as_granges_list -from rds2py.parser import read_rds +from rds2py import read_rds from genomicranges import GenomicRanges, GenomicRangesList +import numpy as np __author__ = "jkanche" __copyright__ = "jkanche" @@ -11,17 +11,28 @@ def test_granges(): - robj = read_rds("tests/data/granges.rds") - - gr = as_granges(robj=robj) + gr = read_rds("tests/data/granges.rds") assert isinstance(gr, GenomicRanges) + assert gr.get_seqnames("list") == [ + "chr1", + "chr2", + "chr2", + "chr2", + "chr1", + "chr1", + "chr3", + "chr3", + "chr3", + "chr3", + ] + assert np.allclose(gr.get_start(), range(101, 111)) + assert len(gr.get_mcols().get_column_names()) == 2 + assert gr.get_strand("list") == ["-", "+", "+", "*", "*", "+", "+", "+", "-", "-"] def test_granges_list(): - robj = read_rds("tests/data/grangeslist.rds") - - gr = as_granges_list(robj=robj) + gr = read_rds("tests/data/grangeslist.rds") assert isinstance(gr, GenomicRangesList) assert len(gr) == 5 diff --git a/tests/test_interface_matrix.py b/tests/test_interface_matrix.py deleted file mode 100644 index e279419..0000000 --- a/tests/test_interface_matrix.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest - -from rds2py.interface import as_dense_matrix, as_sparse_matrix -from rds2py.parser import read_rds -import numpy as np -from scipy import sparse as sp - -__author__ = "jkanche" -__copyright__ = "jkanche" -__license__ = "MIT" - - -def test_read_s4_matrix_dgc(): - parsed_obj = read_rds("tests/data/s4_matrix.rds") - array = as_sparse_matrix(parsed_obj) - - assert array is not None - assert isinstance(array, sp.spmatrix) - - -def test_read_s4_matrix_dgt(): - parsed_obj = read_rds("tests/data/s4_matrix_dgt.rds") - array = as_sparse_matrix(parsed_obj) - - assert array is not None - assert isinstance(array, sp.spmatrix) - - -def test_read_dense_numpy_dtype(): - parsed_obj = read_rds("tests/data/numpy_dtype.rds") - array = as_dense_matrix(parsed_obj) - - assert array is not None - assert isinstance(array, np.ndarray) diff --git a/tests/test_list.py b/tests/test_list.py deleted file mode 100644 index fa5b012..0000000 --- a/tests/test_list.py +++ /dev/null @@ -1,47 +0,0 @@ -import pytest - -from rds2py.PyRdsReader import PyRdsParser - -__author__ = "jkanche" -__copyright__ = "jkanche" -__license__ = "MIT" - - -def test_read_atomic_lists(): - parsed_obj = PyRdsParser("tests/data/lists.rds") - array = parsed_obj.parse() - - assert array is not None - assert len(array) > 0 - - -def test_read_atomic_lists_nested(): - parsed_obj = PyRdsParser("tests/data/lists_nested.rds") - array = parsed_obj.parse() - - assert array is not None - assert len(array) > 0 - - -def test_read_atomic_lists_nested_deep(): - parsed_obj = PyRdsParser("tests/data/lists_nested_deep.rds") - array = parsed_obj.parse() - - assert array is not None - assert len(array) > 0 - - -def test_read_atomic_lists_df(): - parsed_obj = PyRdsParser("tests/data/lists_df.rds") - array = parsed_obj.parse() - - assert array is not None - assert len(array) > 0 - - -def test_read_atomic_lists_nested_deep_rownames(): - parsed_obj = PyRdsParser("tests/data/lists_df_rownames.rds") - array = parsed_obj.parse() - - assert array is not None - assert len(array) > 0 diff --git a/tests/test_mae.py b/tests/test_mae.py new file mode 100644 index 0000000..485c55a --- /dev/null +++ b/tests/test_mae.py @@ -0,0 +1,17 @@ +import pytest + +from rds2py import read_rds + +from multiassayexperiment import MultiAssayExperiment + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def test_read_sce(): + data = read_rds("tests/data/simple_mae.rds") + + assert data is not None + assert isinstance(data, MultiAssayExperiment) + assert len(data.get_experiment_names()) == 2 diff --git a/tests/test_matrices.py b/tests/test_matrices.py new file mode 100644 index 0000000..52d59b9 --- /dev/null +++ b/tests/test_matrices.py @@ -0,0 +1,35 @@ +import pytest + +from rds2py import read_rds +import numpy as np +from scipy import sparse as sp + +from rds2py.read_matrix import MatrixWrapper + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def test_read_s4_matrix_dgc(): + array = read_rds("tests/data/s4_matrix.rds") + + assert array is not None + assert isinstance(array, sp.spmatrix) + + +def test_read_s4_matrix_dgt(): + array = read_rds("tests/data/s4_matrix_dgt.rds") + + assert array is not None + assert isinstance(array, sp.spmatrix) + + +def test_read_dense_numpy_dtype(): + array = read_rds("tests/data/numpy_dtype.rds") + + assert array is not None + assert isinstance(array, MatrixWrapper) + assert isinstance(array.matrix, np.ndarray) + assert array.dimnames is not None + assert len(array.dimnames) == len(array.matrix.shape) diff --git a/tests/test_rle.py b/tests/test_rle.py new file mode 100644 index 0000000..71acc84 --- /dev/null +++ b/tests/test_rle.py @@ -0,0 +1,18 @@ +import pytest + +from rds2py import read_rds + +from biocutils import BooleanList, FloatList, IntegerList, StringList + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + +## With attributes + + +def test_read_simple_rle(): + data = read_rds("tests/data/simple_rle.rds") + + assert data is not None + assert len(data) == 36 diff --git a/tests/test_s4.py b/tests/test_s4.py index 1dd7bda..78fbfa2 100644 --- a/tests/test_s4.py +++ b/tests/test_s4.py @@ -1,10 +1,10 @@ -import pytest +# import pytest from rds2py.PyRdsReader import PyRdsParser -__author__ = "jkanche" -__copyright__ = "jkanche" -__license__ = "MIT" +# __author__ = "jkanche" +# __copyright__ = "jkanche" +# __license__ = "MIT" def test_read_s4_class(): diff --git a/tests/test_sce.py b/tests/test_sce.py new file mode 100644 index 0000000..6edaa0a --- /dev/null +++ b/tests/test_sce.py @@ -0,0 +1,17 @@ +import pytest + +from rds2py import read_rds + +from singlecellexperiment import SingleCellExperiment + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def test_read_sce(): + data = read_rds("tests/data/simple_sce.rds") + + assert data is not None + assert isinstance(data, SingleCellExperiment) + assert data.shape == (100, 100) diff --git a/tests/test_se.py b/tests/test_se.py new file mode 100644 index 0000000..8ceb6b5 --- /dev/null +++ b/tests/test_se.py @@ -0,0 +1,25 @@ +import pytest + +from rds2py import read_rds + +from summarizedexperiment import SummarizedExperiment, RangedSummarizedExperiment + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def test_read_summ_expt(): + data = read_rds("tests/data/sumexpt.rds") + + assert data is not None + assert isinstance(data, SummarizedExperiment) + assert data.shape == (200, 6) + + +def test_read_ranged_summ_expt(): + data = read_rds("tests/data/ranged_se.rds") + + assert data is not None + assert isinstance(data, RangedSummarizedExperiment) + assert data.shape == (200, 6)