sfs_core/input/site/reader/
builder.rs

1//! Site reader builder.
2
3use std::{collections::HashSet, fmt, io, path::PathBuf};
4
5use sample::Sample;
6
7use crate::{
8    array::Shape,
9    input::{genotype, sample},
10    spectrum::project::{PartialProjection, ProjectionError},
11};
12
13/// A site reader builder.
14#[derive(Debug, Default)]
15pub struct Builder {
16    samples: Option<Option<Samples>>,
17    project: Option<Option<Project>>,
18}
19
20impl Builder {
21    /// Returns a new reader based on the provided genotype reader.
22    ///
23    /// # Errors
24    ///
25    /// For a variety of reasons, see [`Error`] for details.
26    pub fn build(self, reader: genotype::reader::DynReader) -> Result<super::Reader, Error> {
27        let sample_map = match self.samples.unwrap_or(None) {
28            Some(Samples::List(list)) => sample::Map::from_iter(list),
29            Some(Samples::Path(path)) => sample::Map::from_path(path)?,
30            None => sample::Map::from_all(reader.samples().iter().cloned()),
31        };
32
33        if sample_map.is_empty() {
34            return Err(Error::EmptySamplesMap);
35        }
36
37        // All samples in sample map should be in reader samples
38        let reader_samples = HashSet::<_>::from_iter(reader.samples());
39        if let Some(unknown_sample) = sample_map
40            .samples()
41            .find(|sample| !reader_samples.contains(sample))
42        {
43            return Err(Error::UnknownSample {
44                sample: unknown_sample.as_ref().to_string(),
45            });
46        }
47
48        let projection = if let Some(project_to) = self.project.unwrap_or(None).map(Project::shape)
49        {
50            let project_from = sample_map.shape();
51
52            if project_from.dimensions() != project_to.dimensions() {
53                return Err(ProjectionError::UnequalDimensions {
54                    from: project_from.dimensions(),
55                    to: project_to.dimensions(),
56                }
57                .into());
58            } else if let Some((dimension, (&from, &to))) = project_from
59                .iter()
60                .zip(project_to.iter())
61                .enumerate()
62                .find(|(_, (from, to))| from < to)
63            {
64                return Err(ProjectionError::InvalidProjection {
65                    dimension,
66                    from,
67                    to,
68                }
69                .into());
70            } else {
71                Some(PartialProjection::from_shape(project_to)?)
72            }
73        } else {
74            None
75        };
76
77        Ok(super::Reader::new_unchecked(reader, sample_map, projection))
78    }
79
80    /// Sets the projection used for reading.
81    ///
82    /// By default, no projection will be used.
83    pub fn set_project(mut self, project: Option<Project>) -> Self {
84        self.project = Some(project);
85        self
86    }
87
88    /// Sets the sample mapping used for reading.
89    ///
90    /// By default, all samples will be mapped to the same, unnamed population.
91    pub fn set_samples(mut self, samples: Option<Samples>) -> Self {
92        self.samples = Some(samples);
93        self
94    }
95}
96
97/// A source for a sample mapping.
98#[derive(Debug)]
99pub enum Samples {
100    /// A path to a samples file.
101    Path(PathBuf),
102    /// A list of samples and associated populations.
103    List(Vec<(Sample, sample::Population)>),
104}
105
106/// A projection specification.
107#[derive(Debug)]
108pub enum Project {
109    /// Project to specified number of individuals.
110    Individuals(Vec<usize>),
111    /// Project to specified shape.
112    Shape(Shape),
113}
114
115impl Project {
116    fn shape(self) -> Shape {
117        match self {
118            Project::Individuals(individuals) => {
119                Shape(individuals.into_iter().map(|i| 2 * i + 1).collect())
120            }
121            Project::Shape(shape) => shape,
122        }
123    }
124}
125
126/// An error associated with building a site reader.
127#[derive(Debug)]
128pub enum Error {
129    /// Provided sample mappping is empty.
130    EmptySamplesMap,
131    /// I/O error.
132    Io(io::Error),
133    /// A provided path does not exist.
134    PathDoesNotExist {
135        /// The provided path.
136        path: PathBuf,
137    },
138    /// A projection error.
139    Projection(ProjectionError),
140    /// Provided sample mapping defines a sample not defined by the genotype reader.
141    UnknownSample {
142        /// The unknown sample.
143        sample: String,
144    },
145}
146
147impl From<io::Error> for Error {
148    fn from(e: io::Error) -> Self {
149        Self::Io(e)
150    }
151}
152
153impl From<ProjectionError> for Error {
154    fn from(e: ProjectionError) -> Self {
155        Self::Projection(e)
156    }
157}
158
159impl fmt::Display for Error {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        match self {
162            Error::EmptySamplesMap => f.write_str("empty samples mapping"),
163            Error::Io(e) => write!(f, "{e}"),
164            Error::PathDoesNotExist { path } => {
165                write!(f, "path '{}' not found", path.display())
166            }
167            Error::UnknownSample { sample } => write!(f, "unknown sample {sample}"),
168            Error::Projection(e) => write!(f, "{e}"),
169        }
170    }
171}
172
173impl std::error::Error for Error {}