Skip to content

Commit 5814410

Browse files
committed
fixed missing tool implementations
1 parent f50a12f commit 5814410

23 files changed

Lines changed: 2080 additions & 268 deletions

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ list_comprehension_macro = "0.1"
2828
mysql = "25.0.1"
2929
num = "0.4"
3030
plotters = "0.3.7"
31+
quick-xml = { version = "0.38", features = ["serialize"] }
3132
rand = "0.8.5"
3233
reqwest = { version = "0.12.8", features = ["blocking"] }
3334
serde = { version = "1.0", features = ["derive"] }

README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ The package contains the following command line tools:
2525
| bam-to-fastq | reconstruct FASTQ records from a BAM file |
2626
| bam-to-bigwig | convert bam to bigWig (estimate fragment length if required) |
2727
| bam-view | print contents of a bam file |
28+
| bed-remove-overlaps | remove BED or table rows that overlap inadmissible regions |
29+
| bigwig-counts-to-quantiles | convert bigWig counts to empirical quantiles |
2830
| bigwig-edit-chrom-names | rewrite a bigWig with chromosome names transformed by a regex |
2931
| bigwig-extract | extract bigWig data for BED regions as a table or bigWig |
3032
| bigwig-extract-chroms | write a new bigWig containing only selected chromosomes |
@@ -49,6 +51,7 @@ The package contains the following command line tools:
4951
| pwm-scan-regions | score genomic regions with one or more PWMs |
5052
| pwm-scan-sequences | scan FASTA sequences with a PWM and export a bigWig track |
5153
| segmentation-differential| merge and score differential chromatin states across segmentations |
54+
| sequence-similarity | compute sliding-window k-mer similarity to a reference sequence |
5255

5356

5457
## Examples
@@ -240,6 +243,9 @@ The result is:
240243
# Apply a custom shared-library function across tracks
241244
bigwig-map mapper.so result.bw track1.bw track2.bw
242245

246+
# For Rust plugins, export `rustynetics_bigwig_map` and select the Rust ABI
247+
bigwig-map --abi rust mapper.so result.bw track1.bw track2.bw
248+
243249
# Extract selected regions from a bigWig file as a table
244250
bigwig-extract signal.bw regions.bed signal.table
245251

@@ -260,6 +266,62 @@ bigwig-positive result.table track1.bw:2.0 track2.bw:2.0
260266
bigwig-edit-chrom-names --dry-run signal.bw '^chr' ''
261267
```
262268
269+
`bigwig-map` loads a shared library and calls a mapping function for each bin.
270+
The mapper receives the sequence name, the genomic position, and the values from
271+
all input tracks for the current bin, and must return the output value for that bin.
272+
273+
For Rust, the recommended interface is the Rust ABI plugin mode:
274+
275+
- build the mapper as a `cdylib`
276+
- export the symbol `rustynetics_bigwig_map`
277+
- accept a pointer to `rustynetics::bigwig_map_plugin::BigWigMapInput`
278+
279+
`bigwig-map` defaults to `--abi auto`, which first looks for the Rust symbol
280+
`rustynetics_bigwig_map` and then falls back to the legacy shared-library ABI
281+
(`F` or `bigwig_map`). You can force one mode explicitly with `--abi rust` or
282+
`--abi legacy`. `--symbol` overrides the default symbol name.
283+
284+
A minimal Rust mapper looks like this:
285+
286+
```rust
287+
use rustynetics::bigwig_map_plugin::BigWigMapInput;
288+
289+
#[no_mangle]
290+
pub unsafe extern "C" fn rustynetics_bigwig_map(input: *const BigWigMapInput) -> f64 {
291+
let input = &*input;
292+
let values = input.values();
293+
294+
if values.is_empty() {
295+
return f64::NAN;
296+
}
297+
298+
values.iter().copied().sum::<f64>() / values.len() as f64
299+
}
300+
```
301+
302+
The `BigWigMapInput` helper provides:
303+
304+
- `input.seqname()` for the chromosome name
305+
- `input.position` for the current genomic position
306+
- `input.values()` for the per-track input values at that position
307+
308+
The plugin crate needs to be compiled as a shared library:
309+
310+
```toml
311+
[lib]
312+
crate-type = ["cdylib"]
313+
```
314+
315+
Then build and run it like this:
316+
317+
```bash
318+
cargo build --release
319+
bigwig-map --abi rust target/release/libmy_mapper.so result.bw track1.bw track2.bw
320+
```
321+
322+
On macOS the shared library is usually `libmy_mapper.dylib`, and on Windows
323+
`my_mapper.dll`.
324+
263325
### Motif XML extraction
264326
265327
```bash

src/bbi.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,10 @@ impl Default for BData {
11401140
/* -------------------------------------------------------------------------- */
11411141

11421142
impl BData {
1143+
pub fn key_offsets(&self) -> &[i64] {
1144+
&self.ptr_keys
1145+
}
1146+
11431147
pub fn add(&mut self, key: Vec<u8>, value: Vec<u8>) -> Result<(), String> {
11441148
if key.len() as u32 != self.key_size {
11451149
return Err("BData.Add(): key has invalid length".to_string());

src/bigwig_map_plugin.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use std::ffi::c_char;
2+
use std::slice;
3+
use std::str::{self, Utf8Error};
4+
5+
/// Default symbol name for Rust-oriented `bigwig-map` plugins.
6+
pub const BIGWIG_MAP_RUST_SYMBOL: &str = "rustynetics_bigwig_map";
7+
8+
/// Compatibility symbol used by older shared-library mappers.
9+
pub const BIGWIG_MAP_LEGACY_SYMBOL: &str = "bigwig_map";
10+
11+
/// Legacy `bigwig-map` ABI:
12+
/// `(seqname_ptr, position, values_ptr, values_len) -> f64`.
13+
pub type BigWigMapLegacyFn = unsafe extern "C" fn(*const c_char, usize, *const f64, usize) -> f64;
14+
15+
/// Rust-oriented `bigwig-map` ABI:
16+
/// `(&BigWigMapInput) -> f64`.
17+
pub type BigWigMapRustFn = unsafe extern "C" fn(*const BigWigMapInput) -> f64;
18+
19+
/// FFI payload passed to Rust-oriented `bigwig-map` plugins.
20+
///
21+
/// A plugin can depend on `rustynetics` and export:
22+
///
23+
/// ```ignore
24+
/// use rustynetics::bigwig_map_plugin::{BigWigMapInput, BIGWIG_MAP_RUST_SYMBOL};
25+
///
26+
/// #[no_mangle]
27+
/// pub unsafe extern "C" fn rustynetics_bigwig_map(input: *const BigWigMapInput) -> f64 {
28+
/// let input = &*input;
29+
/// let values = input.values();
30+
/// values.iter().copied().sum()
31+
/// }
32+
/// ```
33+
#[repr(C)]
34+
#[derive(Clone, Copy, Debug)]
35+
pub struct BigWigMapInput {
36+
pub seqname_ptr: *const c_char,
37+
pub seqname_len: usize,
38+
pub position: usize,
39+
pub values_ptr: *const f64,
40+
pub values_len: usize,
41+
}
42+
43+
impl BigWigMapInput {
44+
/// Returns the chromosome name as raw bytes.
45+
///
46+
/// # Safety
47+
/// `seqname_ptr` must point to a valid buffer of length `seqname_len`.
48+
pub unsafe fn seqname_bytes(&self) -> &[u8] {
49+
if self.seqname_len == 0 {
50+
&[]
51+
} else {
52+
slice::from_raw_parts(self.seqname_ptr as *const u8, self.seqname_len)
53+
}
54+
}
55+
56+
/// Returns the chromosome name as UTF-8 text.
57+
///
58+
/// # Safety
59+
/// `seqname_ptr` must point to a valid UTF-8 buffer of length `seqname_len`.
60+
pub unsafe fn seqname(&self) -> Result<&str, Utf8Error> {
61+
str::from_utf8(self.seqname_bytes())
62+
}
63+
64+
/// Returns the input values for the current genomic position.
65+
///
66+
/// # Safety
67+
/// `values_ptr` must point to a valid buffer of length `values_len`.
68+
pub unsafe fn values(&self) -> &[f64] {
69+
if self.values_len == 0 {
70+
&[]
71+
} else {
72+
slice::from_raw_parts(self.values_ptr, self.values_len)
73+
}
74+
}
75+
}
76+
77+
#[cfg(test)]
78+
mod tests {
79+
use super::*;
80+
use std::ffi::CString;
81+
82+
#[test]
83+
fn input_exposes_seqname_and_values() {
84+
let seqname = CString::new("chr1").unwrap();
85+
let values = [1.0, 2.5, 3.0];
86+
let input = BigWigMapInput {
87+
seqname_ptr: seqname.as_ptr(),
88+
seqname_len: 4,
89+
position: 10,
90+
values_ptr: values.as_ptr(),
91+
values_len: values.len(),
92+
};
93+
94+
let parsed_seqname = unsafe { input.seqname().unwrap() };
95+
let parsed_values = unsafe { input.values() };
96+
97+
assert_eq!(parsed_seqname, "chr1");
98+
assert_eq!(parsed_values, &values);
99+
}
100+
}

src/bin/bam-to-bigwig.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,10 @@ fn main() {
314314
.long("filter-chromosomes")
315315
.num_args(1)
316316
.help("Remove all reads on the given chromosomes [comma separated list]"))
317+
.arg(Arg::new("remove-filtered-chromosomes")
318+
.long("remove-filtered-chromosomes")
319+
.action(ArgAction::SetTrue)
320+
.help("Remove all chromosomes that have been filtered out"))
317321
// track options
318322
.arg(Arg::new("binning-method")
319323
.long("binning-method")
@@ -331,7 +335,7 @@ fn main() {
331335
.arg(Arg::new("pseudocounts")
332336
.long("pseudocounts")
333337
.num_args(1)
334-
.help("Pseudocounts added to treatment and control signal [default: `1.0,1.0']"))
338+
.help("Pseudocounts added to treatment and control signal [default: `0.0,0.0']"))
335339
.arg(Arg::new("smoothen-control")
336340
.long("smoothen-control")
337341
.action(ArgAction::SetTrue)
@@ -591,6 +595,9 @@ fn main() {
591595
opt_filter_chroms.split(',').map(String::from).collect(),
592596
));
593597
}
598+
options_list.push(OptionCoverage::RemoveFilteredChroms(
599+
matches.get_flag("remove-filtered-chromosomes"),
600+
));
594601

595602
if let Some(opt_initial_value) = matches.get_one::<String>("initial-value") {
596603
let value: f64 = opt_initial_value.parse().unwrap_or_else(|_| {

src/bin/bed-remove-overlaps.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use std::process;
2+
3+
use clap::{Arg, ArgAction, Command};
4+
5+
use rustynetics::granges::GRanges;
6+
use rustynetics::granges_table::OptionPrintStrand;
7+
8+
mod common;
9+
10+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11+
enum InputFormat {
12+
Bed3,
13+
Bed6,
14+
Bed9,
15+
Table,
16+
}
17+
18+
fn read_table_auto(path: Option<&str>) -> Result<GRanges, Box<dyn std::error::Error>> {
19+
let mut reader = common::open_reader(path)?;
20+
let mut granges = GRanges::default();
21+
granges.read_table_all(&mut reader)?;
22+
Ok(granges)
23+
}
24+
25+
fn import_input(
26+
path: Option<&str>,
27+
format: InputFormat,
28+
) -> Result<GRanges, Box<dyn std::error::Error>> {
29+
match format {
30+
InputFormat::Bed3 => {
31+
let mut reader = common::open_reader(path)?;
32+
let mut granges = GRanges::default();
33+
granges.read_bed3(&mut reader)?;
34+
Ok(granges)
35+
}
36+
InputFormat::Bed6 => {
37+
let mut reader = common::open_reader(path)?;
38+
let mut granges = GRanges::default();
39+
granges.read_bed6(&mut reader)?;
40+
Ok(granges)
41+
}
42+
InputFormat::Bed9 => {
43+
let mut reader = common::open_reader(path)?;
44+
let mut granges = GRanges::default();
45+
granges.read_bed9(&mut reader)?;
46+
Ok(granges)
47+
}
48+
InputFormat::Table => read_table_auto(path),
49+
}
50+
}
51+
52+
fn export_output(
53+
granges: &GRanges,
54+
path: Option<&str>,
55+
format: InputFormat,
56+
) -> Result<(), Box<dyn std::error::Error>> {
57+
let mut writer = common::open_writer(path)?;
58+
match format {
59+
InputFormat::Bed3 => granges.write_bed3(&mut writer)?,
60+
InputFormat::Bed6 => granges.write_bed6(&mut writer)?,
61+
InputFormat::Bed9 => granges.write_bed9(&mut writer)?,
62+
InputFormat::Table => granges.write_table(&mut writer, &[&OptionPrintStrand(true)])?,
63+
}
64+
Ok(())
65+
}
66+
67+
fn main() {
68+
let matches = Command::new("bed-remove-overlaps")
69+
.about("Remove regions that overlap a set of inadmissible intervals")
70+
.arg(
71+
Arg::new("input-format")
72+
.long("input-format")
73+
.default_value("bed3")
74+
.value_parser(["bed3", "bed6", "bed9", "table"]),
75+
)
76+
.arg(
77+
Arg::new("verbose")
78+
.short('v')
79+
.long("verbose")
80+
.action(ArgAction::Count),
81+
)
82+
.arg(Arg::new("inadmissible").required(true).index(1))
83+
.arg(Arg::new("input").index(2))
84+
.arg(Arg::new("output").index(3))
85+
.get_matches();
86+
87+
let format = match matches.get_one::<String>("input-format").unwrap().as_str() {
88+
"bed3" => InputFormat::Bed3,
89+
"bed6" => InputFormat::Bed6,
90+
"bed9" => InputFormat::Bed9,
91+
"table" => InputFormat::Table,
92+
_ => unreachable!(),
93+
};
94+
let inadmissible = matches.get_one::<String>("inadmissible").unwrap();
95+
let input = matches.get_one::<String>("input").map(String::as_str);
96+
let output = matches.get_one::<String>("output").map(String::as_str);
97+
let verbose = matches.get_count("verbose") > 0;
98+
99+
if verbose {
100+
eprintln!("Reading inadmissible regions `{inadmissible}`...");
101+
}
102+
let remove = import_input(Some(inadmissible), InputFormat::Bed3).unwrap_or_else(|error| {
103+
eprintln!("reading inadmissible regions failed: {error}");
104+
process::exit(1);
105+
});
106+
if verbose {
107+
if let Some(path) = input {
108+
eprintln!("Reading input `{path}`...");
109+
} else {
110+
eprintln!("Reading input from stdin...");
111+
}
112+
}
113+
let input_ranges = import_input(input, format).unwrap_or_else(|error| {
114+
eprintln!("reading input failed: {error}");
115+
process::exit(1);
116+
});
117+
let before = input_ranges.num_rows();
118+
let filtered = input_ranges.remove_overlaps_with(&remove);
119+
if verbose {
120+
eprintln!("Removed {} regions", before - filtered.num_rows());
121+
}
122+
if let Err(error) = export_output(&filtered, output, format) {
123+
eprintln!("writing output failed: {error}");
124+
process::exit(1);
125+
}
126+
}

0 commit comments

Comments
 (0)