-
Notifications
You must be signed in to change notification settings - Fork 388
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Work-in-progress initial scaffolding
- Loading branch information
Showing
8 changed files
with
316 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
from __future__ import annotations | ||
from typing import Optional, Type | ||
|
||
from rerun import bindings | ||
from rerun._baseclasses import ComponentMixin | ||
|
||
|
||
class Schema: | ||
"""The schema representing all columns in a [`Dataset`][].""" | ||
|
||
def __init__(self, storage: bindings.PySchema) -> None: | ||
self.storage = storage | ||
|
||
def control_columns(self) -> list[bindings.PyControlColumn]: | ||
return self.storage.control_columns() | ||
|
||
def time_columns(self) -> list[bindings.PyTimeColumn]: | ||
return self.storage.time_columns() | ||
|
||
def component_columns(self) -> list[bindings.PyComponentColumn]: | ||
return self.storage.component_columns() | ||
|
||
def column_for(self, entity_path: str, component: str | Type[ComponentMixin]) -> Optional[bindings.PyColumn]: | ||
if not isinstance(component, str): | ||
component = component._BATCH_TYPE._ARROW_TYPE._TYPE_NAME | ||
|
||
for col in self.component_columns(): | ||
if col.matches(entity_path, component): | ||
return col | ||
|
||
|
||
class Dataset: | ||
"""A single dataset from an RRD, representing a Recording or a Blueprint.""" | ||
|
||
def __init__(self, storage: bindings.PyChunkStore) -> None: | ||
self.storage = storage | ||
|
||
def schema(self) -> bindings.PySchema: | ||
"""The schema of the dataset.""" | ||
return Schema(self.storage.schema()) | ||
|
||
def range_query(self, entity_path_expr: str, pov: bindings.PyControlColumn) -> list[pa.RecordBatch]: | ||
"""Execute a range query on the dataset.""" | ||
return self.storage.range_query(entity_path_expr, pov) | ||
|
||
|
||
class Archive: | ||
"""An archive containing all the data stores in an RRD file.""" | ||
|
||
def __init__(self, storage: bindings.PyRRDArchive) -> None: | ||
self.storage = storage | ||
|
||
def num_recordings(self) -> int: | ||
"""The number of recordings in the archive.""" | ||
return self.storage.num_recordings() | ||
|
||
def all_recordings(self) -> list[Dataset]: | ||
"""The recordings in the archive.""" | ||
return [Dataset(r) for r in self.storage.all_recordings()] | ||
|
||
|
||
def load_recording(filename: str) -> Dataset: | ||
""" | ||
Load a rerun data file from disk. | ||
:param filename: The path to the file to load. | ||
:return: A dictionary of stores in the file. | ||
""" | ||
archive = load_archive(filename) | ||
|
||
if archive.num_recordings() != 1: | ||
raise ValueError(f"Expected exactly one recording in the archive, got {archive.num_recordings()}") | ||
|
||
recordings = archive.all_recordings() | ||
|
||
return Dataset(recordings[0]) | ||
|
||
|
||
def load_archive(filename: str) -> Archive: | ||
""" | ||
Load a rerun archive file from disk. | ||
:param filename: The path to the file to load. | ||
:return: A dictionary of stores in the file. | ||
""" | ||
stores = bindings.load_rrd(filename) | ||
|
||
return Archive(stores) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
#![allow(clippy::needless_pass_by_value)] // A lot of arguments to #[pyfunction] need to be by value | ||
#![allow(clippy::borrow_deref_ref)] // False positive due to #[pyfunction] macro | ||
#![allow(unsafe_op_in_unsafe_fn)] // False positive due to #[pyfunction] macro | ||
|
||
use std::collections::BTreeMap; | ||
|
||
use arrow::{array::RecordBatch, pyarrow::PyArrowType}; | ||
use pyo3::{exceptions::PyRuntimeError, prelude::*}; | ||
use re_chunk_store::{ | ||
ChunkStore, ChunkStoreConfig, ColumnDescriptor, ComponentColumnDescriptor, | ||
ControlColumnDescriptor, RangeQueryExpression, TimeColumnDescriptor, VersionPolicy, | ||
}; | ||
use re_dataframe::QueryEngine; | ||
use re_log_types::ResolvedTimeRange; | ||
use re_sdk::{StoreId, StoreKind, Timeline}; | ||
|
||
#[pyclass(frozen)] | ||
#[derive(Clone)] | ||
pub struct PyControlColumn { | ||
pub column: ControlColumnDescriptor, | ||
} | ||
|
||
#[pymethods] | ||
impl PyControlColumn { | ||
fn __repr__(&self) -> String { | ||
format!("Ctrl({})", self.column.component_name.short_name()) | ||
} | ||
} | ||
|
||
#[pyclass(frozen)] | ||
#[derive(Clone)] | ||
pub struct PyTimeColumn { | ||
pub column: TimeColumnDescriptor, | ||
} | ||
|
||
#[pymethods] | ||
impl PyTimeColumn { | ||
fn __repr__(&self) -> String { | ||
format!("Time({})", self.column.timeline.name()) | ||
} | ||
} | ||
|
||
#[pyclass(frozen)] | ||
#[derive(Clone)] | ||
pub struct PyComponentColumn { | ||
pub column: ComponentColumnDescriptor, | ||
} | ||
|
||
#[pymethods] | ||
impl PyComponentColumn { | ||
fn __repr__(&self) -> String { | ||
format!( | ||
"Component({}:{})", | ||
self.column.entity_path, | ||
self.column.component_name.short_name() | ||
) | ||
} | ||
|
||
fn matches(&self, entity_path: &str, component_name: &str) -> bool { | ||
self.column.entity_path == entity_path.into() | ||
&& self.column.component_name == component_name | ||
} | ||
} | ||
|
||
#[pyclass(frozen)] | ||
#[derive(Clone)] | ||
pub struct PySchema { | ||
// TODO(jleibs): This gets replaced with the new schema object | ||
pub schema: Vec<ColumnDescriptor>, | ||
} | ||
|
||
#[pymethods] | ||
impl PySchema { | ||
fn control_columns(&self) -> Vec<PyControlColumn> { | ||
self.schema | ||
.iter() | ||
.filter_map(|column| { | ||
if let ColumnDescriptor::Control(col) = column { | ||
Some(PyControlColumn { | ||
column: col.clone(), | ||
}) | ||
} else { | ||
None | ||
} | ||
}) | ||
.collect() | ||
} | ||
|
||
fn time_columns(&self) -> Vec<PyTimeColumn> { | ||
self.schema | ||
.iter() | ||
.filter_map(|column| { | ||
if let ColumnDescriptor::Time(col) = column { | ||
Some(PyTimeColumn { | ||
column: col.clone(), | ||
}) | ||
} else { | ||
None | ||
} | ||
}) | ||
.collect() | ||
} | ||
|
||
fn component_columns(&self) -> Vec<PyComponentColumn> { | ||
self.schema | ||
.iter() | ||
.filter_map(|column| { | ||
if let ColumnDescriptor::Component(col) = column { | ||
Some(PyComponentColumn { | ||
column: col.clone(), | ||
}) | ||
} else { | ||
None | ||
} | ||
}) | ||
.collect() | ||
} | ||
} | ||
|
||
#[pyclass(frozen)] | ||
#[derive(Clone)] | ||
pub struct PyDataset { | ||
pub store: ChunkStore, | ||
} | ||
|
||
#[pymethods] | ||
impl PyDataset { | ||
fn schema(&self) -> PySchema { | ||
PySchema { | ||
schema: self.store.schema(), | ||
} | ||
} | ||
|
||
fn range_query( | ||
&self, | ||
expr: &str, | ||
pov: PyComponentColumn, | ||
) -> PyResult<PyArrowType<Vec<RecordBatch>>> { | ||
// TODO(jleibs): Move this ctx into PyChunkStore? | ||
let cache = re_dataframe::external::re_query::Caches::new(&self.store); | ||
let engine = QueryEngine { | ||
store: &self.store, | ||
cache: &cache, | ||
}; | ||
|
||
// TODO(jleibs): Move to arguments | ||
let timeline = Timeline::log_tick(); | ||
let time_range = ResolvedTimeRange::EVERYTHING; | ||
|
||
let query = RangeQueryExpression { | ||
entity_path_expr: expr.into(), | ||
timeline, | ||
time_range, | ||
pov: pov.column, | ||
}; | ||
|
||
let query_handle = engine.range(&query, None /* columns */); | ||
|
||
let batches: Result<Vec<_>, _> = query_handle | ||
.into_iter() | ||
.map(|batch| batch.try_as_arrow_record_batch()) | ||
.collect(); | ||
|
||
let batches = batches.map_err(|err| PyRuntimeError::new_err(err.to_string()))?; | ||
|
||
Ok(PyArrowType(batches)) | ||
} | ||
} | ||
|
||
#[pyclass(frozen)] | ||
#[derive(Clone)] | ||
pub struct PyRRDArchive { | ||
pub datasets: BTreeMap<StoreId, ChunkStore>, | ||
} | ||
|
||
#[pymethods] | ||
impl PyRRDArchive { | ||
fn num_recordings(&self) -> usize { | ||
self.datasets | ||
.iter() | ||
.filter(|(id, _)| matches!(id.kind, StoreKind::Recording)) | ||
.count() | ||
} | ||
|
||
// TODO(jleibs): This could probably return an iterator | ||
fn all_recordings(&self) -> Vec<PyDataset> { | ||
self.datasets | ||
.iter() | ||
.filter(|(id, _)| matches!(id.kind, StoreKind::Recording)) | ||
.map(|(_, store)| PyDataset { | ||
store: store.clone(), | ||
}) | ||
.collect() | ||
} | ||
} | ||
|
||
#[pyfunction] | ||
pub fn load_rrd(path_to_rrd: String) -> PyResult<PyRRDArchive> { | ||
let stores = | ||
ChunkStore::from_rrd_filepath(&ChunkStoreConfig::DEFAULT, path_to_rrd, VersionPolicy::Warn) | ||
.map_err(|err| PyRuntimeError::new_err(err.to_string()))?; | ||
|
||
let archive = PyRRDArchive { datasets: stores }; | ||
|
||
Ok(archive) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters