Skip to main content

document_svg/document/
sedml.rs

1//! Bounded SED-ML simulation-experiment-description previews.
2//!
3//! SED-ML describes how models and simulations are combined. This adapter
4//! reports experiment structure only; model changes, MathML, algorithm URIs,
5//! external model files and simulation engines remain inert.
6
7use std::path::Path;
8
9use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
10use crate::document::html::{HtmlBlock, render_blocks_to_pages};
11use crate::error::{Error, Result};
12use crate::geospatial::xml_tree::{XmlElement, XmlLimits, parse_xml_tree};
13use crate::table::{TableAlign, TableData};
14
15const MAX_SEDML_BYTES: u64 = 128 * 1024 * 1024;
16const MAX_SEDML_EVENTS: usize = 1_000_000;
17const MAX_SEDML_NODES: usize = 500_000;
18const MAX_SEDML_DEPTH: usize = 128;
19const MAX_SEDML_TEXT_BYTES: usize = 32 * 1024 * 1024;
20const MAX_SEDML_ROWS: usize = 200_000;
21const MAX_SEDML_DISPLAY_BYTES: usize = 512;
22
23#[derive(Default)]
24struct Summary {
25    level: String,
26    version: String,
27    models: usize,
28    simulations: usize,
29    tasks: usize,
30    repeated_tasks: usize,
31    data_descriptions: usize,
32    data_generators: usize,
33    outputs: usize,
34    plots: usize,
35    reports: usize,
36    changes: usize,
37    ranges: usize,
38    variables: usize,
39    parameters: usize,
40    algorithms: usize,
41    rows: Vec<Vec<String>>,
42}
43
44struct SedmlPageSink<'a> {
45    inner: &'a mut dyn PageConsumer,
46    warnings: &'a [String],
47}
48
49impl PageConsumer for SedmlPageSink<'_> {
50    fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
51        page.source_format = "sedml".into();
52        if page.title.is_empty() {
53            page.title = "SED-ML experiment".into();
54        }
55        page.description =
56            "SED-ML experiment structure is rendered as bounded inert metadata; models, algorithms and simulations are not executed".into();
57        for warning in self.warnings {
58            page.warn(warning.clone());
59        }
60        self.inner.consume(page)
61    }
62}
63
64pub(crate) fn looks_like_prefix(prefix: &[u8]) -> bool {
65    crate::geospatial::xml_tree::looks_like_root(prefix, b"sedML", None)
66        && String::from_utf8_lossy(prefix)
67            .to_ascii_lowercase()
68            .contains("sed-ml.org/sed-ml")
69}
70
71pub(crate) fn convert(
72    path: &Path,
73    options: &ConvertOptions,
74    sink: &mut dyn PageConsumer,
75) -> Result<Vec<String>> {
76    let bytes = read_limited_file(
77        path,
78        options.max_input_bytes.min(MAX_SEDML_BYTES),
79        "SED-ML input",
80    )?;
81    let root = parse_xml_tree(
82        &bytes,
83        &XmlLimits {
84            max_events: options.max_xml_events.min(MAX_SEDML_EVENTS),
85            max_nodes: MAX_SEDML_NODES,
86            max_depth: MAX_SEDML_DEPTH,
87            max_text_bytes: MAX_SEDML_TEXT_BYTES,
88        },
89        "SED-ML",
90    )?;
91    if !root.name.eq_ignore_ascii_case("sedML") {
92        return Err(Error::InvalidInput("SED-ML root must be <sedML>".into()));
93    }
94    if root
95        .namespace
96        .as_deref()
97        .is_none_or(|namespace| !namespace.to_ascii_lowercase().contains("sed-ml.org/sed-ml"))
98    {
99        return Err(Error::InvalidInput(
100            "SED-ML root uses an unsupported namespace".into(),
101        ));
102    }
103    let mut summary = Summary {
104        level: attr_local(&root, "level").map(truncate).unwrap_or_default(),
105        version: attr_local(&root, "version")
106            .map(truncate)
107            .unwrap_or_default(),
108        models: count_named(&root, "model"),
109        simulations: count_named(&root, "simulation")
110            + count_named(&root, "uniformTimeCourse")
111            + count_named(&root, "oneStep")
112            + count_named(&root, "steadyState")
113            + count_named(&root, "uniformTimeCourseSimulation"),
114        tasks: count_named(&root, "task"),
115        repeated_tasks: count_named(&root, "repeatedTask"),
116        data_descriptions: count_named(&root, "dataDescription"),
117        data_generators: count_named(&root, "dataGenerator"),
118        outputs: count_named(&root, "output"),
119        plots: count_named(&root, "plot2D") + count_named(&root, "plot3D"),
120        reports: count_named(&root, "report"),
121        changes: count_named(&root, "changeAttribute")
122            + count_named(&root, "computeChange")
123            + count_named(&root, "addXML")
124            + count_named(&root, "removeXML")
125            + count_named(&root, "changeXML"),
126        ranges: count_named(&root, "uniformRange")
127            + count_named(&root, "vectorRange")
128            + count_named(&root, "functionalRange"),
129        variables: count_named(&root, "variable"),
130        parameters: count_named(&root, "parameter"),
131        algorithms: count_named(&root, "algorithm"),
132        ..Summary::default()
133    };
134    push_row(
135        &mut summary.rows,
136        "Experiment",
137        &format!(
138            "level={} version={}",
139            display_or_dash(&summary.level),
140            display_or_dash(&summary.version)
141        ),
142        &format!(
143            "models={} simulations={}",
144            summary.models, summary.simulations
145        ),
146    )?;
147    push_row(
148        &mut summary.rows,
149        "Tasks",
150        &summary.tasks.to_string(),
151        &format!(
152            "repeatedTasks={} changes={} ranges={}",
153            summary.repeated_tasks, summary.changes, summary.ranges
154        ),
155    )?;
156    push_row(
157        &mut summary.rows,
158        "Analysis",
159        &summary.data_generators.to_string(),
160        &format!(
161            "dataDescriptions={} variables={} parameters={}",
162            summary.data_descriptions, summary.variables, summary.parameters
163        ),
164    )?;
165    push_row(
166        &mut summary.rows,
167        "Outputs",
168        &summary.outputs.to_string(),
169        &format!(
170            "plots={} reports={} algorithms={}",
171            summary.plots, summary.reports, summary.algorithms
172        ),
173    )?;
174    let blocks = vec![
175        HtmlBlock::Heading {
176            level: 1,
177            text: "SED-ML experiment".into(),
178        },
179        HtmlBlock::Paragraph {
180            text: "Simulation Experiment Description Markup Language structure is summarized without executing models, changes, algorithms or simulations.".into(),
181        },
182        HtmlBlock::Table(TableData {
183            headers: vec!["Kind".into(), "Value".into(), "Detail".into()],
184            rows: summary.rows,
185            alignments: vec![TableAlign::Left; 3],
186            raw_source: String::new(),
187        }),
188    ];
189    let warnings = vec![
190        "SED-ML IDs, model paths, algorithm URIs, MathML, variable/parameter values, XPath changes and output data are omitted or redacted".into(),
191        "SED-ML model imports, external resources, numerical solvers, plotting, data processing and simulation execution never run".into(),
192    ];
193    let mut page_sink = SedmlPageSink {
194        inner: sink,
195        warnings: &warnings,
196    };
197    render_blocks_to_pages(&blocks, &mut page_sink, options)?;
198    Ok(warnings)
199}
200
201fn count_named(element: &XmlElement, name: &str) -> usize {
202    element
203        .children
204        .iter()
205        .map(|child| usize::from(child.name.eq_ignore_ascii_case(name)) + count_named(child, name))
206        .sum()
207}
208
209fn attr_local<'a>(element: &'a XmlElement, name: &str) -> Option<&'a str> {
210    element.attributes.iter().find_map(|(key, value)| {
211        key.rsplit(':')
212            .next()
213            .filter(|local| local.eq_ignore_ascii_case(name))
214            .map(|_| value.as_str())
215    })
216}
217
218fn display_or_dash(value: &str) -> &str {
219    if value.is_empty() { "—" } else { value }
220}
221
222fn push_row(rows: &mut Vec<Vec<String>>, kind: &str, value: &str, detail: &str) -> Result<()> {
223    if rows.len() >= MAX_SEDML_ROWS {
224        return Err(Error::LimitExceeded(format!(
225            "SED-ML rows exceed {MAX_SEDML_ROWS}"
226        )));
227    }
228    rows.push(vec![truncate(kind), truncate(value), truncate(detail)]);
229    Ok(())
230}
231
232fn truncate(value: &str) -> String {
233    if value.len() <= MAX_SEDML_DISPLAY_BYTES {
234        value.to_owned()
235    } else {
236        let mut end = MAX_SEDML_DISPLAY_BYTES;
237        while end > 0 && !value.is_char_boundary(end) {
238            end -= 1;
239        }
240        format!("{}…", &value[..end])
241    }
242}