Skip to main content

bidirected_adjacency_array/io/
gfa1.rs

1use std::{
2    borrow::Cow,
3    collections::HashMap,
4    fmt::Debug,
5    io::{BufRead, Write},
6};
7
8use log::warn;
9use tagged_vec::TaggedVec;
10
11use crate::{
12    graph::{BidirectedAdjacencyArray, BidirectedEdge},
13    index::{EdgeIndex, GraphIndexInteger, NodeIndex},
14};
15
16#[cfg(test)]
17mod tests;
18
19pub trait GfaNodeData {
20    fn name(&'_ self) -> Cow<'_, str>;
21    fn sequence(&'_ self) -> Cow<'_, str>;
22}
23
24pub trait GfaEdgeData {
25    fn overlap(&self) -> u16;
26}
27
28#[derive(thiserror::Error, Debug)]
29pub enum GfaReadError {
30    #[error("I/O error: {0}")]
31    IoError(#[from] std::io::Error),
32
33    #[error("a header line was found after other lines")]
34    WronglyPositionedHeader,
35
36    #[error("an S line is missing the sequence name")]
37    MissingSequenceNameInSLine,
38
39    #[error("an L line is missing the four fields specifying the edge endpoints")]
40    LLineTooShort,
41
42    #[error("unknown node name '{0}' in an L line")]
43    UnknownNodeName(String),
44
45    #[error("unknown sign '{0}' in an L line")]
46    UnknownGfaNodeSign(String),
47}
48
49struct UnresolvedBidirectedEdge {
50    from: String,
51    from_forward: bool,
52    to: String,
53    to_forward: bool,
54    data: PlainGfaEdgeData,
55}
56
57impl<
58    IndexType: GraphIndexInteger,
59    NodeData: From<PlainGfaNodeData>,
60    EdgeData: From<PlainGfaEdgeData>,
61> BidirectedAdjacencyArray<IndexType, NodeData, EdgeData>
62{
63    pub fn read_gfa1(
64        reader: impl BufRead,
65    ) -> Result<BidirectedAdjacencyArray<IndexType, NodeData, EdgeData>, GfaReadError> {
66        let mut node_name_to_node = HashMap::new();
67        let mut nodes = TaggedVec::<NodeIndex<IndexType>, _>::new();
68        let mut edges = TaggedVec::<EdgeIndex<IndexType>, _>::new();
69        let mut is_header_allowed = true;
70
71        for line in reader.lines() {
72            let line = line?;
73            let line = line.trim().split('\t').collect::<Vec<_>>();
74
75            match line[0] {
76                "H" => {
77                    if is_header_allowed {
78                        if let Some(&version) = line.get(1) {
79                            if version.starts_with("VN:Z:") {
80                                let version = version.trim_start_matches("VN:Z:");
81                                if version != "1.0" {
82                                    warn!(
83                                        "Unsupported GFA version {version:?}, expected \"1.0\". Attempting to parse anyway, but this may lead to errors or a wrong graph.",
84                                    );
85                                }
86                            } else {
87                                warn!(
88                                    "GFA header line has unrecognized version information, expected \"VN:Z:1.0\", but got \"{}\"",
89                                    version
90                                );
91                            }
92                        } else {
93                            warn!(
94                                "GFA header line is missing version information, expected \"VN:Z:1.0\""
95                            );
96                        }
97                    } else {
98                        return Err(GfaReadError::WronglyPositionedHeader);
99                    }
100                }
101
102                "S" => {
103                    let name = line
104                        .get(1)
105                        .ok_or(GfaReadError::MissingSequenceNameInSLine)?
106                        .to_string();
107                    let sequence = line.get(2).unwrap_or(&"").to_string();
108                    let node = nodes.push(
109                        PlainGfaNodeData {
110                            name: name.clone(),
111                            sequence,
112                        }
113                        .into(),
114                    );
115                    node_name_to_node.insert(name.clone(), node);
116                }
117
118                "L" => {
119                    // Parse edge line.
120                    let from = line.get(1).ok_or(GfaReadError::LLineTooShort)?.to_string();
121                    let from_forward = match *line.get(2).ok_or(GfaReadError::LLineTooShort)? {
122                        "+" => true,
123                        "-" => false,
124                        other => return Err(GfaReadError::UnknownGfaNodeSign(other.to_string())),
125                    };
126                    let to = line.get(3).ok_or(GfaReadError::LLineTooShort)?.to_string();
127                    let to_forward = match *line.get(4).ok_or(GfaReadError::LLineTooShort)? {
128                        "+" => true,
129                        "-" => false,
130                        other => return Err(GfaReadError::UnknownGfaNodeSign(other.to_string())),
131                    };
132                    let overlap_str = line.get(5).unwrap_or(&"0M");
133                    let overlap = overlap_str
134                        .trim_end_matches('M')
135                        .parse::<u16>()
136                        .unwrap_or(0);
137
138                    edges.push(UnresolvedBidirectedEdge {
139                        from,
140                        from_forward,
141                        to,
142                        to_forward,
143                        data: PlainGfaEdgeData { overlap },
144                    });
145                }
146
147                other => {
148                    warn!("Unsupported GFA line type: {}", other);
149                }
150            }
151
152            is_header_allowed = false;
153        }
154
155        let edges = edges
156            .into_values_iter()
157            .map(|edge| {
158                let from = node_name_to_node
159                    .get(&edge.from)
160                    .copied()
161                    .ok_or(GfaReadError::UnknownNodeName(edge.from))?;
162                let to = node_name_to_node
163                    .get(&edge.to)
164                    .copied()
165                    .ok_or(GfaReadError::UnknownNodeName(edge.to))?;
166
167                let from_forward = edge.from_forward;
168                let to_forward = edge.to_forward;
169                let data = EdgeData::from(edge.data);
170
171                Result::<_, GfaReadError>::Ok(BidirectedEdge {
172                    from,
173                    from_forward,
174                    to,
175                    to_forward,
176                    data,
177                })
178            })
179            .collect::<Result<Vec<_>, GfaReadError>>()?;
180
181        // Drop name index before constructing graph to save RAM.
182        drop(node_name_to_node);
183        Ok(BidirectedAdjacencyArray::new(nodes, edges.into()))
184    }
185}
186
187impl<IndexType: GraphIndexInteger, NodeData: GfaNodeData, EdgeData: GfaEdgeData>
188    BidirectedAdjacencyArray<IndexType, NodeData, EdgeData>
189{
190    pub fn write_gfa1(&self, mut writer: impl Write) -> Result<(), std::io::Error> {
191        // Write header.
192        writeln!(writer, "H\tVN:Z:1.0")?;
193
194        // Write nodes.
195        for node in self.iter_nodes() {
196            let node_data = self.node_data(node);
197            writeln!(writer, "S\t{}\t{}", node_data.name(), node_data.sequence())?;
198        }
199
200        // Write edges.
201        for edge in self.iter_edges() {
202            let edge_data = self.edge(edge);
203
204            let from_node_name = self.node_data(edge_data.from().into_bidirected()).name();
205            let to_node_name = self.node_data(edge_data.to().into_bidirected()).name();
206
207            // In mathematical notation, traversing an edge from a to b means using edge (a, \hat{b}).
208            // But in GFA1, this means using edge (a, b), where both signs are unchanged.
209            let from_node_sign = if edge_data.from().is_forward() {
210                "+"
211            } else {
212                "-"
213            };
214            let to_node_sign = if edge_data.to().is_forward() {
215                "+"
216            } else {
217                "-"
218            };
219
220            let overlap = edge_data.data().overlap();
221
222            writeln!(
223                writer,
224                "L\t{from_node_name}\t{from_node_sign}\t{to_node_name}\t{to_node_sign}\t{overlap}M",
225            )?;
226        }
227
228        Ok(())
229    }
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
233pub struct PlainGfaNodeData {
234    name: String,
235    sequence: String,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
239pub struct PlainGfaEdgeData {
240    overlap: u16,
241}
242
243impl PlainGfaNodeData {
244    pub fn new(name: impl ToString, sequence: impl ToString) -> Self {
245        Self {
246            name: name.to_string(),
247            sequence: sequence.to_string(),
248        }
249    }
250}
251
252impl GfaNodeData for PlainGfaNodeData {
253    fn name(&'_ self) -> Cow<'_, str> {
254        Cow::Borrowed(&self.name)
255    }
256
257    fn sequence(&'_ self) -> Cow<'_, str> {
258        Cow::Borrowed(&self.sequence)
259    }
260}
261
262impl PlainGfaEdgeData {
263    pub fn new(overlap: u16) -> Self {
264        Self { overlap }
265    }
266}
267
268impl GfaEdgeData for PlainGfaEdgeData {
269    fn overlap(&self) -> u16 {
270        self.overlap
271    }
272}