nml2/
acc.rs

1use crate::{
2    error::{Error, Result},
3    expr::Expr,
4    expr::Quantity,
5    lems::file::LemsFile,
6    neuroml::{
7        process_files,
8        raw::{
9            BiophysicalProperties, BiophysicalPropertiesBody, ChannelDensity, ChannelDensityNernst,
10            ChannelDensityNonUniform, ChannelDensityNonUniformNernst, ExtracellularProperties,
11            InhomogeneousParameter, InitMembPotential, IntracellularProperties,
12            IntracellularPropertiesBody, MembraneProperties, MembranePropertiesBody, Resistivity,
13            Species, SpecificCapacitance, VariableParameter,
14        },
15    },
16    nml2_error,
17    xml::XML,
18    Map,
19};
20
21use std::fmt::Write as _;
22use std::fs::write;
23use std::path::PathBuf;
24use tracing::{info, trace, warn};
25
26#[derive(Debug, Default, PartialEq, PartialOrd)]
27pub struct Cell {
28    pub decor: Vec<Decor>,
29    pub spike_threshold: Option<f64>,
30}
31
32impl Cell {
33    fn append(&mut self, rhs: &Self) -> Result<()> {
34        if self.spike_threshold.is_some() {
35            if self.spike_threshold.is_some() && self.spike_threshold != rhs.spike_threshold {
36                return Err(nml2_error!("Duplicated, mismatching spike threshold."));
37            }
38        } else {
39            self.spike_threshold = rhs.spike_threshold;
40        }
41        self.decor.extend_from_slice(&rhs.decor[..]);
42        Ok(())
43    }
44}
45
46pub fn to_cell_list(lems: &LemsFile, nml: &[String], ions: &[String]) -> Result<Map<String, Cell>> {
47    let mut cells = Map::new();
48    process_files(nml, |_, node| {
49        if node.tag_name().name() == "cell" {
50            if let Some(id) = node.attribute("id") {
51                let mut cell: Cell = Default::default();
52                let inhomogeneous_parameters = parse_inhomogeneous_parameters(node)?;
53                for bpp in node.children() {
54                    if bpp.tag_name().name() == "biophysicalProperties" {
55                        let tmp = &mut biophys(
56                            &XML::from_node(&bpp),
57                            lems,
58                            ions,
59                            &inhomogeneous_parameters,
60                        )?;
61                        cell.append(tmp)?;
62                    }
63                }
64                *cells.entry(id.to_string()).or_default() = cell;
65            }
66        }
67        Ok(())
68    })?;
69    Ok(cells)
70}
71
72pub fn parse_inhomogeneous_parameters(
73    cell: &roxmltree::Node<'_, '_>,
74) -> Result<Map<String, ParsedInhomogeneousParameter>> {
75    let mut inhomogeneous_parameters = Map::new();
76    let Some(m) = cell.children().find(|n| n.has_tag_name("morphology")) else {
77        return Ok(inhomogeneous_parameters);
78    };
79
80    for ihp in m
81        .children()
82        .filter(|n| n.has_tag_name("segmentGroup"))
83        .flat_map(|n| n.children())
84        .filter(|n| n.has_tag_name("inhomogeneousParameter"))
85    {
86        if let Some(segment_group_id) = ihp.parent().and_then(|p| p.attribute("id")) {
87            use crate::neuroml::raw::InhomogeneousParameterBody::*;
88            use crate::neuroml::raw::{DistalDetails, ProximalDetails};
89            let ihp: InhomogeneousParameter = XML::from_node(&ihp);
90            if ihp.metric != "Path Length from root" {
91                return Err(nml2_error!(
92                    "Only path length from root is supported as inhomogeneous parameter metric"
93                ));
94            }
95            let mut subtract_the_minimum = false;
96            for elem in ihp.body {
97                match elem {
98                    proximal(ProximalDetails { translationStart }) => {
99                        if translationStart == 0.0 {
100                            subtract_the_minimum = true;
101                        } else {
102                            return Err(acc_unimplemented(
103                                "Proximal translationStart must be 0 in InhomogeneousParameter",
104                            ));
105                        }
106                    }
107                    distal(DistalDetails { .. }) => return Err(acc_unimplemented(
108                        "Endpoint normalization for inhomogeneous parameters is not yet supported",
109                    )),
110                }
111            }
112            let metric = if subtract_the_minimum {
113                Expr::ProximalDistanceFromRegion(segment_group_id.to_string())
114            } else {
115                Expr::DistanceFromRoot()
116            };
117
118            inhomogeneous_parameters.insert(
119                ihp.id,
120                ParsedInhomogeneousParameter {
121                    variable: ihp.variable,
122                    region: segment_group_id.to_string(),
123                    metric,
124                },
125            );
126        } else {
127            return Err(acc_unimplemented(
128                "inhomogeneousParameter definition must be inside segmentGroup group with id",
129            ));
130        }
131    }
132    Ok(inhomogeneous_parameters)
133}
134
135pub fn export(
136    lems: &LemsFile,
137    nml: &[String],
138    pfx: &str,
139    ions: &[String],
140    cat_prefix: &str,
141) -> Result<()> {
142    trace!("Creating path {}", pfx);
143    std::fs::create_dir_all(pfx)?;
144    let cells = to_cell_list(lems, nml, ions)?;
145    for (name, cell) in cells {
146        let mut file = PathBuf::from(pfx);
147        file.push(name);
148        file.set_extension("acc");
149        info!("Writing ACC to {:?}", &file);
150        let decor = cell
151            .decor
152            .iter()
153            .map(|d| d.add_catalogue_prefix(cat_prefix))
154            .collect::<Vec<_>>();
155        write(&file, decor.to_sexp())?;
156    }
157    Ok(())
158}
159
160fn acc_unimplemented(f: &str) -> Error {
161    Error::Acc {
162        what: format!("Feature '{f}' not implemented for ACC export."),
163    }
164}
165
166pub trait Sexp {
167    fn to_sexp(&self) -> String;
168}
169
170#[derive(Clone, Debug, PartialEq, PartialOrd)]
171pub struct ParsedInhomogeneousParameter {
172    variable: String, // p
173    region: String,   // apicalDends
174    metric: Expr,
175}
176
177#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
178pub struct MechVariableParameter {
179    value: String, // 10 * p
180}
181
182#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
183pub enum Paintable {
184    Xi(String, String),
185    Xo(String, String),
186    Ra(String),
187    Vm(String),
188    Cm(String),
189    Er(String, String),
190    Em(String, String),
191    Mech(String, Map<String, String>),
192    NonUniformMech {
193        name: String,
194        ps: Map<String, String>,
195        ns: Map<String, MechVariableParameter>,
196    },
197}
198
199impl Paintable {
200    fn normalise(&self, lems: &LemsFile) -> Result<Self> {
201        let norm = |v: &str| -> Result<String> {
202            let q = Quantity::parse(v)?;
203            let u = lems.normalise_quantity(&q)?;
204            Ok(format!("{}", u.value))
205        };
206        let norm_map = |ps: &Map<String, String>| -> Result<Map<String, String>> {
207            ps.iter()
208                .map(|(k, v)| norm(v).map(|v| (k.to_string(), v)))
209                .collect::<Result<_>>()
210        };
211        let r = match self {
212            Paintable::Xi(i, v) => Paintable::Xi(i.clone(), norm(v)?),
213            Paintable::Xo(i, v) => Paintable::Xo(i.clone(), norm(v)?),
214            Paintable::Ra(v) => Paintable::Ra(norm(v)?),
215            Paintable::Vm(v) => Paintable::Vm(norm(v)?),
216            Paintable::Cm(v) => Paintable::Cm(norm(v)?),
217            Paintable::Er(i, v) => Paintable::Er(i.clone(), norm(v)?),
218            Paintable::Em(i, m) if m == "nernst" => Paintable::Em(i.clone(), m.clone()),
219            Paintable::Em(i, m) => Paintable::Em(i.clone(), norm(m)?),
220            Paintable::Mech(m, ps) => Paintable::Mech(m.clone(), norm_map(ps)?),
221            Paintable::NonUniformMech { name: m, ps, ns } => Paintable::NonUniformMech {
222                name: m.clone(),
223                ps: norm_map(ps)?,
224                ns: ns.clone(),
225            },
226        };
227        Ok(r)
228    }
229}
230
231impl Sexp for Expr {
232    fn to_sexp(&self) -> String {
233        fn op_to_sexp(op: &str, args: &[Expr]) -> String {
234            format!(
235                "({op} {})",
236                args.iter()
237                    .map(|x| x.to_sexp())
238                    .collect::<Vec<_>>()
239                    .join(" ")
240            )
241        }
242        match self {
243            Expr::F64(x) => format!("{x}"),
244            Expr::Var(x) => x.to_string(),
245            Expr::Add(x) => op_to_sexp("add", x),
246            Expr::Mul(x) => op_to_sexp("mul", x),
247            Expr::Pow(x) => op_to_sexp("pow", x),
248            Expr::Exp(x) => format!("(exp {})", x.to_sexp()),
249            Expr::Log(x) => format!("(log {})", x.to_sexp()),
250            Expr::Sqrt(x) => format!("(sqrt {})", x.to_sexp()),
251            Expr::Fun(nm, x) => {
252                format!("({} {})", if nm == "H" { "step" } else { nm }, x.to_sexp())
253            }
254            Expr::ProximalDistanceFromRegion(region) => {
255                format!("(proximal-distance (region \"{region}\"))")
256            }
257            Expr::DistanceFromRoot() => "(distance (root))".to_string(),
258        }
259    }
260}
261
262impl Sexp for String {
263    fn to_sexp(&self) -> String {
264        self.clone()
265    }
266}
267
268impl<K, V> Sexp for Map<K, V>
269where
270    K: Sexp,
271    V: Sexp,
272{
273    fn to_sexp(&self) -> String {
274        self.iter()
275            .map(|(k, v)| format!("(\"{}\" {})", k.to_sexp(), v.to_sexp()))
276            .collect::<Vec<_>>()
277            .join(" ")
278    }
279}
280
281impl Sexp for Paintable {
282    fn to_sexp(&self) -> String {
283        match self {
284            Paintable::Xi(i, v) => format!("(ion-internal-concentration \"{i}\" {v} (scalar 1))"),
285            Paintable::Xo(i, v) => format!("(ion-external-concentration \"{i}\" {v} (scalar 1))"),
286            Paintable::Er(i, v) => format!("(ion-reversal-potential \"{i}\" {v} (scalar 1))"),
287            Paintable::Em(i, v) => {
288                format!("(ion-reversal-potential-method \"{i}\" (mechanism \"{v}/{i}\"))")
289            }
290            Paintable::Ra(v) => format!("(axial-resistivity {v} (scalar 1))"),
291            Paintable::Vm(v) => format!("(membrane-potential {v} (scalar 1))"),
292            Paintable::Cm(v) => format!("(membrane-capacitance {v} (scalar 1))"),
293            Paintable::Mech(m, gs) => format!("(density (mechanism \"{m}\" {}))", gs.to_sexp()),
294            Paintable::NonUniformMech { name: m, ps, ns } => {
295                let ns = ns
296                    .iter()
297                    .map(|(k, v)| (k.to_string(), v.value.to_string()))
298                    .collect::<Map<String, String>>()
299                    .to_sexp();
300                format!(
301                    "(scaled-mechanism (density (mechanism \"{m}\" {})) {ns})",
302                    ps.to_sexp()
303                )
304            }
305        }
306    }
307}
308
309#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
310pub enum Decor {
311    Default(Paintable),
312    Paint(String, Paintable), // region -> what
313}
314
315impl Decor {
316    fn new(g: &str, p: Paintable, f: bool) -> Self {
317        if g.is_empty() || g == "all" {
318            if f {
319                Decor::Paint("all".to_string(), p)
320            } else {
321                Decor::Default(p)
322            }
323        } else {
324            Decor::Paint(g.to_string(), p)
325        }
326    }
327
328    pub fn add_catalogue_prefix(&self, pfx: &str) -> Self {
329        match self {
330            Self::Paint(r, Paintable::Mech(name, ps)) => Self::Paint(
331                r.clone(),
332                Paintable::Mech(format!("{pfx}{name}"), ps.clone()),
333            ),
334            Self::Paint(r, Paintable::NonUniformMech { name, ps, ns }) => Self::Paint(
335                r.clone(),
336                Paintable::NonUniformMech {
337                    name: format!("{pfx}{name}"),
338                    ps: ps.clone(),
339                    ns: ns.clone(),
340                },
341            ),
342            _ => self.clone(),
343        }
344    }
345
346    fn normalise(&self, lems: &LemsFile) -> Result<Self> {
347        match self {
348            Decor::Default(p) => Ok(Decor::Default(p.normalise(lems)?)),
349            Decor::Paint(r, p) => Ok(Decor::Paint(r.clone(), p.normalise(lems)?)),
350        }
351    }
352
353    pub fn vm(group: &str, value: &str) -> Self {
354        Decor::new(group, Paintable::Vm(value.to_string()), false)
355    }
356
357    pub fn xi(group: &str, ion: &str, value: &str) -> Self {
358        Decor::new(
359            group,
360            Paintable::Xi(ion.to_string(), value.to_string()),
361            false,
362        )
363    }
364
365    pub fn xo(group: &str, ion: &str, value: &str) -> Self {
366        Decor::new(
367            group,
368            Paintable::Xo(ion.to_string(), value.to_string()),
369            false,
370        )
371    }
372
373    pub fn ra(group: &str, value: &str) -> Self {
374        Decor::new(group, Paintable::Ra(value.to_string()), false)
375    }
376
377    pub fn er(group: &str, ion: &str, value: &str) -> Self {
378        Decor::new(
379            group,
380            Paintable::Er(ion.to_string(), value.to_string()),
381            false,
382        )
383    }
384
385    pub fn mechanism(group: &str, mech: &str, gs: &Map<String, String>) -> Self {
386        Decor::new(group, Paintable::Mech(mech.to_string(), gs.clone()), true)
387    }
388
389    pub fn nernst(ion: &str) -> Self {
390        Decor::new(
391            "",
392            Paintable::Em(ion.to_string(), "nernst".to_string()), // TODO This is poison
393            false,
394        )
395    }
396
397    pub fn non_uniform_mechanism(
398        segment_group: &str,
399        ion: &str,
400        gs: &Map<String, String>,
401        ns: &Map<String, MechVariableParameter>,
402    ) -> Self {
403        Decor::new(
404            segment_group,
405            Paintable::NonUniformMech {
406                name: ion.to_string(),
407                ps: gs.clone(),
408                ns: ns.clone(),
409            },
410            true,
411        )
412    }
413
414    pub fn cm(group: &str, value: &str) -> Self {
415        Decor::new(group, Paintable::Cm(value.to_string()), false)
416    }
417}
418
419impl Sexp for Decor {
420    fn to_sexp(&self) -> String {
421        match self {
422            Decor::Default(i) => format!("(default {})", i.to_sexp()),
423            Decor::Paint(r, i) => {
424                format!("(paint (region \"{r}\") {})", i.to_sexp())
425            }
426        }
427    }
428}
429
430impl Sexp for Vec<Decor> {
431    fn to_sexp(&self) -> String {
432        let mut result = String::from(
433            "(arbor-component
434  (meta-data (version \"0.9-dev\"))
435  (decor
436",
437        );
438        for it in self {
439            writeln!(result, "    {}", it.to_sexp()).unwrap();
440        }
441        result.pop();
442        result.push_str("))\n");
443        result
444    }
445}
446
447pub fn biophys(
448    prop: &BiophysicalProperties,
449    lems: &LemsFile,
450    ions: &[String],
451    inhomogeneous_parameters: &Map<String, ParsedInhomogeneousParameter>,
452) -> Result<Cell> {
453    use BiophysicalPropertiesBody::*;
454    let mut result: Cell = Default::default();
455    for item in &prop.body {
456        match item {
457            membraneProperties(m) => {
458                let cell = &mut membrane(m, ions, inhomogeneous_parameters)?;
459                result.append(cell)?;
460            }
461            intracellularProperties(i) => result.decor.append(&mut intra(i)?),
462            extracellularProperties(e) => result.decor.append(&mut extra(e)?),
463            property(_) | notes(_) | annotation(_) => {}
464        }
465    }
466    for it in result.decor.iter_mut() {
467        *it = it.normalise(lems)?;
468    }
469    Ok(result)
470}
471
472fn make_variable_parameter_map(
473    vp: &VariableParameter,
474    inhomogeneous_parameters: &Map<String, ParsedInhomogeneousParameter>,
475) -> Result<Map<String, MechVariableParameter>> {
476    use crate::neuroml::raw::VariableParameterBody::inhomogeneousValue;
477    if vp.body.len() != 1 {
478        return Err(acc_unimplemented(
479            "InhomogeneousValue must contain a single InhomogeneousParameter",
480        ));
481    }
482    let inhomogeneousValue(ival) = &vp.body.first().ok_or(nml2_error!(
483        "expected InhomogeneousValue in VariableParameter"
484    ))?;
485    let ihv = inhomogeneous_parameters
486        .get(&ival.inhomogeneousParameter)
487        .ok_or(nml2_error!(
488            "Inhomogeneous parameter definition {} not found",
489            ival.inhomogeneousParameter
490        ))?;
491
492    let parameter = rename_cond_density_to_conductance(&vp.parameter);
493
494    let expr = Expr::parse(&ival.value)?
495        .map(&|ex| -> _ {
496            if ex.is_var_with_name(&ihv.variable) {
497                ihv.metric.clone()
498            } else {
499                ex.clone()
500            }
501        })
502        .to_sexp();
503
504    let instance = MechVariableParameter { value: expr };
505
506    let ns = Map::from([(parameter, instance)]);
507    Ok(ns)
508}
509
510fn rename_cond_density_to_conductance(x: &str) -> String {
511    if x == "condDensity" {
512        String::from("conductance")
513    } else {
514        x.to_string()
515    }
516}
517
518fn membrane(
519    membrane: &MembraneProperties,
520    known_ions: &[String],
521    inhomogeneous_parameters: &Map<String, ParsedInhomogeneousParameter>,
522) -> Result<Cell> {
523    use MembranePropertiesBody::*;
524    let mut result: Cell = Default::default();
525    for item in &membrane.body {
526        match item {
527            channelDensity(ChannelDensity {
528                ionChannel,
529                condDensity,
530                erev,
531                segmentGroup,
532                ion,
533                body,
534                ..
535            }) => {
536                if !body.is_empty() {
537                    return Err(acc_unimplemented(
538                        "Non-empty body in ChannelDensityProperties",
539                    ));
540                }
541                let mut gs = simple_ion(known_ions, &mut result.decor, ion, segmentGroup, erev)?;
542                if let Some(g) = condDensity {
543                    gs.insert(String::from("conductance"), g.clone());
544                }
545                result
546                    .decor
547                    .push(Decor::mechanism(segmentGroup, ionChannel, &gs));
548            }
549            channelDensityNernst(ChannelDensityNernst {
550                ionChannel,
551                condDensity,
552                segmentGroup,
553                ion,
554                body,
555                ..
556            }) => {
557                if !body.is_empty() {
558                    return Err(acc_unimplemented("Non-empty body in ChannelDensityNernst"));
559                }
560                let gs = if let Some(g) = condDensity {
561                    Map::from([(String::from("conductance"), g.clone())])
562                } else {
563                    Map::new()
564                };
565                result
566                    .decor
567                    .push(Decor::mechanism(segmentGroup, ionChannel, &gs));
568                result.decor.push(Decor::nernst(ion));
569            }
570            channelDensityNonUniform(ChannelDensityNonUniform {
571                ionChannel,
572                ion,
573                body,
574                erev,
575                ..
576            }) => {
577                use crate::neuroml::raw::ChannelDensityNonUniformBody::variableParameter;
578                let variableParameter(vp) = &body.first().ok_or(nml2_error!(
579                    "expected VariableParameter in ChannelDensityNonUniform"
580                ))?;
581                let ns = make_variable_parameter_map(vp, inhomogeneous_parameters)?;
582                let ps = simple_ion(known_ions, &mut result.decor, ion, &vp.segmentGroup, erev)?;
583                result.decor.push(Decor::non_uniform_mechanism(
584                    &vp.segmentGroup,
585                    ionChannel,
586                    &ps,
587                    &ns,
588                ));
589            }
590            channelDensityNonUniformNernst(ChannelDensityNonUniformNernst {
591                ionChannel,
592                ion,
593                body,
594                ..
595            }) => {
596                use crate::neuroml::raw::ChannelDensityNonUniformNernstBody::variableParameter;
597                let variableParameter(vp) = &body.first().ok_or(nml2_error!(
598                    "expected VariableParameter in ChannelDensityNonUniformNernst"
599                ))?;
600                let ns = make_variable_parameter_map(vp, inhomogeneous_parameters)?;
601                result.decor.push(Decor::nernst(ion));
602                result.decor.push(Decor::non_uniform_mechanism(
603                    &vp.segmentGroup,
604                    ionChannel,
605                    &Map::new(),
606                    &ns,
607                ));
608            }
609            spikeThresh(v) => result.spike_threshold = Some(Quantity::parse(&v.value)?.value),
610            specificCapacitance(SpecificCapacitance {
611                value,
612                segmentGroup,
613            }) => result.decor.push(Decor::cm(segmentGroup, value)),
614            initMembPotential(InitMembPotential {
615                value,
616                segmentGroup,
617            }) => result.decor.push(Decor::vm(segmentGroup, value)),
618            channelPopulation(_)
619            | channelDensityVShift(_)
620            | channelDensityGHK(_)
621            | channelDensityGHK2(_)
622            | channelDensityNonUniformGHK(_) => {
623                return Err(acc_unimplemented("Complex channel type"))
624            }
625        }
626    }
627    Ok(result)
628}
629
630fn intra(intra: &IntracellularProperties) -> Result<Vec<Decor>> {
631    use IntracellularPropertiesBody::*;
632    let mut result = Vec::new();
633    for item in &intra.body {
634        match &item {
635            species(Species {
636                ion: Some(ion),
637                initialConcentration,
638                initialExtConcentration,
639                segmentGroup,
640                concentrationModel,
641                ..
642            }) => {
643                result.push(Decor::xi(segmentGroup, ion, initialConcentration));
644                result.push(Decor::xo(segmentGroup, ion, initialExtConcentration));
645                result.push(Decor::mechanism(
646                    segmentGroup,
647                    concentrationModel,
648                    &Map::from([(
649                        String::from("initialConcentration"),
650                        initialConcentration.to_string(),
651                    )]),
652                ))
653            }
654            resistivity(Resistivity {
655                value,
656                segmentGroup,
657            }) => result.push(Decor::ra(segmentGroup, value)),
658            _ => {}
659        }
660    }
661    Ok(result)
662}
663
664fn simple_ion(
665    known_ions: &[String],
666    result: &mut Vec<Decor>,
667    ion: &str,
668    group: &str,
669    erev: &str,
670) -> Result<Map<String, String>> {
671    if known_ions.contains(&ion.to_string()) {
672        for item in result.iter() {
673            if let Decor::Paint(r, Paintable::Er(i, e)) = item {
674                // We are trying to add the same key with a...
675                if r == group && ion == i {
676                    return if e == erev {
677                        // ... matching value: SKIP
678                        Ok(Default::default())
679                    } else {
680                        // ... mismatch: ERROR
681                        Err(nml2_error!("Overwriting different Er[{ion}] on {group}."))
682                    };
683                }
684            }
685        }
686        // New value: add it.
687        result.push(Decor::er(group, ion, erev));
688        Ok(Map::new())
689    } else {
690        Ok(Map::from([(
691            format!("e{}", if ion == "non_specific" { "" } else { ion }),
692            erev.to_string(),
693        )]))
694    }
695}
696
697fn extra(_: &ExtracellularProperties) -> Result<Vec<Decor>> {
698    warn!("Not handling extracellular settings, if required please file an issue.");
699    Ok(Vec::new())
700}
701
702#[test]
703fn test_simple_ion() {
704    let known_ions = ["k".to_string()];
705    let mut result = Vec::new();
706    assert!(simple_ion(&known_ions, &mut result, "k", "soma", "-80").is_ok());
707    assert!(result.len() == 1);
708    assert!(simple_ion(&known_ions, &mut result, "k", "soma", "-80").is_ok());
709    assert!(result.len() == 1);
710    assert!(simple_ion(&known_ions, &mut result, "k", "soma", "-90").is_err());
711    assert!(result.len() == 1);
712    assert!(simple_ion(&known_ions, &mut result, "k", "dend", "-90").is_ok());
713    assert!(result.len() == 2);
714    assert!(simple_ion(&known_ions, &mut result, "na", "soma", "-90")
715        .and_then(|gs| if gs.len() == 1 {
716            Ok(())
717        } else {
718            Err(nml2_error!("non_specific"))
719        })
720        .is_ok());
721    assert!(result.len() == 2);
722}