use polars_core::prelude::{Column, DataFrame, DataType, NamedFrom, PlSmallStr, Series};
pub use crate::bioframe_ext::BioNearestDirection as GenomicNearestDirection;
use crate::bioframe_ext::StrandBehavior as Strandedness;
use crate::dataframe_ext::{nearest_fast, overlap_fast, overlap_pairs_fast, DataFrameRanges};
use crate::range_frame::take_rows_owned;
use crate::factorize::borrowed_string_values;
use crate::interval_frame::{
PYRANGES_CHROMOSOME_COL, PYRANGES_END_COL, PYRANGES_START_COL, PYRANGES_STRAND_COL,
};
use crate::range_ops::{NearestDirection, NearestOptions};
use crate::{
ClipRangesOptions, ComplementRangesOptions, CountOverlapsOptions, ExtendRangesOptions,
GroupCumsumOptions, IntersectOverlapsOptions, JoinOverlapsOptions, MaxDisjointOptions,
OuterRangesOptions, OverlapMode, OverlapOptions, OverlapPairs, RangeFrameError, Result,
SetIntersectOverlapsOptions, SetUnionOverlapsOptions, SortRangesOptions, SplitOverlapsOptions,
TileRangesOptions,
};
const POSITIVE_STRAND: &str = "+";
const NEGATIVE_STRAND: &str = "-";
const GENOMIC_ROW_ID_PREFIX: &str = "__polaranges_genomic_row_id";
#[derive(Clone, Debug, PartialEq)]
pub struct GenomicOverlapOptions {
pub multiple: OverlapMode,
pub slack: i64,
pub contained_intervals_only: bool,
pub preserve_input_order: bool,
pub invert: bool,
pub strand_behavior: Strandedness,
pub chromosome_col: PlSmallStr,
pub strand_col: PlSmallStr,
pub start_col: PlSmallStr,
pub end_col: PlSmallStr,
pub match_by: Vec<PlSmallStr>,
}
impl Default for GenomicOverlapOptions {
fn default() -> Self {
Self {
multiple: OverlapMode::All,
slack: 0,
contained_intervals_only: false,
preserve_input_order: true,
invert: false,
strand_behavior: Strandedness::Auto,
chromosome_col: PYRANGES_CHROMOSOME_COL.into(),
strand_col: PYRANGES_STRAND_COL.into(),
start_col: PYRANGES_START_COL.into(),
end_col: PYRANGES_END_COL.into(),
match_by: Vec::new(),
}
}
}
impl GenomicOverlapOptions {
pub fn with_match_by<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<PlSmallStr>,
{
self.match_by = columns.into_iter().map(Into::into).collect();
self
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct GenomicNearestOptions {
pub suffix: String,
pub exclude_overlaps: bool,
pub k: usize,
pub distance_column: Option<String>,
pub direction: GenomicNearestDirection,
pub preserve_input_order: bool,
pub strand_behavior: Strandedness,
pub chromosome_col: PlSmallStr,
pub strand_col: PlSmallStr,
pub start_col: PlSmallStr,
pub end_col: PlSmallStr,
pub match_by: Vec<PlSmallStr>,
}
impl Default for GenomicNearestOptions {
fn default() -> Self {
Self {
suffix: "_b".to_owned(),
exclude_overlaps: false,
k: 1,
distance_column: Some("Distance".to_owned()),
direction: GenomicNearestDirection::Any,
preserve_input_order: true,
strand_behavior: Strandedness::Auto,
chromosome_col: PYRANGES_CHROMOSOME_COL.into(),
strand_col: PYRANGES_STRAND_COL.into(),
start_col: PYRANGES_START_COL.into(),
end_col: PYRANGES_END_COL.into(),
match_by: Vec::new(),
}
}
}
impl GenomicNearestOptions {
pub fn with_match_by<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<PlSmallStr>,
{
self.match_by = columns.into_iter().map(Into::into).collect();
self
}
}
pub trait DataFrameIntervalAccessors {
fn r(&self) -> DataFrameRangeAccessor<'_>;
fn b(&self) -> DataFrameBioAccessor<'_>;
}
impl DataFrameIntervalAccessors for DataFrame {
fn r(&self) -> DataFrameRangeAccessor<'_> {
DataFrameRangeAccessor { frame: self }
}
fn b(&self) -> DataFrameBioAccessor<'_> {
DataFrameBioAccessor { frame: self }
}
}
pub struct DataFrameRangeAccessor<'a> {
frame: &'a DataFrame,
}
impl DataFrameRangeAccessor<'_> {
pub fn overlap(&self, other: &DataFrame, options: OverlapOptions) -> Result<DataFrame> {
overlap_fast(self.frame, other, &options)
}
pub fn overlap_pairs(
&self,
other: &DataFrame,
options: OverlapOptions,
) -> Result<OverlapPairs> {
overlap_pairs_fast(self.frame, other, &options)
}
pub fn nearest(&self, other: &DataFrame, options: NearestOptions) -> Result<DataFrame> {
nearest_fast(self.frame, other, &options)
}
pub fn merge_overlaps(&self, options: crate::MergeOptions) -> Result<DataFrame> {
self.frame.merge_overlaps(options)
}
pub fn cluster_overlaps(&self, options: crate::ClusterOptions) -> Result<DataFrame> {
self.frame.cluster_overlaps(options)
}
pub fn count_overlaps(
&self,
other: &DataFrame,
options: CountOverlapsOptions,
) -> Result<Series> {
self.frame.count_overlaps(other, options)
}
pub fn join_overlaps(
&self,
other: &DataFrame,
options: JoinOverlapsOptions,
) -> Result<DataFrame> {
self.frame.join_overlaps(other, options)
}
pub fn intersect_overlaps(
&self,
other: &DataFrame,
options: IntersectOverlapsOptions,
) -> Result<DataFrame> {
self.frame.intersect_overlaps(other, options)
}
pub fn set_intersect_overlaps(
&self,
other: &DataFrame,
options: SetIntersectOverlapsOptions,
) -> Result<DataFrame> {
self.frame.set_intersect_overlaps(other, options)
}
pub fn set_union_overlaps(
&self,
other: &DataFrame,
options: SetUnionOverlapsOptions,
) -> Result<DataFrame> {
self.frame.set_union_overlaps(other, options)
}
pub fn sort_ranges(&self, options: SortRangesOptions) -> Result<DataFrame> {
self.frame.sort_ranges(options)
}
pub fn extend_ranges(&self, options: ExtendRangesOptions) -> Result<DataFrame> {
self.frame.extend_ranges(options)
}
pub fn tile_ranges(&self, options: TileRangesOptions) -> Result<DataFrame> {
self.frame.tile_ranges(options)
}
pub fn clip_ranges(&self, options: ClipRangesOptions) -> Result<DataFrame> {
self.frame.clip_ranges(options)
}
pub fn group_cumsum(&self, options: GroupCumsumOptions) -> Result<DataFrame> {
self.frame.group_cumsum(options)
}
pub fn max_disjoint_overlaps(&self, options: MaxDisjointOptions) -> Result<DataFrame> {
self.frame.max_disjoint_overlaps(options)
}
pub fn split_overlaps(&self, options: SplitOverlapsOptions) -> Result<DataFrame> {
self.frame.split_overlaps(options)
}
pub fn outer_ranges(&self, options: OuterRangesOptions) -> Result<DataFrame> {
self.frame.outer_ranges(options)
}
pub fn complement_ranges(&self, options: ComplementRangesOptions) -> Result<DataFrame> {
self.frame.complement_ranges(options)
}
pub fn subtract_overlaps(
&self,
other: &DataFrame,
options: crate::SubtractOptions,
) -> Result<DataFrame> {
self.frame.subtract_overlaps(other, options)
}
}
pub struct DataFrameBioAccessor<'a> {
frame: &'a DataFrame,
}
impl DataFrameBioAccessor<'_> {
pub fn overlap(
&self,
other: &DataFrame,
options: GenomicOverlapOptions,
) -> Result<DataFrame> {
let resolved = resolve_strandedness_eager(
self.frame,
other,
options.strand_behavior,
options.strand_col.as_str(),
)?;
let match_by = genomic_match_by(
&options.match_by,
&options.chromosome_col,
resolved,
&options.strand_col,
);
let prepared_other =
prepare_genomic_other_eager(other, resolved, options.strand_col.as_str())?;
let overlap_options = to_overlap_options(
&options,
match_by.iter().map(|s| s.as_str().to_owned()).collect(),
);
if options.invert {
let pairs = overlap_pairs_fast(self.frame, &prepared_other, &overlap_options)?;
return take_non_overlapping_rows(self.frame, pairs.left);
}
overlap_fast(self.frame, &prepared_other, &overlap_options)
}
pub fn nearest(
&self,
other: &DataFrame,
options: GenomicNearestOptions,
) -> Result<DataFrame> {
let resolved = resolve_strandedness_eager(
self.frame,
other,
options.strand_behavior,
options.strand_col.as_str(),
)?;
if options.direction != GenomicNearestDirection::Any && resolved == Strandedness::Ignore {
return Err(RangeFrameError::DirectionalNearestRequiresStrand);
}
let match_by = genomic_match_by(
&options.match_by,
&options.chromosome_col,
resolved,
&options.strand_col,
);
let prepared_other =
prepare_genomic_other_eager(other, resolved, options.strand_col.as_str())?;
let match_by_strings: Vec<String> =
match_by.iter().map(|s| s.as_str().to_owned()).collect();
if options.direction == GenomicNearestDirection::Any {
return nearest_fast(
self.frame,
&prepared_other,
&to_nearest_options(&options, NearestDirection::Any, match_by_strings),
);
}
let row_id_name = unique_temp_name_from_df(self.frame, GENOMIC_ROW_ID_PREFIX);
let indexed_self = self.frame.with_row_index(row_id_name.clone(), None)?;
let forward =
filter_rows_by_strand(&indexed_self, options.strand_col.as_str(), POSITIVE_STRAND)?;
let reverse =
filter_rows_by_strand(&indexed_self, options.strand_col.as_str(), NEGATIVE_STRAND)?;
let forward_direction = match options.direction {
GenomicNearestDirection::Downstream => NearestDirection::Forward,
GenomicNearestDirection::Upstream => NearestDirection::Backward,
GenomicNearestDirection::Any => unreachable!("handled above"),
};
let reverse_direction = match options.direction {
GenomicNearestDirection::Downstream => NearestDirection::Backward,
GenomicNearestDirection::Upstream => NearestDirection::Forward,
GenomicNearestDirection::Any => unreachable!("handled above"),
};
let mut frames = Vec::new();
frames.push(nearest_fast(
&forward,
&prepared_other,
&to_nearest_options(&options, forward_direction, match_by_strings.clone()),
)?);
frames.push(nearest_fast(
&reverse,
&prepared_other,
&to_nearest_options(&options, reverse_direction, match_by_strings),
)?);
let mut combined = concat_dataframes(frames)?;
combined = combined.sort([row_id_name.as_str()], Default::default())?;
let _ = combined.drop_in_place(row_id_name.as_str())?;
Ok(combined)
}
pub fn merge_overlaps(&self, options: crate::BioMergeOptions) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_merge_overlaps(options)
}
pub fn cluster_overlaps(&self, options: crate::BioClusterOptions) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_cluster_overlaps(options)
}
pub fn count_overlaps(
&self,
other: &DataFrame,
options: crate::BioCountOverlapsOptions,
) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_count_overlaps(other, options)
}
pub fn join_overlaps(
&self,
other: &DataFrame,
options: crate::BioJoinOverlapsOptions,
) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_join_overlaps(other, options)
}
pub fn intersect_overlaps(
&self,
other: &DataFrame,
options: crate::BioIntersectOverlapsOptions,
) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_intersect_overlaps(other, options)
}
pub fn set_intersect_overlaps(
&self,
other: &DataFrame,
options: crate::BioSetIntersectOverlapsOptions,
) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_set_intersect_overlaps(other, options)
}
pub fn set_union_overlaps(
&self,
other: &DataFrame,
options: crate::BioSetUnionOverlapsOptions,
) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_set_union_overlaps(other, options)
}
pub fn sort_ranges(&self, options: crate::BioSortOptions) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_sort_ranges(options)
}
pub fn extend_ranges(&self, options: crate::BioExtendOptions) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_extend_ranges(options)
}
pub fn tile_ranges(&self, options: crate::BioTileOptions) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_tile_ranges(options)
}
pub fn clip_ranges(&self, options: crate::BioClipOptions) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_clip_ranges(options)
}
pub fn group_cumsum(&self, options: crate::BioGroupCumsumOptions) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_group_cumsum(options)
}
pub fn max_disjoint_overlaps(
&self,
options: crate::BioMaxDisjointOptions,
) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_max_disjoint_overlaps(options)
}
pub fn split_overlaps(&self, options: crate::BioSplitOptions) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_split_overlaps(options)
}
pub fn outer_ranges(&self, options: crate::BioOuterRangesOptions) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_outer_ranges(options)
}
pub fn complement_ranges(
&self,
options: crate::BioComplementRangesOptions,
) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_complement_ranges(options)
}
pub fn subtract_overlaps(
&self,
other: &DataFrame,
options: crate::BioSubtractOptions,
) -> Result<DataFrame> {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_subtract_overlaps(other, options)
}
pub fn has_valid_strand(&self, strand_column: Option<&str>) -> bool {
use crate::bioframe_ext::BioDataFrameRanges;
self.frame.bio_has_valid_strand(strand_column)
}
}
fn genomic_match_by(
extra: &[PlSmallStr],
chromosome_col: &PlSmallStr,
strand_behavior: Strandedness,
strand_col: &PlSmallStr,
) -> Vec<PlSmallStr> {
let mut out = vec![chromosome_col.clone()];
if strand_behavior != Strandedness::Ignore {
out.push(strand_col.clone());
}
out.extend(extra.iter().cloned());
dedupe_smallstr(out)
}
fn dedupe_smallstr(values: Vec<PlSmallStr>) -> Vec<PlSmallStr> {
let mut out: Vec<PlSmallStr> = Vec::with_capacity(values.len());
for value in values {
if !out
.iter()
.any(|existing| existing.as_str() == value.as_str())
{
out.push(value);
}
}
out
}
fn resolve_strandedness_eager(
left: &DataFrame,
right: &DataFrame,
strand_behavior: Strandedness,
strand_col: &str,
) -> Result<Strandedness> {
let left_valid = eager_has_valid_strand(left, strand_col)?;
let right_valid = eager_has_valid_strand(right, strand_col)?;
resolve_strandedness_impl(left_valid, right_valid, strand_behavior)
}
fn resolve_strandedness_impl(
left_valid: bool,
right_valid: bool,
strand_behavior: Strandedness,
) -> Result<Strandedness> {
match strand_behavior {
Strandedness::Auto => Ok(if left_valid && right_valid {
Strandedness::Same
} else {
Strandedness::Ignore
}),
Strandedness::Same | Strandedness::Opposite => {
if left_valid && right_valid {
Ok(strand_behavior)
} else {
Err(RangeFrameError::InvalidStrandBehavior {
behavior: strand_behavior_label(strand_behavior).to_owned(),
})
}
}
Strandedness::Ignore => Ok(Strandedness::Ignore),
}
}
fn eager_has_valid_strand(df: &DataFrame, strand_col: &str) -> Result<bool> {
let column = match df.column(strand_col) {
Ok(column) => column,
Err(_) => return Ok(false),
};
if column.null_count() > 0 {
return Ok(false);
}
if !is_string_like_dtype(column.dtype()) {
return Ok(false);
}
let mut values = borrowed_string_values(column.as_materialized_series())?;
Ok(values.all(|value| matches!(value, POSITIVE_STRAND | NEGATIVE_STRAND)))
}
fn is_string_like_dtype(dtype: &DataType) -> bool {
matches!(
dtype,
DataType::String | DataType::Categorical(..) | DataType::Enum(..)
)
}
fn prepare_genomic_other_eager(
other: &DataFrame,
strand_behavior: Strandedness,
strand_col: &str,
) -> Result<DataFrame> {
if strand_behavior == Strandedness::Opposite {
flip_strand_eager(other, strand_col)
} else {
Ok(other.clone())
}
}
fn flip_strand_eager(df: &DataFrame, strand_col: &str) -> Result<DataFrame> {
let column = df
.column(strand_col)
.map_err(|_| RangeFrameError::MissingStrandColumn {
column: strand_col.to_owned(),
})?;
if column.null_count() > 0 {
return Err(RangeFrameError::NullValues {
column: strand_col.to_owned(),
});
}
let swapped = borrowed_string_values(column.as_materialized_series())?
.map(|value| match value {
POSITIVE_STRAND => NEGATIVE_STRAND,
NEGATIVE_STRAND => POSITIVE_STRAND,
other => other,
})
.collect::<Vec<_>>();
let mut out = df.clone();
out.with_column(Column::from(Series::new(strand_col.into(), swapped)))?;
Ok(out)
}
fn strand_behavior_label(strand_behavior: Strandedness) -> &'static str {
match strand_behavior {
Strandedness::Auto => "auto",
Strandedness::Same => "same",
Strandedness::Opposite => "opposite",
Strandedness::Ignore => "ignore",
}
}
fn to_nearest_options(
options: &GenomicNearestOptions,
direction: NearestDirection,
match_by: Vec<String>,
) -> NearestOptions {
NearestOptions {
match_by,
suffix: options.suffix.clone(),
exclude_overlaps: options.exclude_overlaps,
k: options.k,
distance_column: options.distance_column.clone(),
direction,
preserve_input_order: options.preserve_input_order,
left_start_col: options.start_col.as_str().to_owned(),
left_end_col: options.end_col.as_str().to_owned(),
right_start_col: options.start_col.as_str().to_owned(),
right_end_col: options.end_col.as_str().to_owned(),
}
}
fn to_overlap_options(options: &GenomicOverlapOptions, match_by: Vec<String>) -> OverlapOptions {
OverlapOptions {
multiple: options.multiple,
slack: options.slack,
contained_intervals_only: options.contained_intervals_only,
match_by,
preserve_input_order: options.preserve_input_order,
left_start_col: options.start_col.as_str().to_owned(),
left_end_col: options.end_col.as_str().to_owned(),
right_start_col: options.start_col.as_str().to_owned(),
right_end_col: options.end_col.as_str().to_owned(),
}
}
fn take_non_overlapping_rows(df: &DataFrame, overlapping_rows: Vec<u32>) -> Result<DataFrame> {
let mut overlapping = vec![false; df.height()];
for row_id in overlapping_rows {
if let Some(slot) = overlapping.get_mut(row_id as usize) {
*slot = true;
}
}
let remaining = overlapping
.into_iter()
.enumerate()
.filter_map(|(row_idx, is_overlapping)| (!is_overlapping).then_some(row_idx as u32))
.collect::<Vec<_>>();
take_rows_owned(df, remaining)
}
fn filter_rows_by_strand(df: &DataFrame, strand_col: &str, strand: &str) -> Result<DataFrame> {
let column = df
.column(strand_col)
.map_err(|_| RangeFrameError::MissingStrandColumn {
column: strand_col.to_owned(),
})?;
if column.null_count() > 0 {
return Err(RangeFrameError::NullValues {
column: strand_col.to_owned(),
});
}
let indices = borrowed_string_values(column.as_materialized_series())?
.enumerate()
.filter_map(|(idx, value)| (value == strand).then_some(idx as u32))
.collect::<Vec<_>>();
take_rows_owned(df, indices)
}
fn concat_dataframes(mut frames: Vec<DataFrame>) -> Result<DataFrame> {
let mut iter = frames.drain(..);
let mut combined = iter
.next()
.expect("concat_dataframes requires at least one frame");
for frame in iter {
combined.vstack_mut(&frame)?;
}
Ok(combined)
}
fn unique_temp_name_from_df(df: &DataFrame, prefix: &str) -> PlSmallStr {
if df.column(prefix).is_err() {
return prefix.into();
}
let mut suffix = 1_usize;
loop {
let candidate = format!("{prefix}_{suffix}");
if df.column(&candidate).is_err() {
return candidate.into();
}
suffix = suffix.saturating_add(1);
}
}
#[cfg(test)]
mod tests {
use polars_core::prelude::{Column, DataFrame, NamedFrom, Series};
use super::{DataFrameIntervalAccessors, GenomicOverlapOptions, Strandedness};
use crate::factorize::factorize_pair_by;
use crate::OverlapOptions;
fn make_df(columns: Vec<Series>) -> DataFrame {
let columns = columns.into_iter().map(Column::from).collect::<Vec<_>>();
DataFrame::new_infer_height(columns).unwrap()
}
#[test]
fn default_pyranges_columns_work_for_range_accessor() {
let left = make_df(vec![
Series::new("Chromosome".into(), &["chr1", "chr1", "chr2"]),
Series::new("Start".into(), &[1_i64, 10, 20]),
Series::new("End".into(), &[5_i64, 20, 30]),
]);
let right = make_df(vec![
Series::new("Chromosome".into(), &["chr1", "chr2"]),
Series::new("Start".into(), &[3_i64, 25]),
Series::new("End".into(), &[4_i64, 27]),
]);
let result = left
.r()
.overlap(
&right,
OverlapOptions::default().with_match_by(["Chromosome"]),
)
.unwrap();
assert_eq!(result.height(), 2);
}
#[test]
fn genomic_overlap_same_strand_matches_generic_range_grouping() {
let left = make_df(vec![
Series::new("Chromosome".into(), &["chr1", "chr1"]),
Series::new("Strand".into(), &["+", "-"]),
Series::new("Start".into(), &[1_i64, 1]),
Series::new("End".into(), &[5_i64, 5]),
]);
let right = make_df(vec![
Series::new("Chromosome".into(), &["chr1", "chr1"]),
Series::new("Strand".into(), &["+", "-"]),
Series::new("Start".into(), &[2_i64, 2]),
Series::new("End".into(), &[4_i64, 4]),
]);
let generic = left
.r()
.overlap(
&right,
OverlapOptions::default().with_match_by(["Chromosome", "Strand"]),
)
.unwrap();
let genomic = left
.b()
.overlap(
&right,
GenomicOverlapOptions {
strand_behavior: Strandedness::Same,
..GenomicOverlapOptions::default()
},
)
.unwrap();
assert_eq!(generic, genomic);
}
#[test]
fn genomic_overlap_ignore_matches_generic_chromosome_grouping() {
let left = make_df(vec![
Series::new("Chromosome".into(), &["chr1", "chr1"]),
Series::new("Strand".into(), &["+", "-"]),
Series::new("Start".into(), &[1_i64, 8]),
Series::new("End".into(), &[5_i64, 9]),
]);
let right = make_df(vec![
Series::new("Chromosome".into(), &["chr1"]),
Series::new("Strand".into(), &["-"]),
Series::new("Start".into(), &[2_i64]),
Series::new("End".into(), &[4_i64]),
]);
let generic = left
.r()
.overlap(
&right,
OverlapOptions::default().with_match_by(["Chromosome"]),
)
.unwrap();
let genomic = left
.b()
.overlap(
&right,
GenomicOverlapOptions {
strand_behavior: Strandedness::Ignore,
..GenomicOverlapOptions::default()
},
)
.unwrap();
assert_eq!(generic, genomic);
}
#[test]
fn generic_range_accessor_supports_non_genomic_columns() {
let left = make_df(vec![
Series::new("Bucket".into(), &[1_i32, 1, 2]),
Series::new("Lower".into(), &[1_i64, 10, 20]),
Series::new("Upper".into(), &[5_i64, 20, 30]),
]);
let right = make_df(vec![
Series::new("Bucket".into(), &[1_i32, 2]),
Series::new("Lower".into(), &[3_i64, 25]),
Series::new("Upper".into(), &[4_i64, 27]),
]);
let options = OverlapOptions::default()
.with_interval_columns("Lower", "Upper")
.with_match_by(["Bucket"]);
let result = left.r().overlap(&right, options).unwrap();
assert_eq!(result.height(), 2);
}
#[test]
fn factorization_helper_stays_generic_for_non_genomic_keys() {
let left = make_df(vec![Series::new("Bucket".into(), &[1_i32, 2])]);
let right = make_df(vec![Series::new("Bucket".into(), &[2_i32, 1])]);
let by = ["Bucket".to_owned()];
let (left_ids, right_ids) = factorize_pair_by(&left, &right, &by, &by).unwrap();
assert_eq!(left_ids[0], right_ids[1]);
assert_eq!(left_ids[1], right_ids[0]);
}
}