Skip to main content

dspk_synapse/provenance/
mod.rs

1//! Where every sorter and model comes from: the paper to cite, the upstream code and its license,
2//! and the files downloaded to run it. Exposed by every sorter and model through [`Attributed`],
3//! and stored in the catalog, so a result can always be traced to its authors.
4
5use serde::{Deserialize, Serialize};
6
7/// How a sorter or model relates to its upstream.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ProvenanceKind {
11    /// Algorithm written here from the paper (upstream code not ported).
12    ReimplementedFromPaper,
13    /// Algorithm written here from the paper with its own architecture (GPU kernels, batching),
14    /// its behaviour matched to upstream's code by reading it (named functions, defaults, stage
15    /// order); no code copied. Distributed under upstream's licence.
16    ReimplementedFromPaperAndCode,
17    /// Published weights / arrays run as released.
18    UpstreamWeights,
19    /// Algorithm ported from the upstream source (permissive license), stage by stage.
20    PortedFromCode,
21}
22
23/// The publication to cite.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct Paper {
26    pub title: String,
27    /// Family names in publication order (first author first).
28    pub authors: Vec<String>,
29    /// Journal or preprint server.
30    pub venue: String,
31    pub year: u16,
32    /// DOI without the `https://doi.org/` prefix (e.g. `10.1038/s41592-024-02232-7`).
33    pub doi: String,
34    /// License of the paper itself, when stated (e.g. `CC-BY-4.0`).
35    #[serde(default)]
36    pub license: Option<String>,
37}
38
39impl Paper {
40    /// `https://doi.org/<doi>`.
41    pub fn doi_url(&self) -> String {
42        format!("https://doi.org/{}", self.doi)
43    }
44}
45
46/// The upstream source code.
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct UpstreamCode {
49    pub url: String,
50    /// SPDX identifier, or `None` when the repository states none.
51    pub license: Option<String>,
52    /// Release tag or commit the work follows.
53    pub version: String,
54}
55
56/// A file downloaded to run the sorter or model.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct ArtifactSource {
59    /// File name as published (e.g. `wTEMP.npz`).
60    pub name: String,
61    /// Where it was downloaded from.
62    pub url: String,
63    /// Lowercase hex SHA-256 of the file, as checked when it was added.
64    pub sha256: String,
65    pub size_bytes: u64,
66    /// License of the file, when stated separately from the code.
67    #[serde(default)]
68    pub license: Option<String>,
69}
70
71/// Provenance of one sorter or model. See the module docs.
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct Provenance {
74    /// Name as its authors publish it (e.g. `Kilosort4`, `EMUsort`).
75    pub name: String,
76    pub kind: ProvenanceKind,
77    /// `None` when no paper could be verified.
78    pub paper: Option<Paper>,
79    pub code: UpstreamCode,
80    /// Files it needs (empty when it learns everything from the recording).
81    #[serde(default)]
82    pub artifacts: Vec<ArtifactSource>,
83    /// What differs from upstream, what is unverified.
84    #[serde(default)]
85    pub notes: String,
86}
87
88impl Provenance {
89    /// One-line reference: `Authors (year). Title. Venue. https://doi.org/…`, then the code.
90    pub fn citation(&self) -> String {
91        let code = match &self.code.license {
92            Some(l) => format!("Code: {} ({}, {l})", self.code.url, self.code.version),
93            None => format!("Code: {} ({})", self.code.url, self.code.version),
94        };
95        match &self.paper {
96            Some(p) => {
97                let authors = match p.authors.as_slice() {
98                    [] => String::new(),
99                    [one] => one.clone(),
100                    [first, .., last] if p.authors.len() > 2 => format!("{first} … {last}"),
101                    [first, second] => format!("{first} & {second}"),
102                    _ => unreachable!(),
103                };
104                format!("{authors} ({}). {}. {}. {}. {code}", p.year, p.title, p.venue, p.doi_url())
105            }
106            None => format!("{} (no verified paper). {code}", self.name),
107        }
108    }
109}
110
111/// Anything whose origin can be cited.
112pub trait Attributed {
113    fn provenance(&self) -> Provenance;
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn citation_names_paper_doi_and_code() {
122        let p = Provenance {
123            name: "Kilosort4".into(),
124            kind: ProvenanceKind::ReimplementedFromPaper,
125            paper: Some(Paper {
126                title: "Spike sorting with Kilosort4".into(),
127                authors: vec!["Pachitariu".into(), "Sridhar".into(), "Pennington".into(), "Stringer".into()],
128                venue: "Nature Methods".into(),
129                year: 2024,
130                doi: "10.1038/s41592-024-02232-7".into(),
131                license: None,
132            }),
133            code: UpstreamCode { url: "https://github.com/MouseLand/Kilosort".into(), license: Some("GPL-3.0".into()), version: "v4.1.3".into() },
134            artifacts: Vec::new(),
135            notes: String::new(),
136        };
137        let c = p.citation();
138        assert!(c.starts_with("Pachitariu … Stringer (2024). Spike sorting with Kilosort4. Nature Methods. https://doi.org/10.1038/s41592-024-02232-7."), "{c}");
139        assert!(c.ends_with("Code: https://github.com/MouseLand/Kilosort (v4.1.3, GPL-3.0)"), "{c}");
140    }
141}