|
|
@@ -238,9 +238,9 @@ impl VcfVariant {
|
|
|
(ReferenceAlternative::Nucleotide(_), ReferenceAlternative::Nucleotides(_)) => {
|
|
|
AlterationCategory::INS
|
|
|
}
|
|
|
- (ReferenceAlternative::Nucleotide(_), ReferenceAlternative::Unstructured(_)) => {
|
|
|
- AlterationCategory::Other
|
|
|
- }
|
|
|
+ // (ReferenceAlternative::Nucleotide(_), ReferenceAlternative::Unstructured(_)) => {
|
|
|
+ // AlterationCategory::Other
|
|
|
+ // }
|
|
|
(ReferenceAlternative::Nucleotides(_), ReferenceAlternative::Nucleotide(_)) => {
|
|
|
AlterationCategory::DEL
|
|
|
}
|
|
|
@@ -283,67 +283,10 @@ impl VcfVariant {
|
|
|
/// # Errors
|
|
|
/// This function will return an error if:
|
|
|
/// - The alteration category is not BND
|
|
|
- /// - The alternative string cannot be parsed into exactly 3 parts
|
|
|
- /// - The b_position cannot be parsed as a number
|
|
|
pub fn bnd_desc(&self) -> anyhow::Result<BNDDesc> {
|
|
|
let alt = self.alternative.to_string();
|
|
|
- if alt.contains('[') || alt.contains(']') {
|
|
|
- let alt_rep = alt.replace("[", ";").replace("]", ";");
|
|
|
- let alt_is_joined_after = !alt_rep.starts_with(";");
|
|
|
- let parts = alt_rep
|
|
|
- .split(";")
|
|
|
- .filter(|c| !c.is_empty())
|
|
|
- .collect::<Vec<&str>>();
|
|
|
-
|
|
|
- if alt_is_joined_after {
|
|
|
- // a is ref b is alt
|
|
|
- let a_sens = true;
|
|
|
- let a_contig = self.position.contig();
|
|
|
- let a_position = self.position.position + 1;
|
|
|
-
|
|
|
- let added_nt = parts[0][1..].to_string();
|
|
|
-
|
|
|
- let b_sens = alt.contains('[');
|
|
|
- let (contig, pos) = parts[1].split_once(':').unwrap();
|
|
|
- let b_contig = contig.to_string();
|
|
|
- let b_position: u32 = pos.parse()?;
|
|
|
-
|
|
|
- Ok(BNDDesc {
|
|
|
- a_contig,
|
|
|
- a_position,
|
|
|
- a_sens,
|
|
|
- b_contig,
|
|
|
- b_position,
|
|
|
- b_sens,
|
|
|
- added_nt,
|
|
|
- })
|
|
|
- } else {
|
|
|
- // a is alt b is ref
|
|
|
- let b_sens = true;
|
|
|
- let b_contig = self.position.contig();
|
|
|
- let b_position = self.position.position + 1;
|
|
|
-
|
|
|
- let mut added_nt = parts[1].to_string();
|
|
|
- added_nt.pop();
|
|
|
-
|
|
|
- let a_sens = alt.contains(']');
|
|
|
- let (contig, pos) = parts[0].split_once(':').unwrap();
|
|
|
- let a_contig = contig.to_string();
|
|
|
- let a_position: u32 = pos.parse()?;
|
|
|
-
|
|
|
- Ok(BNDDesc {
|
|
|
- a_contig,
|
|
|
- a_position,
|
|
|
- a_sens,
|
|
|
- b_contig,
|
|
|
- b_position,
|
|
|
- b_sens,
|
|
|
- added_nt,
|
|
|
- })
|
|
|
- }
|
|
|
- } else {
|
|
|
- Err(anyhow::anyhow!("The alteration is not BND: {alt}"))
|
|
|
- }
|
|
|
+ BNDDesc::from_vcf_breakend(&self.position().contig(), self.position.position + 1, &alt)
|
|
|
+ .context(format!("The alteration is not BND: {alt}"))
|
|
|
}
|
|
|
|
|
|
/// Returns the length of the deletion if the variant is a deletion (`DEL`).
|
|
|
@@ -412,15 +355,393 @@ impl VcfVariant {
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
|
|
pub struct BNDDesc {
|
|
|
+ /// Coordinates of the **5′ end** (A)
|
|
|
pub a_contig: String,
|
|
|
- pub a_position: u32, // 1-based
|
|
|
+ pub a_position: u32,
|
|
|
pub a_sens: bool,
|
|
|
+
|
|
|
+ /// Coordinates of the **3′ end** (B)
|
|
|
pub b_contig: String,
|
|
|
- pub b_position: u32, // 1-based
|
|
|
+ pub b_position: u32,
|
|
|
pub b_sens: bool,
|
|
|
+
|
|
|
+ /// Inserted nucleotides at the junction (may be empty)
|
|
|
pub added_nt: String,
|
|
|
}
|
|
|
|
|
|
+impl BNDDesc {
|
|
|
+ /// Construct a `BNDDesc` from VCF `CHROM`, `POS`, and break‑end `ALT` string.
|
|
|
+ /// Supports the four VCF break‑end ALT forms:
|
|
|
+ ///
|
|
|
+ /// 1) `t[p[` → A(5′)=t flank, B(3′)=p flank (forward-forward)
|
|
|
+ /// 2) `t]p]` → A(5′)=t flank, B(3′)=p flank but on reverse (`a_sens=true`, `b_sens=false`)
|
|
|
+ /// 3) `]p]t` → A(5′)=p flank, B(3′)=t flank (`a_sens=false`, `b_sens=true`)
|
|
|
+ /// 4) `[p[t` → A(5′)=p flank, B(3′)=t flank with both on reverse (`a_sens=false`, `b_sens=false`)
|
|
|
+ ///
|
|
|
+ /// Here `t` is the local flank sequence, `p` is the remote contig:pos.
|
|
|
+ pub fn from_vcf_breakend(chrom: &str, pos: u32, alt: &str) -> Option<Self> {
|
|
|
+ // locate first bracket
|
|
|
+ let (open, bracket) = alt.char_indices().find(|&(_, c)| c == '[' || c == ']')?;
|
|
|
+ // find matching bracket
|
|
|
+ let close = alt[open + 1..].find(bracket)? + open + 1;
|
|
|
+ // parse remote contig:pos between brackets
|
|
|
+ let addr = &alt[open + 1..close];
|
|
|
+ let mut parts = addr.splitn(2, ':');
|
|
|
+ let rc = parts.next()?;
|
|
|
+ let rp: u32 = parts.next()?.parse().ok()?;
|
|
|
+ // inserted sequence
|
|
|
+ let seq_before = &alt[..open];
|
|
|
+ let seq_after = &alt[close + 1..];
|
|
|
+ let mut added_nt = String::new();
|
|
|
+ added_nt.push_str(seq_before);
|
|
|
+ added_nt.push_str(seq_after);
|
|
|
+ // determine form and orientation
|
|
|
+ // form 1 & 2: bracket after t (open>0): local before remote
|
|
|
+ // form 3 & 4: bracket before t (open==0): remote before local
|
|
|
+ let after_local = open > 0;
|
|
|
+ let (a_sens, b_sens) = match (bracket, after_local) {
|
|
|
+ ('[', true) => (true, true), // form 1
|
|
|
+ (']', true) => (true, false), // form 2
|
|
|
+ (']', false) => (false, true), // form 3
|
|
|
+ ('[', false) => (false, false), // form 4
|
|
|
+ _ => return None,
|
|
|
+ };
|
|
|
+ // assign A/B depending on form
|
|
|
+ let (a_contig, a_position, b_contig, b_position) = if after_local {
|
|
|
+ // forms 1 & 2: A is local, B is remote
|
|
|
+ (chrom.into(), pos, rc.into(), rp)
|
|
|
+ } else {
|
|
|
+ // forms 3 & 4: A is remote, B is local
|
|
|
+ (rc.into(), rp, chrom.into(), pos)
|
|
|
+ };
|
|
|
+ Some(Self {
|
|
|
+ a_contig,
|
|
|
+ a_position,
|
|
|
+ a_sens,
|
|
|
+ b_contig,
|
|
|
+ b_position,
|
|
|
+ b_sens,
|
|
|
+ added_nt,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Serialize this `BNDDesc` back into a VCF ALT break‑end string.
|
|
|
+ /// Reconstructs one of the four canonical forms using `added_nt` as the flank
|
|
|
+ /// sequence (`t`) and `b_contig:b_position` as the remote site (`p`).
|
|
|
+ pub fn to_vcf_breakend(&self) -> String {
|
|
|
+ // `t` is inserted NTs, `p` is remote site
|
|
|
+ let t = &self.added_nt;
|
|
|
+ let p = format!("{}:{}", self.b_contig, self.b_position);
|
|
|
+ match (self.a_sens, self.b_sens) {
|
|
|
+ (true, true) => format!("{}[{}[", t, p), // t[p[
|
|
|
+ (true, false) => format!("{}]{}]", t, p), // t]p]
|
|
|
+ (false, true) => format!("]{}]{}", p, t), // ]p]t
|
|
|
+ (false, false) => format!("[{}[{}", p, t), // [p[t
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl core::fmt::Display for BNDDesc {
|
|
|
+ /// Compact textual form: `(A:contig:pos<arrow>;B:contig:pos<arrow>;ins=...)`.
|
|
|
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
|
+ fn arrow(b: bool) -> &'static str {
|
|
|
+ if b {
|
|
|
+ "->"
|
|
|
+ } else {
|
|
|
+ "<-"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ write!(
|
|
|
+ f,
|
|
|
+ "({}:{}{};{}:{}{};ins={})",
|
|
|
+ self.a_contig,
|
|
|
+ self.a_position,
|
|
|
+ arrow(self.a_sens),
|
|
|
+ self.b_contig,
|
|
|
+ self.b_position,
|
|
|
+ arrow(self.b_sens),
|
|
|
+ self.added_nt
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+use petgraph::graph::{DiGraph, NodeIndex};
|
|
|
+use petgraph::visit::{IntoNodeIdentifiers, NodeIndexable};
|
|
|
+use petgraph::Direction;
|
|
|
+
|
|
|
+/// Wrapper around `petgraph::DiGraph` with `BNDDesc` nodes.
|
|
|
+pub struct BNDGraph<E = ()> {
|
|
|
+ graph: DiGraph<BNDDesc, E>,
|
|
|
+}
|
|
|
+
|
|
|
+impl<E> Default for BNDGraph<E> {
|
|
|
+ fn default() -> Self {
|
|
|
+ Self {
|
|
|
+ graph: DiGraph::new(),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl<E> BNDGraph<E> {
|
|
|
+ pub fn new() -> Self {
|
|
|
+ Self::default()
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn add_breakpoint(&mut self, desc: BNDDesc) -> NodeIndex {
|
|
|
+ self.graph.add_node(desc)
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn successors(&self, n: NodeIndex) -> impl Iterator<Item = NodeIndex> + '_ {
|
|
|
+ self.graph.neighbors_directed(n, Direction::Outgoing)
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn predecessors(&self, n: NodeIndex) -> impl Iterator<Item = NodeIndex> + '_ {
|
|
|
+ self.graph.neighbors_directed(n, Direction::Incoming)
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn inner(&self) -> &DiGraph<BNDDesc, E> {
|
|
|
+ &self.graph
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Stringify all nodes for quick debugging.
|
|
|
+ pub fn nodes_as_strings(&self) -> Vec<String> {
|
|
|
+ self.graph.node_weights().map(|n| n.to_string()).collect()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[allow(clippy::collapsible_if)]
|
|
|
+impl<E: Default> BNDGraph<E> {
|
|
|
+ /// Build edges following downstream 5′→3′ logic for two patterns:
|
|
|
+ ///
|
|
|
+ /// **Pattern 1** (B → A, *u precedes v*):
|
|
|
+ ///
|
|
|
+ /// * forward (→) `u.b ≤ v.a` → edge **u → v**
|
|
|
+ /// * reverse (←) `u.b ≥ v.a` → edge **v → u**
|
|
|
+ ///
|
|
|
+ /// **Pattern 2** (A → B, *v precedes u*):
|
|
|
+ ///
|
|
|
+ /// * forward (→) `u.a ≥ v.b` → edge **v → u**
|
|
|
+ /// * reverse (←) `u.a ≤ v.b` → edge **u → v**
|
|
|
+ pub fn auto_connect(&mut self)
|
|
|
+ where
|
|
|
+ E: Default,
|
|
|
+ {
|
|
|
+ let nodes: Vec<NodeIndex> = self.graph.node_indices().collect();
|
|
|
+ let mut pending: Vec<(NodeIndex, NodeIndex)> = Vec::new();
|
|
|
+
|
|
|
+ for &u_idx in &nodes {
|
|
|
+ for &v_idx in &nodes {
|
|
|
+ if u_idx == v_idx {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ let u = &self.graph[u_idx];
|
|
|
+ let v = &self.graph[v_idx];
|
|
|
+
|
|
|
+ // Pattern 1: B(u) -> A(v) (u precedes v)
|
|
|
+ if u.b_contig == v.a_contig && u.b_sens == v.a_sens {
|
|
|
+ if (u.b_sens && u.b_position <= v.a_position)
|
|
|
+ || (!u.b_sens && u.b_position >= v.a_position)
|
|
|
+ {
|
|
|
+ pending.push((u_idx, v_idx));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Pattern 2: A(u) -> B(v) (v precedes u)
|
|
|
+ if u.a_contig == v.b_contig && u.a_sens == v.b_sens {
|
|
|
+ if (u.a_sens && u.a_position >= v.b_position)
|
|
|
+ || (!u.a_sens && u.a_position <= v.b_position)
|
|
|
+ {
|
|
|
+ // Edge direction depends on strand logic
|
|
|
+ let (src, dst) = if u.a_sens {
|
|
|
+ // forward ⇒ edge v -> u
|
|
|
+ (v_idx, u_idx)
|
|
|
+ } else {
|
|
|
+ // reverse ⇒ edge u -> v
|
|
|
+ (u_idx, v_idx)
|
|
|
+ };
|
|
|
+ pending.push((src, dst));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // second pass – insert, skipping duplicates
|
|
|
+ for (src, dst) in pending {
|
|
|
+ if self.graph.find_edge(src, dst).is_none() {
|
|
|
+ self.graph.add_edge(src, dst, E::default());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Add an edge if absent.
|
|
|
+ pub fn add_edge_if_absent(&mut self, src: NodeIndex, dst: NodeIndex) {
|
|
|
+ if self.graph.find_edge(src, dst).is_none() {
|
|
|
+ self.graph.add_edge(src, dst, E::default());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Pretty‑print a sequence of nodes indicating **actual traversal
|
|
|
+ /// direction** between successive nodes:
|
|
|
+ /// * `→` when the graph contains an edge from *previous* → *current*.
|
|
|
+ /// * `←` when the edge exists from *current* → *previous* (path goes
|
|
|
+ /// "against" the stored direction).
|
|
|
+ ///
|
|
|
+ /// If neither directional edge is present the placeholder `--` is used so
|
|
|
+ /// debugging clearly shows missing links.
|
|
|
+ pub fn fmt_path(&self, path: &[NodeIndex]) -> String {
|
|
|
+ use core::fmt::Write as _;
|
|
|
+ let mut out = String::new();
|
|
|
+ if let Some((&first, rest)) = path.split_first() {
|
|
|
+ let _ = write!(out, "{}", &self.graph[first]);
|
|
|
+ let mut prev = first;
|
|
|
+ for &curr in rest {
|
|
|
+ let arrow = if self.graph.find_edge(prev, curr).is_some() {
|
|
|
+ " → "
|
|
|
+ } else if self.graph.find_edge(curr, prev).is_some() {
|
|
|
+ " ← "
|
|
|
+ } else {
|
|
|
+ " -- " // unexpected: no direct edge
|
|
|
+ };
|
|
|
+ out.push_str(arrow);
|
|
|
+ let _ = write!(out, "{}", &self.graph[curr]);
|
|
|
+ prev = curr;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ out
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // Traversal utilities
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ /// Return a directed path that visits **every node exactly once** (Hamiltonian
|
|
|
+ /// path) if one exists. Uses naive DFS back-tracking which is exponential—
|
|
|
+ /// fine for small graphs (<15 nodes) but aborts early otherwise.
|
|
|
+ pub fn hamiltonian_path(&self) -> Option<Vec<NodeIndex>> {
|
|
|
+ let n = self.graph.node_count();
|
|
|
+ if n == 0 {
|
|
|
+ return Some(Vec::new());
|
|
|
+ }
|
|
|
+ if n > 15 {
|
|
|
+ // avoid combinatorial explosion
|
|
|
+ return None;
|
|
|
+ }
|
|
|
+ let mut path = Vec::with_capacity(n);
|
|
|
+ let mut visited = vec![false; self.graph.node_bound()];
|
|
|
+ for start in self.graph.node_identifiers() {
|
|
|
+ path.push(start);
|
|
|
+ visited[start.index()] = true;
|
|
|
+ if self.dfs_hamiltonian(start, &mut visited, &mut path, n) {
|
|
|
+ return Some(path);
|
|
|
+ }
|
|
|
+ visited[start.index()] = false;
|
|
|
+ path.clear();
|
|
|
+ }
|
|
|
+ None
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ Recursive helper for [`hamiltonian_path`](#method.hamiltonian_path).
|
|
|
+
|
|
|
+ * `current` – node we are currently expanding from.
|
|
|
+ * `visited` – mutable bitmap indicating which nodes are already on `path`.
|
|
|
+ * `path` – growing list of nodes (last element is always `current`).
|
|
|
+ * `target` – desired final length (= `graph.node_count()`).
|
|
|
+
|
|
|
+ The function explores each outgoing neighbour that hasn’t been visited yet,
|
|
|
+ marking it **visited → recurse → unvisit** (classic DFS back-tracking). It
|
|
|
+ returns `true` as soon as a full-length path is discovered, which bubbles
|
|
|
+ up through the recursion to terminate the search early.
|
|
|
+ */
|
|
|
+ fn dfs_hamiltonian(
|
|
|
+ &self,
|
|
|
+ current: NodeIndex,
|
|
|
+ visited: &mut [bool],
|
|
|
+ path: &mut Vec<NodeIndex>,
|
|
|
+ target: usize,
|
|
|
+ ) -> bool {
|
|
|
+ // Base‑case: reached required length → success.
|
|
|
+ if path.len() == target {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Explore every outgoing neighbour.
|
|
|
+ for next in self.graph.neighbors(current) {
|
|
|
+ if !visited[next.index()] {
|
|
|
+ // choose
|
|
|
+ visited[next.index()] = true;
|
|
|
+ path.push(next);
|
|
|
+
|
|
|
+ // explore
|
|
|
+ if self.dfs_hamiltonian(next, visited, path, target) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // un-choose (back-track)
|
|
|
+ path.pop();
|
|
|
+ visited[next.index()] = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ false
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Weakly-connected components sorted descending by size (no `Clone` bound).
|
|
|
+ pub fn components_by_size(&self) -> Vec<Vec<NodeIndex>> {
|
|
|
+ let mut visited = vec![false; self.graph.node_bound()];
|
|
|
+ let mut comps: Vec<Vec<NodeIndex>> = Vec::new();
|
|
|
+ for start in self.graph.node_indices() {
|
|
|
+ if visited[start.index()] {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // DFS over undirected neighbours
|
|
|
+ let mut stack = vec![start];
|
|
|
+ let mut comp = Vec::new();
|
|
|
+ visited[start.index()] = true;
|
|
|
+ while let Some(n) = stack.pop() {
|
|
|
+ comp.push(n);
|
|
|
+ for neigh in self.graph.neighbors_undirected(n) {
|
|
|
+ if !visited[neigh.index()] {
|
|
|
+ visited[neigh.index()] = true;
|
|
|
+ stack.push(neigh);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ comps.push(comp);
|
|
|
+ }
|
|
|
+ comps.sort_by_key(|c| std::cmp::Reverse(c.len()));
|
|
|
+ comps
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Convenience wrapper: try to return a Hamiltonian path; if impossible,
|
|
|
+ /// return all weakly‑connected subgraphs ordered by size.
|
|
|
+ pub fn path_or_components(&self) -> Result<Vec<NodeIndex>, Vec<Vec<NodeIndex>>> {
|
|
|
+ if let Some(p) = self.hamiltonian_path() {
|
|
|
+ Ok(p)
|
|
|
+ } else {
|
|
|
+ Err(self.components_by_size())
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+pub trait ToBNDGraph<E = ()> {
|
|
|
+ /// Consume the vector and return a `BNDGraph` whose edges are created with
|
|
|
+ /// `auto_connect()`.
|
|
|
+ fn to_bnd_graph(self) -> BNDGraph<E>
|
|
|
+ where
|
|
|
+ E: Default;
|
|
|
+}
|
|
|
+
|
|
|
+impl<E: Default> ToBNDGraph<E> for Vec<BNDDesc> {
|
|
|
+ fn to_bnd_graph(self) -> BNDGraph<E> {
|
|
|
+ let mut g = BNDGraph::<E>::new();
|
|
|
+ for b in self {
|
|
|
+ g.add_breakpoint(b);
|
|
|
+ }
|
|
|
+ g.auto_connect();
|
|
|
+ g
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
|
|
pub struct DeletionDesc {
|
|
|
pub contig: String,
|
|
|
@@ -1671,13 +1992,17 @@ impl ReferenceAlternative {
|
|
|
match self {
|
|
|
ReferenceAlternative::Nucleotide(_) => None,
|
|
|
ReferenceAlternative::Nucleotides(bases) => {
|
|
|
- let seq = bases.iter().skip(1).map(|b| b.to_string()).collect::<String>();
|
|
|
+ let seq = bases
|
|
|
+ .iter()
|
|
|
+ .skip(1)
|
|
|
+ .map(|b| b.to_string())
|
|
|
+ .collect::<String>();
|
|
|
if seq.len() > 1 {
|
|
|
Some(estimate_shannon_entropy(&seq))
|
|
|
} else {
|
|
|
None
|
|
|
}
|
|
|
- },
|
|
|
+ }
|
|
|
ReferenceAlternative::Unstructured(_) => None,
|
|
|
}
|
|
|
}
|