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, region: String, metric: Expr,
175}
176
177#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
178pub struct MechVariableParameter {
179 value: String, }
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), }
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()), 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 if r == group && ion == i {
676 return if e == erev {
677 Ok(Default::default())
679 } else {
680 Err(nml2_error!("Overwriting different Er[{ion}] on {group}."))
682 };
683 }
684 }
685 }
686 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}