Skip to content

Commit a7a69ad

Browse files
authored
Merge pull request #20 from molpopgen/metadata
Add metadata support.
2 parents d89dcd1 + 54ac431 commit a7a69ad

File tree

15 files changed

+493
-69
lines changed

15 files changed

+493
-69
lines changed

.github/workflows/tests.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ jobs:
3333
override: true
3434
- run: cargo check
3535
- name: run tests
36-
run: cargo test
36+
run: |
37+
cargo test
38+
cargo test --examples
3739
3840
build-osx:
3941
runs-on: macos-latest
@@ -53,7 +55,9 @@ jobs:
5355
override: true
5456
- run: cargo check
5557
- name: run tests
56-
run: cargo test
58+
run: |
59+
cargo test
60+
cargo test --examples
5761
5862
fmt:
5963
name: rust fmt

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ repository = "https://github.com/molpopgen/tskit_rust"
1313

1414
[dependencies]
1515
thiserror = "1.0"
16+
libc = "0.2.81"
17+
18+
[dev-dependencies]
19+
serde = {version = "1.0.118", features = ["derive"]}
20+
bincode = "1.3.1"
1621

1722
[build-dependencies]
1823
bindgen = "0.56.0"

examples/mutation.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#![macro_use]
2+
use serde::{Deserialize, Serialize};
3+
4+
#[derive(Serialize, Deserialize, Debug)]
5+
pub struct Mutation {
6+
pub effect_size: f64,
7+
pub dominance: f64,
8+
pub origin_time: i32,
9+
}
10+
11+
macro_rules! make_mutation_metadata_run {
12+
() => {
13+
pub fn run() {
14+
let mut tables = TableCollection::new(1000.).unwrap();
15+
// The simulation generates a mutation:
16+
let m = Mutation {
17+
effect_size: -0.235423,
18+
dominance: 0.5,
19+
origin_time: 1,
20+
};
21+
22+
// The mutation's data are included as metadata:
23+
tables
24+
.add_mutation_with_metadata(0, 0, 0, 0.0, None, Some(&m))
25+
.unwrap();
26+
27+
// Decoding requres 2 unwraps:
28+
// 1. The first is to handle errors.
29+
// 2. The second is b/c metadata are optional,
30+
// so a row may return None.
31+
let decoded = tables.mutations().metadata::<Mutation>(0).unwrap().unwrap();
32+
33+
// Check that we've made the round trip:
34+
assert_eq!(decoded.origin_time, 1);
35+
assert!((m.effect_size - decoded.effect_size).abs() < f64::EPSILON);
36+
assert!((m.dominance - decoded.dominance).abs() < f64::EPSILON);
37+
}
38+
};
39+
}
40+
41+
#[allow(dead_code)]
42+
fn main() {}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use tskit_rust::metadata;
2+
use tskit_rust::*;
3+
mod mutation;
4+
5+
use mutation::Mutation;
6+
7+
impl metadata::MetadataRoundtrip for Mutation {
8+
fn encode(&self) -> Result<Vec<u8>, metadata::MetadataError> {
9+
let mut rv = vec![];
10+
rv.extend(self.effect_size.to_le_bytes().iter().copied());
11+
rv.extend(self.dominance.to_le_bytes().iter().copied());
12+
rv.extend(self.origin_time.to_le_bytes().iter().copied());
13+
Ok(rv)
14+
}
15+
16+
fn decode(md: &[u8]) -> Result<Self, metadata::MetadataError> {
17+
use std::convert::TryInto;
18+
let (effect_size_bytes, rest) = md.split_at(std::mem::size_of::<f64>());
19+
let (dominance_bytes, rest) = rest.split_at(std::mem::size_of::<f64>());
20+
let (origin_time_bytes, _) = rest.split_at(std::mem::size_of::<i32>());
21+
Ok(Self {
22+
effect_size: f64::from_le_bytes(effect_size_bytes.try_into().unwrap()),
23+
dominance: f64::from_le_bytes(dominance_bytes.try_into().unwrap()),
24+
origin_time: i32::from_le_bytes(origin_time_bytes.try_into().unwrap()),
25+
})
26+
}
27+
}
28+
29+
make_mutation_metadata_run!();
30+
31+
#[test]
32+
fn run_bincode() {
33+
run();
34+
}
35+
36+
fn main() {
37+
run();
38+
}

examples/mutation_metadata_std.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use tskit_rust::metadata;
2+
use tskit_rust::*;
3+
mod mutation;
4+
5+
use mutation::Mutation;
6+
7+
// Implement the metadata trait for our mutation
8+
// type. Will will use the standard library for the implementation
9+
// details.
10+
impl metadata::MetadataRoundtrip for Mutation {
11+
fn encode(&self) -> Result<Vec<u8>, metadata::MetadataError> {
12+
Ok(bincode::serialize(&self).unwrap())
13+
}
14+
15+
fn decode(md: &[u8]) -> Result<Self, metadata::MetadataError> {
16+
Ok(bincode::deserialize(md).unwrap())
17+
}
18+
}
19+
20+
make_mutation_metadata_run!();
21+
22+
#[test]
23+
fn run_std() {
24+
run();
25+
}
26+
27+
fn main() {
28+
run();
29+
}

src/_macros.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,42 @@ macro_rules! build_tskit_type {
5858
};
5959
}
6060

61+
macro_rules! metadata_to_vector {
62+
($T: ty, $self: expr, $row: expr) => {
63+
crate::metadata::char_column_to_vector(
64+
$self.table_.metadata,
65+
$self.table_.metadata_offset,
66+
$row,
67+
$self.table_.num_rows,
68+
$self.table_.metadata_length,
69+
)?
70+
};
71+
}
72+
73+
macro_rules! decode_metadata_row {
74+
($T: ty, $buffer: expr) => {
75+
match $buffer {
76+
None => Ok(None),
77+
Some(v) => Ok(Some(<$T as crate::metadata::MetadataRoundtrip>::decode(
78+
&v,
79+
)?)),
80+
}
81+
};
82+
}
83+
84+
macro_rules! process_state_input {
85+
($state: expr) => {
86+
match $state {
87+
Some(x) => (
88+
x.as_ptr() as *const libc::c_char,
89+
x.len() as crate::bindings::tsk_size_t,
90+
$state,
91+
),
92+
None => (std::ptr::null(), 0, $state),
93+
}
94+
};
95+
}
96+
6197
#[cfg(test)]
6298
mod test {
6399
use crate::error::TskitRustError;

src/edge_table.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::bindings as ll_bindings;
2+
use crate::metadata;
23
use crate::{tsk_id_t, tsk_size_t, TskitRustError};
34

45
/// An immutable view of an edge table.
@@ -59,4 +60,12 @@ impl<'a> EdgeTable<'a> {
5960
pub fn right(&'a self, row: tsk_id_t) -> Result<f64, TskitRustError> {
6061
unsafe_tsk_column_access!(row, 0, self.num_rows(), self.table_.right);
6162
}
63+
64+
pub fn metadata<T: metadata::MetadataRoundtrip>(
65+
&'a self,
66+
row: tsk_id_t,
67+
) -> Result<Option<T>, TskitRustError> {
68+
let buffer = metadata_to_vector!(T, self, row);
69+
decode_metadata_row!(T, buffer)
70+
}
6271
}

src/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ pub enum TskitRustError {
1616
/// Wrapper around tskit C API error codes.
1717
#[error("{}", get_tskit_error_message(*code))]
1818
ErrorCode { code: i32 },
19+
/// A redirection of [``crate::metadata::MetadataError``]
20+
#[error("{value:?}")]
21+
MetadataError {
22+
/// The redirected error
23+
#[from]
24+
value: crate::metadata::MetadataError,
25+
},
1926
}
2027

2128
/// Takes the return code from a tskit

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod _macros; // Starts w/_ to be sorted at front by rustfmt!
1010
mod edge_table;
1111
pub mod error;
1212
pub mod ffi;
13+
pub mod metadata;
1314
mod mutation_table;
1415
mod node_table;
1516
mod population_table;

src/metadata.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
use crate::bindings::{tsk_id_t, tsk_size_t};
2+
use thiserror::Error;
3+
4+
pub trait MetadataRoundtrip {
5+
fn encode(&self) -> Result<Vec<u8>, MetadataError>;
6+
fn decode(md: &[u8]) -> Result<Self, MetadataError>
7+
where
8+
Self: Sized;
9+
}
10+
11+
pub(crate) struct EncodedMetadata {
12+
encoded: Option<Vec<u8>>,
13+
}
14+
15+
impl EncodedMetadata {
16+
pub(crate) fn new(md: Option<&dyn MetadataRoundtrip>) -> Result<Self, MetadataError> {
17+
match md {
18+
Some(x) => {
19+
let e = x.encode()?;
20+
Ok(Self { encoded: Some(e) })
21+
}
22+
None => Ok(Self { encoded: None }),
23+
}
24+
}
25+
26+
pub(crate) fn as_ptr(&self) -> *const libc::c_char {
27+
match &self.encoded {
28+
Some(x) => x.as_ptr() as *const libc::c_char,
29+
None => std::ptr::null(),
30+
}
31+
}
32+
33+
pub(crate) fn len(&self) -> tsk_size_t {
34+
match &self.encoded {
35+
Some(x) => x.len() as tsk_size_t,
36+
None => 0,
37+
}
38+
}
39+
}
40+
41+
#[derive(Error, Debug, PartialEq)]
42+
pub enum MetadataError {
43+
/// Error related to types implementing
44+
/// [``MetadataRoundtrip``]
45+
#[error("{}", *msg)]
46+
RoundtripError { msg: String },
47+
}
48+
49+
pub(crate) fn char_column_to_vector(
50+
column: *const libc::c_char,
51+
column_offset: *const tsk_size_t,
52+
row: tsk_id_t,
53+
num_rows: tsk_size_t,
54+
column_length: tsk_size_t,
55+
) -> Result<Option<Vec<u8>>, crate::TskitRustError> {
56+
if row < 0 || (row as tsk_size_t) >= num_rows {
57+
return Err(crate::TskitRustError::IndexError {});
58+
}
59+
if column_length == 0 {
60+
return Ok(None);
61+
}
62+
let start = unsafe { *column_offset.offset(row as isize) };
63+
let stop = if (row as tsk_size_t) < num_rows {
64+
unsafe { *column_offset.offset((row + 1) as isize) }
65+
} else {
66+
column_length
67+
};
68+
if start >= stop {
69+
return Ok(None);
70+
}
71+
if column_length == 0 {
72+
return Ok(None);
73+
}
74+
let mut buffer = vec![];
75+
for i in start..stop {
76+
buffer.push(unsafe { *column.offset(i as isize) } as u8);
77+
}
78+
Ok(Some(buffer))
79+
}
80+
81+
#[cfg(test)]
82+
mod tests {
83+
use super::*;
84+
85+
#[test]
86+
fn test_vec8_cast_to_c_string() {
87+
let v: Vec<u8> = vec![0, 1, b'\0', 2, 3];
88+
let c = v.as_ptr() as *const libc::c_char;
89+
for (i, vi) in v.iter().enumerate() {
90+
assert_eq!(*vi as i8, unsafe { *c.add(i) });
91+
}
92+
93+
let _ = match Some(&v) {
94+
Some(x) => x.as_ptr() as *const libc::c_char,
95+
None => std::ptr::null(),
96+
};
97+
}
98+
99+
struct F {
100+
x: i32,
101+
y: u32,
102+
}
103+
104+
impl MetadataRoundtrip for F {
105+
fn encode(&self) -> Result<Vec<u8>, MetadataError> {
106+
let mut rv = vec![];
107+
rv.extend(self.x.to_le_bytes().iter().copied());
108+
rv.extend(self.y.to_le_bytes().iter().copied());
109+
Ok(rv)
110+
}
111+
fn decode(md: &[u8]) -> Result<Self, MetadataError> {
112+
use std::convert::TryInto;
113+
let (x_int_bytes, rest) = md.split_at(std::mem::size_of::<i32>());
114+
let (y_int_bytes, _) = rest.split_at(std::mem::size_of::<u32>());
115+
Ok(Self {
116+
x: i32::from_le_bytes(x_int_bytes.try_into().unwrap()),
117+
y: u32::from_le_bytes(y_int_bytes.try_into().unwrap()),
118+
})
119+
}
120+
}
121+
122+
#[test]
123+
fn test_metadata_round_trip() {
124+
let f = F { x: -3, y: 42 };
125+
let v = f.encode().unwrap();
126+
let c = v.as_ptr() as *const libc::c_char;
127+
let mut d = vec![];
128+
for i in 0..v.len() {
129+
d.push(unsafe { *c.add(i as usize) as u8 });
130+
}
131+
let df = F::decode(&d).unwrap();
132+
assert_eq!(f.x, df.x);
133+
assert_eq!(f.y, df.y);
134+
}
135+
136+
#[test]
137+
fn test_encoded_metadata_roundtrip() {
138+
let f = F { x: -3, y: 42 };
139+
let enc = EncodedMetadata::new(Some(&f)).unwrap();
140+
let p = enc.as_ptr();
141+
let mut d = vec![];
142+
for i in 0..enc.len() {
143+
d.push(unsafe { *p.add(i as usize) as u8 });
144+
}
145+
let df = F::decode(&d).unwrap();
146+
assert_eq!(f.x, df.x);
147+
assert_eq!(f.y, df.y);
148+
}
149+
}

0 commit comments

Comments
 (0)