Skip to main content

bidirected_adjacency_array/io/
gfa1.rs

1use std::{
2    borrow::Cow,
3    collections::{HashMap, HashSet},
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        let mut unsupported_line_types = HashSet::new();
71
72        for line in reader.lines() {
73            let line = line?;
74            let line = line.trim().split('\t').collect::<Vec<_>>();
75
76            match line[0] {
77                "H" => {
78                    if is_header_allowed {
79                        if let Some(&version) = line.get(1) {
80                            if version.starts_with("VN:Z:") {
81                                let version = version.trim_start_matches("VN:Z:");
82                                if version != "1.0" {
83                                    warn!(
84                                        "Unsupported GFA version {version:?}, expected \"1.0\". Attempting to parse anyway, but this may lead to errors or a wrong graph.",
85                                    );
86                                }
87                            } else {
88                                warn!(
89                                    "GFA header line has unrecognized version information, expected \"VN:Z:1.0\", but got \"{}\"",
90                                    version
91                                );
92                            }
93                        } else {
94                            warn!(
95                                "GFA header line is missing version information, expected \"VN:Z:1.0\""
96                            );
97                        }
98                    } else {
99                        return Err(GfaReadError::WronglyPositionedHeader);
100                    }
101                }
102
103                "S" => {
104                    let name = line
105                        .get(1)
106                        .ok_or(GfaReadError::MissingSequenceNameInSLine)?
107                        .to_string();
108                    let sequence = line.get(2).unwrap_or(&"").to_string();
109                    let node = nodes.push(
110                        PlainGfaNodeData {
111                            name: name.clone(),
112                            sequence,
113                        }
114                        .into(),
115                    );
116                    node_name_to_node.insert(name.clone(), node);
117                }
118
119                "L" => {
120                    // Parse edge line.
121                    let from = line.get(1).ok_or(GfaReadError::LLineTooShort)?.to_string();
122                    let from_forward = match *line.get(2).ok_or(GfaReadError::LLineTooShort)? {
123                        "+" => true,
124                        "-" => false,
125                        other => return Err(GfaReadError::UnknownGfaNodeSign(other.to_string())),
126                    };
127                    let to = line.get(3).ok_or(GfaReadError::LLineTooShort)?.to_string();
128                    let to_forward = match *line.get(4).ok_or(GfaReadError::LLineTooShort)? {
129                        "+" => true,
130                        "-" => false,
131                        other => return Err(GfaReadError::UnknownGfaNodeSign(other.to_string())),
132                    };
133                    let overlap_str = line.get(5).unwrap_or(&"0M");
134                    let overlap = overlap_str
135                        .trim_end_matches('M')
136                        .parse::<u16>()
137                        .unwrap_or(0);
138
139                    edges.push(UnresolvedBidirectedEdge {
140                        from,
141                        from_forward,
142                        to,
143                        to_forward,
144                        data: PlainGfaEdgeData { overlap },
145                    });
146                }
147
148                other => {
149                    if unsupported_line_types.insert(other.to_string()) {
150                        warn!(
151                            "Unsupported GFA line type: {other:?}. Subsequent lines of this type will be ignored.",
152                        );
153                    }
154                }
155            }
156
157            is_header_allowed = false;
158        }
159
160        let edges = edges
161            .into_values_iter()
162            .map(|edge| {
163                let from = node_name_to_node
164                    .get(&edge.from)
165                    .copied()
166                    .ok_or(GfaReadError::UnknownNodeName(edge.from))?;
167                let to = node_name_to_node
168                    .get(&edge.to)
169                    .copied()
170                    .ok_or(GfaReadError::UnknownNodeName(edge.to))?;
171
172                let from_forward = edge.from_forward;
173                let to_forward = edge.to_forward;
174                let data = EdgeData::from(edge.data);
175
176                Result::<_, GfaReadError>::Ok(BidirectedEdge {
177                    from,
178                    from_forward,
179                    to,
180                    to_forward,
181                    data,
182                })
183            })
184            .collect::<Result<Vec<_>, GfaReadError>>()?;
185
186        // Drop name index before constructing graph to save RAM.
187        drop(node_name_to_node);
188        Ok(BidirectedAdjacencyArray::new(nodes, edges.into()))
189    }
190}
191
192impl<IndexType: GraphIndexInteger, NodeData: GfaNodeData, EdgeData: GfaEdgeData>
193    BidirectedAdjacencyArray<IndexType, NodeData, EdgeData>
194{
195    pub fn write_gfa1(&self, mut writer: impl Write) -> Result<(), std::io::Error> {
196        // Write header.
197        writeln!(writer, "H\tVN:Z:1.0")?;
198
199        // Write nodes.
200        for node in self.iter_nodes() {
201            let node_data = self.node_data(node);
202            writeln!(writer, "S\t{}\t{}", node_data.name(), node_data.sequence())?;
203        }
204
205        // Write edges.
206        for edge in self.iter_edges() {
207            let edge_data = self.edge(edge);
208
209            let from_node_name = self.node_data(edge_data.from().into_bidirected()).name();
210            let to_node_name = self.node_data(edge_data.to().into_bidirected()).name();
211
212            // In mathematical notation, traversing an edge from a to b means using edge (a, \hat{b}).
213            // But in GFA1, this means using edge (a, b), where both signs are unchanged.
214            let from_node_sign = if edge_data.from().is_forward() {
215                "+"
216            } else {
217                "-"
218            };
219            let to_node_sign = if edge_data.to().is_forward() {
220                "+"
221            } else {
222                "-"
223            };
224
225            let overlap = edge_data.data().overlap();
226
227            writeln!(
228                writer,
229                "L\t{from_node_name}\t{from_node_sign}\t{to_node_name}\t{to_node_sign}\t{overlap}M",
230            )?;
231        }
232
233        Ok(())
234    }
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
238pub struct PlainGfaNodeData {
239    name: String,
240    sequence: String,
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
244pub struct PlainGfaEdgeData {
245    overlap: u16,
246}
247
248impl PlainGfaNodeData {
249    pub fn new(name: impl ToString, sequence: impl ToString) -> Self {
250        Self {
251            name: name.to_string(),
252            sequence: sequence.to_string(),
253        }
254    }
255}
256
257impl GfaNodeData for PlainGfaNodeData {
258    fn name(&'_ self) -> Cow<'_, str> {
259        Cow::Borrowed(&self.name)
260    }
261
262    fn sequence(&'_ self) -> Cow<'_, str> {
263        Cow::Borrowed(&self.sequence)
264    }
265}
266
267impl PlainGfaEdgeData {
268    pub fn new(overlap: u16) -> Self {
269        Self { overlap }
270    }
271}
272
273impl GfaEdgeData for PlainGfaEdgeData {
274    fn overlap(&self) -> u16 {
275        self.overlap
276    }
277}