Push zpwxxpnpktps #67

Merged
coissac merged 46 commits from push-zpwxxpnpktps into main 2026-08-17 09:41:42 +00:00
9 changed files with 269 additions and 253 deletions
Showing only changes of commit cc67023e2c - Show all commits
+1
View File
@@ -1773,6 +1773,7 @@ dependencies = [
"obiskbuilder", "obiskbuilder",
"obiskio", "obiskio",
"obisys", "obisys",
"obitaxonomy",
"rayon", "rayon",
"serde", "serde",
"serde_json", "serde_json",
+1
View File
@@ -6,6 +6,7 @@ edition = "2024"
[dependencies] [dependencies]
obikseq = { path = "../obikseq" } obikseq = { path = "../obikseq" }
obikpartitionner = { path = "../obikpartitionner" } obikpartitionner = { path = "../obikpartitionner" }
obitaxonomy = { path = "../obitaxonomy" }
obiskio = { path = "../obiskio" } obiskio = { path = "../obiskio" }
obisys = { path = "../obisys" } obisys = { path = "../obisys" }
obicompactvec = { path = "../obicompactvec" } obicompactvec = { path = "../obicompactvec" }
+2
View File
@@ -1,5 +1,6 @@
pub mod error; pub mod error;
pub mod meta; pub mod meta;
pub mod predicate;
pub mod state; pub mod state;
mod cardcomp; mod cardcomp;
mod distance; mod distance;
@@ -18,6 +19,7 @@ pub use distance::{DistanceMetric, DistanceOutput};
pub use index::KmerIndex; pub use index::KmerIndex;
pub use merge::MergeMode; pub use merge::MergeMode;
pub use meta::{validate_label, GenomeInfo, IndexConfig, IndexMeta, META_FILENAME}; pub use meta::{validate_label, GenomeInfo, IndexConfig, IndexMeta, META_FILENAME};
pub use predicate::{GroupFilterParams, MetaPred};
pub use state::{IndexState, SENTINEL_COUNTED, SENTINEL_INDEXED, SENTINEL_SCATTERED}; pub use state::{IndexState, SENTINEL_COUNTED, SENTINEL_INDEXED, SENTINEL_SCATTERED};
pub use stats::IndexBitsPerKmer; pub use stats::IndexBitsPerKmer;
pub use siblings::{BasePairTally, CardinalityTally, RawSnpDistanceOutput, SiblingAnnexStats, SnpAlignment}; pub use siblings::{BasePairTally, CardinalityTally, RawSnpDistanceOutput, SiblingAnnexStats, SnpAlignment};
+252
View File
@@ -0,0 +1,252 @@
use std::collections::HashMap;
use obikpartitionner::GroupQuorumFilter;
use obitaxonomy::{TaxPath, TaxPattern};
use crate::meta::{GenomeInfo, IndexMeta};
// ── Operator ──────────────────────────────────────────────────────────────────
enum PredOp { Wildcard, Eq, Ne, Matches, NotMatches }
// ── MetaPred ──────────────────────────────────────────────────────────────────
/// A single predicate on genome metadata: `key OP val1|val2|…`
///
/// Operators: `=` (exact), `!=` (not equal), `~` (path ancestor), `!~` (not ancestor).
/// Multiple values separated by `|` are OR'd.
pub struct MetaPred {
key: String,
op: PredOp,
values: Vec<String>,
}
impl MetaPred {
/// Parse a predicate string of the form `key=v1|v2`, `key!=v`, `key~path`, `key!~path`.
/// The special values `*` and `all` (case-insensitive) match every genome.
pub fn parse(s: &str) -> Result<Self, String> {
let t = s.trim();
if t == "*" || t.eq_ignore_ascii_case("all") {
return Ok(Self { key: String::new(), op: PredOp::Wildcard, values: vec![] });
}
let (op, key, rhs) =
if let Some(pos) = s.find("!=") {
(PredOp::Ne, &s[..pos], &s[pos+2..])
} else if let Some(pos) = s.find("!~") {
(PredOp::NotMatches, &s[..pos], &s[pos+2..])
} else if let Some(pos) = s.find('=') {
(PredOp::Eq, &s[..pos], &s[pos+1..])
} else if let Some(pos) = s.find('~') {
(PredOp::Matches, &s[..pos], &s[pos+1..])
} else {
return Err(format!("no operator found in predicate: {s}"));
};
let key = key.trim().to_string();
if key.is_empty() { return Err(format!("empty key in predicate: {s}")); }
let values: Vec<String> = rhs.split('|').map(|v| v.trim().to_string()).collect();
if values.iter().any(|v| v.is_empty()) {
return Err(format!("empty value in predicate: {s}"));
}
Ok(Self { key, op, values })
}
/// Evaluate against one genome's metadata.
/// Returns `None` when the key is absent (NA propagation).
pub(crate) fn eval(&self, meta: &HashMap<String, String>) -> Option<bool> {
if matches!(self.op, PredOp::Wildcard) { return Some(true); }
let value = meta.get(&self.key)?;
Some(match self.op {
PredOp::Wildcard => unreachable!(),
PredOp::Eq => self.values.iter().any(|v| v == value),
PredOp::Ne => self.values.iter().all(|v| v != value),
PredOp::Matches => self.values.iter().any(|v| path_matches(value, v)),
PredOp::NotMatches => self.values.iter().all(|v| !path_matches(value, v)),
})
}
}
impl GenomeInfo {
/// Evaluate a single metadata predicate against this genome.
/// Returns `None` when the predicate's key is absent (NA propagation).
pub fn matches(&self, pred: &MetaPred) -> Option<bool> {
pred.eval(&self.meta)
}
}
// ── Path matching ─────────────────────────────────────────────────────────────
/// True if the stored taxonomy `value` matches `pattern`.
///
/// `value` must be a valid `TaxPath` (starts with `taxonomy:/`).
/// `pattern` is a `TaxPattern` query (see `obitaxonomy::TaxPattern` for syntax).
/// Returns `false` if either fails to parse.
fn path_matches(value: &str, pattern: &str) -> bool {
let Ok(path) = TaxPath::parse(value) else { return false };
let Ok(pat) = TaxPattern::parse(pattern) else { return false };
pat.matches(&path)
}
// ── Three-value group evaluation ──────────────────────────────────────────────
/// AND of all predicates (ingroup semantics).
/// Short-circuits on `Some(false)`; propagates `None` if no predicate returns `false`.
fn eval_and(preds: &[MetaPred], meta: &HashMap<String, String>) -> Option<bool> {
let mut has_na = false;
for pred in preds {
match pred.eval(meta) {
Some(false) => return Some(false),
Some(true) => {}
None => has_na = true,
}
}
if has_na { None } else { Some(true) }
}
/// OR of all predicates (outgroup semantics).
/// Short-circuits on `Some(true)`; propagates `None` if no predicate returns `true`.
fn eval_or(preds: &[MetaPred], meta: &HashMap<String, String>) -> Option<bool> {
let mut has_na = false;
for pred in preds {
match pred.eval(meta) {
Some(true) => return Some(true),
Some(false) => {}
None => has_na = true,
}
}
if has_na { None } else { Some(false) }
}
// ── Genome classification ─────────────────────────────────────────────────────
enum Membership { Ingroup, Outgroup, Uncategorized }
fn classify(
genomes: &[GenomeInfo],
ingroup: &[MetaPred],
outgroup: &[MetaPred],
) -> Vec<Membership> {
genomes.iter().map(|g| {
let in_r = if ingroup.is_empty() { None } else { eval_and(ingroup, &g.meta) };
let out_r = if outgroup.is_empty() { None } else { eval_or(outgroup, &g.meta) };
// Ingroup wins over outgroup.
if in_r == Some(true) { return Membership::Ingroup; }
if out_r == Some(true) { return Membership::Outgroup; }
Membership::Uncategorized
}).collect()
}
// ── Group quorum filter construction ──────────────────────────────────────────
pub struct GroupFilterParams {
pub threshold: u32,
pub min_count: Option<isize>,
pub max_count: Option<isize>,
pub min_frac: Option<f64>,
pub max_frac: Option<f64>,
pub min_outgroup_count: Option<isize>,
pub max_outgroup_count: Option<isize>,
pub min_outgroup_frac: Option<f64>,
pub max_outgroup_frac: Option<f64>,
}
impl IndexMeta {
/// Returns indices of genomes matching `pred_str` (single predicate).
pub fn matching_genome_indices(&self, pred_str: &str) -> Result<Vec<usize>, String> {
let pred = MetaPred::parse(pred_str)?;
Ok(self.genomes.iter().enumerate()
.filter_map(|(i, g)| {
if g.matches(&pred) == Some(true) { Some(i) } else { std::option::Option::None }
})
.collect())
}
/// Build a `GroupQuorumFilter` from parsed predicates, evaluated against `self.genomes`.
///
/// - No groups defined: `ingroup_idx` = all genomes (implicit ingroup).
/// - `ingroup` predicates only: outgroup indices are empty.
/// - `outgroup` predicates only: ingroup indices are empty.
/// - Both defined: ingroup wins on overlap; uncategorized genomes are ignored.
pub fn build_group_filter(
&self,
ingroup_preds: &[MetaPred],
outgroup_preds: &[MetaPred],
p: GroupFilterParams,
) -> Result<GroupQuorumFilter, String> {
let (ingroup_idx, outgroup_idx) = if ingroup_preds.is_empty() && outgroup_preds.is_empty() {
((0..self.genomes.len()).collect(), vec![])
} else {
let members = classify(&self.genomes, ingroup_preds, outgroup_preds);
let in_idx: Vec<usize> = members.iter().enumerate()
.filter(|(_, m)| matches!(m, Membership::Ingroup))
.map(|(i, _)| i).collect();
let out_idx: Vec<usize> = members.iter().enumerate()
.filter(|(_, m)| matches!(m, Membership::Outgroup))
.map(|(i, _)| i).collect();
(in_idx, out_idx)
};
let in_size = ingroup_idx.len();
let out_size = outgroup_idx.len();
let ingroup_quorum_explicit = p.min_count.is_some() || p.max_count.is_some()
|| p.min_frac.is_some() || p.max_frac.is_some();
let outgroup_quorum_explicit = p.min_outgroup_count.is_some() || p.max_outgroup_count.is_some()
|| p.min_outgroup_frac.is_some() || p.max_outgroup_frac.is_some();
let default_min_frac = if !ingroup_preds.is_empty() && !ingroup_quorum_explicit { 1.0 } else { 0.0 };
let default_max_outgroup_count = if !outgroup_preds.is_empty() && !outgroup_quorum_explicit { 0 } else { out_size };
// Resolve a signed count: negative means an offset from the group size
// (e.g. -1 = all but one), floored at 1 so the negative form always keeps
// constraining the group — even a singleton group, where n-1 would be 0
// and would otherwise drop the constraint entirely.
let resolve = |v: isize, size: usize| -> usize {
if v < 0 { (size as isize + v).max(1) as usize } else { v as usize }
};
let min_count = p.min_count.map(|v| resolve(v, in_size)).unwrap_or(0);
let max_count = p.max_count.map(|v| resolve(v, in_size)).unwrap_or(in_size);
let min_frac = p.min_frac.unwrap_or(default_min_frac);
let max_frac = p.max_frac.unwrap_or(1.0);
let min_outgroup_count = p.min_outgroup_count.map(|v| resolve(v, out_size)).unwrap_or(0);
let max_outgroup_count = p.max_outgroup_count.map(|v| resolve(v, out_size)).unwrap_or(default_max_outgroup_count);
let min_outgroup_frac = p.min_outgroup_frac.unwrap_or(0.0);
let max_outgroup_frac = p.max_outgroup_frac.unwrap_or(1.0);
for (v, lo, hi) in [
("--min-frac/--max-frac", min_frac, max_frac),
("--min-outgroup-frac/--max-outgroup-frac", min_outgroup_frac, max_outgroup_frac),
] {
if !(0.0..=1.0).contains(&lo) || !(0.0..=1.0).contains(&hi) {
return Err(format!("{v}: fraction values must be in [0.0, 1.0]"));
}
if lo > hi {
return Err(format!("{v}: min ({lo}) is greater than max ({hi})"));
}
}
if min_count > max_count {
return Err(format!("--min-count/--max-count: min ({min_count}) is greater than max ({max_count})"));
}
if min_outgroup_count > max_outgroup_count {
return Err(format!("--min-outgroup-count/--max-outgroup-count: min ({min_outgroup_count}) is greater than max ({max_outgroup_count})"));
}
Ok(GroupQuorumFilter {
ingroup_idx,
outgroup_idx,
threshold: p.threshold,
min_count,
max_count,
min_frac,
max_frac,
min_outgroup_count,
max_outgroup_count,
min_outgroup_frac,
max_outgroup_frac,
})
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ pub fn run(args: DumpArgs) {
&idx.meta().genomes.len() &idx.meta().genomes.len()
); );
let filters = args.filter.build_filters(&idx.meta().genomes); let filters = args.filter.build_filters(idx.meta());
let pb = progress_bar("dump", idx.n_partitions() as u64, "partitions"); let pb = progress_bar("dump", idx.n_partitions() as u64, "partitions");
let stdout = io::stdout(); let stdout = io::stdout();
+1 -1
View File
@@ -66,7 +66,7 @@ pub fn run(args: FilterCmdArgs) {
&src.meta().genomes.len(), mode, args.source.display() &src.meta().genomes.len(), mode, args.source.display()
); );
let mut filters = args.filter.build_filters(&src.meta().genomes); let mut filters = args.filter.build_filters(src.meta());
if let Some(v) = args.min_total_count { if let Some(v) = args.min_total_count {
filters.push(Box::new(MinTotalCount { total: v })); filters.push(Box::new(MinTotalCount { total: v }));
+4 -244
View File
@@ -1,144 +1,7 @@
use std::collections::HashMap;
use clap::Args; use clap::Args;
use obikindex::GenomeInfo; use obikindex::{GroupFilterParams, IndexMeta, MetaPred};
use obikpartitionner::{GroupQuorumFilter, KmerFilter}; use obikpartitionner::KmerFilter;
use obitaxonomy::{TaxPath, TaxPattern};
// ── Operator ──────────────────────────────────────────────────────────────────
enum PredOp { Wildcard, Eq, Ne, Matches, NotMatches }
// ── MetaPred ──────────────────────────────────────────────────────────────────
/// A single predicate on genome metadata: `key OP val1|val2|…`
///
/// Operators: `=` (exact), `!=` (not equal), `~` (path ancestor), `!~` (not ancestor).
/// Multiple values separated by `|` are OR'd.
pub struct MetaPred {
key: String,
op: PredOp,
values: Vec<String>,
}
impl MetaPred {
/// Parse a predicate string of the form `key=v1|v2`, `key!=v`, `key~path`, `key!~path`.
/// The special values `*` and `all` (case-insensitive) match every genome.
pub fn parse(s: &str) -> Result<Self, String> {
let t = s.trim();
if t == "*" || t.eq_ignore_ascii_case("all") {
return Ok(Self { key: String::new(), op: PredOp::Wildcard, values: vec![] });
}
let (op, key, rhs) =
if let Some(pos) = s.find("!=") {
(PredOp::Ne, &s[..pos], &s[pos+2..])
} else if let Some(pos) = s.find("!~") {
(PredOp::NotMatches, &s[..pos], &s[pos+2..])
} else if let Some(pos) = s.find('=') {
(PredOp::Eq, &s[..pos], &s[pos+1..])
} else if let Some(pos) = s.find('~') {
(PredOp::Matches, &s[..pos], &s[pos+1..])
} else {
return Err(format!("no operator found in predicate: {s}"));
};
let key = key.trim().to_string();
if key.is_empty() { return Err(format!("empty key in predicate: {s}")); }
let values: Vec<String> = rhs.split('|').map(|v| v.trim().to_string()).collect();
if values.iter().any(|v| v.is_empty()) {
return Err(format!("empty value in predicate: {s}"));
}
Ok(Self { key, op, values })
}
/// Evaluate against one genome's metadata.
/// Returns `None` when the key is absent (NA propagation).
fn eval(&self, meta: &HashMap<String, String>) -> Option<bool> {
if matches!(self.op, PredOp::Wildcard) { return Some(true); }
let value = meta.get(&self.key)?;
Some(match self.op {
PredOp::Wildcard => unreachable!(),
PredOp::Eq => self.values.iter().any(|v| v == value),
PredOp::Ne => self.values.iter().all(|v| v != value),
PredOp::Matches => self.values.iter().any(|v| path_matches(value, v)),
PredOp::NotMatches => self.values.iter().all(|v| !path_matches(value, v)),
})
}
}
// ── Path matching ─────────────────────────────────────────────────────────────
/// True if the stored taxonomy `value` matches `pattern`.
///
/// `value` must be a valid `TaxPath` (starts with `taxonomy:/`).
/// `pattern` is a `TaxPattern` query (see `obitaxonomy::TaxPattern` for syntax).
/// Returns `false` if either fails to parse.
fn path_matches(value: &str, pattern: &str) -> bool {
let Ok(path) = TaxPath::parse(value) else { return false };
let Ok(pat) = TaxPattern::parse(pattern) else { return false };
pat.matches(&path)
}
// ── Three-value group evaluation ──────────────────────────────────────────────
/// AND of all predicates (ingroup semantics).
/// Short-circuits on `Some(false)`; propagates `None` if no predicate returns `false`.
fn eval_and(preds: &[MetaPred], meta: &HashMap<String, String>) -> Option<bool> {
let mut has_na = false;
for pred in preds {
match pred.eval(meta) {
Some(false) => return Some(false),
Some(true) => {}
None => has_na = true,
}
}
if has_na { None } else { Some(true) }
}
/// OR of all predicates (outgroup semantics).
/// Short-circuits on `Some(true)`; propagates `None` if no predicate returns `true`.
fn eval_or(preds: &[MetaPred], meta: &HashMap<String, String>) -> Option<bool> {
let mut has_na = false;
for pred in preds {
match pred.eval(meta) {
Some(true) => return Some(true),
Some(false) => {}
None => has_na = true,
}
}
if has_na { None } else { Some(false) }
}
// ── Genome classification ─────────────────────────────────────────────────────
enum Membership { Ingroup, Outgroup, Uncategorized }
fn classify(
genomes: &[GenomeInfo],
ingroup: &[MetaPred],
outgroup: &[MetaPred],
) -> Vec<Membership> {
genomes.iter().map(|g| {
let in_r = if ingroup.is_empty() { None } else { eval_and(ingroup, &g.meta) };
let out_r = if outgroup.is_empty() { None } else { eval_or(outgroup, &g.meta) };
// Ingroup wins over outgroup.
if in_r == Some(true) { return Membership::Ingroup; }
if out_r == Some(true) { return Membership::Outgroup; }
Membership::Uncategorized
}).collect()
}
// ── Public constructor ────────────────────────────────────────────────────────
/// Build a `GroupQuorumFilter` from parsed predicates and genome metadata.
///
/// - No groups defined: `ingroup_idx` = all genomes (implicit ingroup).
/// - `ingroup` predicates only: outgroup indices are empty.
/// - `outgroup` predicates only: ingroup indices are empty.
/// - Both defined: ingroup wins on overlap; uncategorized genomes are ignored.
/// CLI args for ingroup/outgroup filtering — embeddable in any command via `#[command(flatten)]`. /// CLI args for ingroup/outgroup filtering — embeddable in any command via `#[command(flatten)]`.
#[derive(Args)] #[derive(Args)]
pub struct FilterArgs { pub struct FilterArgs {
@@ -195,7 +58,7 @@ pub struct FilterArgs {
impl FilterArgs { impl FilterArgs {
/// Parse predicates and build a filter list ready to pass to `iter_partition_kmers`. /// Parse predicates and build a filter list ready to pass to `iter_partition_kmers`.
pub fn build_filters(&self, genomes: &[GenomeInfo]) -> Vec<Box<dyn KmerFilter>> { pub fn build_filters(&self, meta: &IndexMeta) -> Vec<Box<dyn KmerFilter>> {
let ingroup_preds: Vec<MetaPred> = self.ingroup.iter() let ingroup_preds: Vec<MetaPred> = self.ingroup.iter()
.map(|s| MetaPred::parse(s).unwrap_or_else(|e| { .map(|s| MetaPred::parse(s).unwrap_or_else(|e| {
eprintln!("error in --ingroup: {e}"); eprintln!("error in --ingroup: {e}");
@@ -208,8 +71,7 @@ impl FilterArgs {
std::process::exit(1); std::process::exit(1);
})) }))
.collect(); .collect();
let filter = build_group_filter( let filter = meta.build_group_filter(
genomes,
&ingroup_preds, &ingroup_preds,
&outgroup_preds, &outgroup_preds,
GroupFilterParams { GroupFilterParams {
@@ -230,105 +92,3 @@ impl FilterArgs {
vec![Box::new(filter)] vec![Box::new(filter)]
} }
} }
/// Returns indices of genomes matching `pred_str` (single predicate).
pub fn matching_genome_indices(pred_str: &str, genomes: &[GenomeInfo]) -> Result<Vec<usize>, String> {
let pred = MetaPred::parse(pred_str)?;
Ok(genomes.iter().enumerate()
.filter_map(|(i, g)| {
if pred.eval(&g.meta) == Some(true) { Some(i) } else { std::option::Option::None }
})
.collect())
}
pub struct GroupFilterParams {
pub threshold: u32,
pub min_count: Option<isize>,
pub max_count: Option<isize>,
pub min_frac: Option<f64>,
pub max_frac: Option<f64>,
pub min_outgroup_count: Option<isize>,
pub max_outgroup_count: Option<isize>,
pub min_outgroup_frac: Option<f64>,
pub max_outgroup_frac: Option<f64>,
}
pub fn build_group_filter(
genomes: &[GenomeInfo],
ingroup_preds: &[MetaPred],
outgroup_preds: &[MetaPred],
p: GroupFilterParams,
) -> Result<GroupQuorumFilter, String> {
let (ingroup_idx, outgroup_idx) = if ingroup_preds.is_empty() && outgroup_preds.is_empty() {
((0..genomes.len()).collect(), vec![])
} else {
let members = classify(genomes, ingroup_preds, outgroup_preds);
let in_idx: Vec<usize> = members.iter().enumerate()
.filter(|(_, m)| matches!(m, Membership::Ingroup))
.map(|(i, _)| i).collect();
let out_idx: Vec<usize> = members.iter().enumerate()
.filter(|(_, m)| matches!(m, Membership::Outgroup))
.map(|(i, _)| i).collect();
(in_idx, out_idx)
};
let in_size = ingroup_idx.len();
let out_size = outgroup_idx.len();
let ingroup_quorum_explicit = p.min_count.is_some() || p.max_count.is_some()
|| p.min_frac.is_some() || p.max_frac.is_some();
let outgroup_quorum_explicit = p.min_outgroup_count.is_some() || p.max_outgroup_count.is_some()
|| p.min_outgroup_frac.is_some() || p.max_outgroup_frac.is_some();
let default_min_frac = if !ingroup_preds.is_empty() && !ingroup_quorum_explicit { 1.0 } else { 0.0 };
let default_max_outgroup_count = if !outgroup_preds.is_empty() && !outgroup_quorum_explicit { 0 } else { out_size };
// Resolve a signed count: negative means an offset from the group size
// (e.g. -1 = all but one), floored at 1 so the negative form always keeps
// constraining the group — even a singleton group, where n-1 would be 0
// and would otherwise drop the constraint entirely.
let resolve = |v: isize, size: usize| -> usize {
if v < 0 { (size as isize + v).max(1) as usize } else { v as usize }
};
let min_count = p.min_count.map(|v| resolve(v, in_size)).unwrap_or(0);
let max_count = p.max_count.map(|v| resolve(v, in_size)).unwrap_or(in_size);
let min_frac = p.min_frac.unwrap_or(default_min_frac);
let max_frac = p.max_frac.unwrap_or(1.0);
let min_outgroup_count = p.min_outgroup_count.map(|v| resolve(v, out_size)).unwrap_or(0);
let max_outgroup_count = p.max_outgroup_count.map(|v| resolve(v, out_size)).unwrap_or(default_max_outgroup_count);
let min_outgroup_frac = p.min_outgroup_frac.unwrap_or(0.0);
let max_outgroup_frac = p.max_outgroup_frac.unwrap_or(1.0);
for (v, lo, hi) in [
("--min-frac/--max-frac", min_frac, max_frac),
("--min-outgroup-frac/--max-outgroup-frac", min_outgroup_frac, max_outgroup_frac),
] {
if !(0.0..=1.0).contains(&lo) || !(0.0..=1.0).contains(&hi) {
return Err(format!("{v}: fraction values must be in [0.0, 1.0]"));
}
if lo > hi {
return Err(format!("{v}: min ({lo}) is greater than max ({hi})"));
}
}
if min_count > max_count {
return Err(format!("--min-count/--max-count: min ({min_count}) is greater than max ({max_count})"));
}
if min_outgroup_count > max_outgroup_count {
return Err(format!("--min-outgroup-count/--max-outgroup-count: min ({min_outgroup_count}) is greater than max ({max_outgroup_count})"));
}
Ok(GroupQuorumFilter {
ingroup_idx,
outgroup_idx,
threshold: p.threshold,
min_count,
max_count,
min_frac,
max_frac,
min_outgroup_count,
max_outgroup_count,
min_outgroup_frac,
max_outgroup_frac,
})
}
+6 -6
View File
@@ -2,13 +2,11 @@ use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf; use std::path::PathBuf;
use clap::{Args, ValueEnum}; use clap::{Args, ValueEnum};
use obikindex::{GenomeInfo, KmerIndex}; use obikindex::{IndexMeta, KmerIndex};
use obikpartitionner::{AggOp, OutputCol}; use obikpartitionner::{AggOp, OutputCol};
use obisys::Reporter; use obisys::Reporter;
use tracing::info; use tracing::info;
use super::predicate::matching_genome_indices;
// ── CLI types ───────────────────────────────────────────────────────────────── // ── CLI types ─────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
@@ -114,9 +112,11 @@ fn default_op(src_is_count: bool) -> AggOp {
/// Returns `(specs, output_presence)`. /// Returns `(specs, output_presence)`.
fn build_specs( fn build_specs(
args: &SelectArgs, args: &SelectArgs,
genomes: &[GenomeInfo], meta: &IndexMeta,
src_is_count: bool, src_is_count: bool,
) -> (Vec<OutputCol>, bool) { ) -> (Vec<OutputCol>, bool) {
let genomes = &meta.genomes;
// ── 1. Build group_indices: name → Vec<usize> ──────────────────────────── // ── 1. Build group_indices: name → Vec<usize> ────────────────────────────
// Also keep insertion order for the default `--select *` case. // Also keep insertion order for the default `--select *` case.
let mut group_order: Vec<String> = Vec::new(); let mut group_order: Vec<String> = Vec::new();
@@ -137,7 +137,7 @@ fn build_specs(
} else { } else {
for raw in &args.group { for raw in &args.group {
let (name, pred) = parse_name_value(raw, "--group"); let (name, pred) = parse_name_value(raw, "--group");
let idxs = matching_genome_indices(&pred, genomes).unwrap_or_else(|e| { let idxs = meta.matching_genome_indices(&pred).unwrap_or_else(|e| {
eprintln!("error in --group {name}: {e}"); eprintln!("error in --group {name}: {e}");
std::process::exit(1); std::process::exit(1);
}); });
@@ -231,7 +231,7 @@ pub fn run(args: SelectArgs) {
}); });
let src_is_count = src.meta().config.with_counts; let src_is_count = src.meta().config.with_counts;
let (specs, output_presence) = build_specs(&args, &src.meta().genomes.clone(), src_is_count); let (specs, output_presence) = build_specs(&args, src.meta(), src_is_count);
info!( info!(
"select: {} genome(s) → {} output column(s), output={}", "select: {} genome(s) → {} output column(s), output={}",
+1 -1
View File
@@ -35,7 +35,7 @@ pub fn run(args: UnitigArgs) {
info!("unitig: building de Bruijn graph from {n} partition(s) (k={k})"); info!("unitig: building de Bruijn graph from {n} partition(s) (k={k})");
let filters = args.filter.build_filters(&idx.meta().genomes); let filters = args.filter.build_filters(idx.meta());
let partition = idx.partition(); let partition = idx.partition();
let mut rep = Reporter::new(); let mut rep = Reporter::new();