arborize 7.2.2__tar.gz → 7.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arborize
3
- Version: 7.2.2
3
+ Version: 7.3.0
4
4
  Summary: Write descriptions for NEURON cell models in an Arbor-like manner for both the Arbor and
5
5
  Author-email: Robin De Schepper <robin@alexandria.sc>
6
6
  Requires-Python: >=3.10,<4
@@ -4,6 +4,7 @@ import typing
4
4
  import warnings
5
5
  from collections import deque
6
6
  from collections.abc import Mapping, Sequence
7
+ from functools import cache
7
8
 
8
9
  import errr
9
10
 
@@ -11,6 +12,7 @@ from .._util import get_arclengths, get_location_name
11
12
  from ..constraints import Constraint
12
13
  from ..definitions import CableProperties, Ion, Mechanism, MechId, mechdict
13
14
  from ..exceptions import (
15
+ ProxyWarning,
14
16
  TransmitterError,
15
17
  UnconnectedPointInSpaceWarning,
16
18
  UnknownLocationError,
@@ -213,7 +215,8 @@ def neuron_build(schematic: "Schematic"):
213
215
  # Connect the proxy to its parent
214
216
  _connect_section_or_proxy(section, branch_map.get(branch.parent))
215
217
 
216
- _collapse_proxies(proxies)
218
+ proxy_locations = _collapse_proxies(proxies, locations)
219
+ locations.update(proxy_locations)
217
220
 
218
221
  return NeuronModel(sections, locations, [*schematic.get_cable_types().keys()])
219
222
 
@@ -256,7 +259,9 @@ def _connect_section_or_proxy(section, parent):
256
259
  section.connect(parent)
257
260
 
258
261
 
259
- def _collapse_proxies(proxies: list[_SinglePointProxy]):
262
+ def _collapse_proxies(
263
+ proxies: list[_SinglePointProxy], real_locations: dict["Location", "LocationAccessor"]
264
+ ):
260
265
  """
261
266
  Removes proxies from the schematic and connects the real sections to each
262
267
  other.
@@ -273,11 +278,14 @@ def _collapse_proxies(proxies: list[_SinglePointProxy]):
273
278
  iteration and can be skipped.
274
279
  """
275
280
 
281
+ locations = {}
282
+
276
283
  for proxy in proxies:
277
284
  if proxy.reified:
278
285
  # This proxy has already been reified/collapsed in a previous
279
286
  # iteration as a downstream of another proxy.
280
287
  continue
288
+
281
289
  if not proxy.parent:
282
290
  # Root proxies
283
291
  if len(proxy.children) == 0:
@@ -305,13 +313,15 @@ def _collapse_proxies(proxies: list[_SinglePointProxy]):
305
313
  # eventually point to.
306
314
  true_children = _reify_proxy_children(proxy)
307
315
 
308
- first_child = true_children[0]
316
+ # The proxied electric location is the start of the first child
317
+ electric_proxy = (true_children[0], (0, 0))
318
+
309
319
  # We loop only over *additional* children, so that this
310
320
  # algorithm does nothing in the case of a single child
311
321
  # connected to a single point root proxy, where the root
312
322
  # proxy point can simply be discarded.
313
323
  for child in true_children[1:]:
314
- child.connect(first_child, 0)
324
+ child.connect(electric_proxy[0], 0)
315
325
  else:
316
326
  # Proxies with a parent, connect the proxy children to the parent.
317
327
 
@@ -323,11 +333,76 @@ def _collapse_proxies(proxies: list[_SinglePointProxy]):
323
333
  " Please report this bug."
324
334
  )
325
335
 
336
+ # The proxied electric location is the end of the proxy parent.
337
+ electric_proxy = (proxy.parent, (1, 1))
338
+
326
339
  # The children might be proxies themselves, so we reify them.
327
340
  true_children = _reify_proxy_children(proxy)
328
341
 
329
342
  for child in true_children:
330
- _connect_section_or_proxy(child, proxy.parent)
343
+ _connect_section_or_proxy(child, electric_proxy[0])
344
+
345
+ # The only branches of the control flow above that can reach this
346
+ # part of the code have exactly 1 point. Check defensively
347
+ if len(proxy.branch.points) != 1:
348
+ raise RuntimeError("Proxied location should have exactly one point.")
349
+
350
+ # Even though we've already figured out where to connect everything
351
+ # electrically, we now need to hand back a location accessor to the user
352
+ # that behaves as unsurprising as possible. We use this heuristic to try
353
+ # to find a good candidate that will hopefully serve users well.
354
+ proxied_loc, proxied_section, proxied_arcs = _guess_best_proxy_accessor(
355
+ proxy, true_children
356
+ )
357
+
358
+ loc = proxy.branch.points[0].loc
359
+ locations[loc] = ProxiedLocationAccessor(
360
+ loc,
361
+ proxy.branch.labels,
362
+ real_locations[proxied_loc],
363
+ proxied_section,
364
+ proxied_arcs,
365
+ )
366
+
367
+ return locations
368
+
369
+
370
+ def _guess_best_proxy_accessor(
371
+ proxy: _SinglePointProxy, true_children: list["Section"]
372
+ ) -> tuple["Location", "Section", tuple[float, float]]:
373
+ """
374
+ Simple heuristic to try to make proxied locations behave like normal. There
375
+ are 2 rules in order of importance:
376
+
377
+ * If a section has the same labels as the proxy means it has the same
378
+ cable properties and mechanisms and works for the most part the same.
379
+ * The tip of the parent is better than a child, because the tip of a branch
380
+ is already considered a 0 surface area point in the rest of Arborize.
381
+
382
+ This means in order of preference we have: a parent with the same labels, a
383
+ child with the same labels, a parent, or a child.
384
+ """
385
+ # Labels in Arborize are sorted according to their insertion order in the
386
+ # definitions, with the last appearing key considered "most specific" and
387
+ # having the final say. Since both Arborize and alphabetic ordering are
388
+ # deterministic and list equality in Python is order-sensitive, we sort
389
+ # the labels alphabetically here to be sure we don't miss any equalities.
390
+ proxy_labels = sorted(proxy.branch.labels)
391
+ if proxy.parent and sorted(proxy.parent.labels) == proxy_labels:
392
+ # Best case: Return the same labeled parent
393
+ return proxy.parent.locations[-1], proxy.parent, (1, 1)
394
+ else:
395
+ for child in true_children:
396
+ if sorted(child.labels) == proxy_labels:
397
+ # Not so best case: Return the same labeled child
398
+ return child.locations[0], child, (0, 0)
399
+ else:
400
+ if proxy.parent:
401
+ # Worse case: Return mislabeled parent
402
+ return proxy.parent.locations[-1], proxy.parent, (1, 1)
403
+ else:
404
+ # Worst case: Return mislabeled first child
405
+ return true_children[0].locations[0], true_children[0], (0, 0)
331
406
 
332
407
 
333
408
  def _reify_proxy_children(proxy):
@@ -409,6 +484,10 @@ class LocationAccessor:
409
484
  "Parameters cannot be changed yet after cell construction."
410
485
  )
411
486
 
487
+ @property
488
+ def location(self):
489
+ return self._loc
490
+
412
491
  @property
413
492
  def section(self):
414
493
  return self._section
@@ -420,3 +499,92 @@ class LocationAccessor:
420
499
  def arc(self, x=0):
421
500
  a0, a1 = self._arcs
422
501
  return (a1 - a0) * x + a0
502
+
503
+ def describe(self):
504
+ return f"{self._loc}[{','.join(self.section.labels)}]"
505
+
506
+
507
+ @cache
508
+ def _mechanisms(proxy_accessor):
509
+ # Cache function outside the Accessor class to avoid
510
+ # memory leaks, see B019
511
+ warnings.warn(
512
+ f"Location {proxy_accessor.describe()} was constructed from less "
513
+ "than 2 points and has 0 surface area, which NEURON does not "
514
+ f"support. Proxied location {proxy_accessor._proxied_loc.describe()} "
515
+ "mechanisms were used instead. Double-check you do not create "
516
+ "conflicting instructions.",
517
+ category=ProxyWarning,
518
+ stacklevel=1,
519
+ )
520
+ return proxymechdict(
521
+ proxy_accessor, proxy_accessor._proxied_loc, proxy_accessor._mechs
522
+ )
523
+
524
+
525
+ class ProxiedLocationAccessor(LocationAccessor):
526
+ def __init__(self, loc, labels, proxied_loc, section, arcs):
527
+ super().__init__(loc, section, proxied_loc.mechanisms, arcs)
528
+ self._labels = labels
529
+ self._proxied_loc = proxied_loc
530
+
531
+ @property
532
+ def proxied_loc(self):
533
+ return self._proxied_loc
534
+
535
+ @property
536
+ def section(self):
537
+ warnings.warn(
538
+ f"Location {self.describe()} was constructed from less "
539
+ "than 2 points and has 0 surface area, which NEURON does not "
540
+ f"support. Proxied location {self._proxied_loc.describe()} section "
541
+ "was used instead. Double-check you do not create conflicting "
542
+ "instructions.",
543
+ category=ProxyWarning,
544
+ stacklevel=1,
545
+ )
546
+ return self._section
547
+
548
+ @property
549
+ def mechanisms(self) -> Mapping["MechId", "MechAccessor"]:
550
+ return _mechanisms(self)
551
+
552
+ def describe(self):
553
+ return f"{self._loc}[{','.join(self._labels)}]"
554
+
555
+
556
+ class proxymechdict(mechdict):
557
+ def __init__(self, loc, proxied_loc, mechs):
558
+ self._loc = loc
559
+ self._proxied_loc = proxied_loc
560
+ super().__init__(mechs)
561
+
562
+ def __getitem__(self, item):
563
+ try:
564
+ return super().__getitem__(item)
565
+ except KeyError:
566
+ raise ProxyKeyError(
567
+ item,
568
+ (
569
+ f"Location {self._loc.describe()} proxies "
570
+ f"{self._proxied_loc.describe()}. Mechanism '{item}' not found "
571
+ "on proxied location."
572
+ ),
573
+ ) from None
574
+
575
+
576
+ class ProxyKeyError(KeyError):
577
+ """
578
+ KeyError with extra steps.
579
+
580
+ Key errors might be inspected to find out which key errored, but they
581
+ don't show much info, this does both.
582
+ """
583
+
584
+ def __init__(self, key, message):
585
+ self.key = key
586
+ self.message = message
587
+ super().__init__(key) # pass just the key to parent
588
+
589
+ def __str__(self):
590
+ return f"{self.key!r} - {self.message}"
@@ -32,3 +32,7 @@ class ArborizeWarning(Warning):
32
32
 
33
33
  class UnconnectedPointInSpaceWarning(ArborizeWarning):
34
34
  pass
35
+
36
+
37
+ class ProxyWarning(ArborizeWarning):
38
+ pass
@@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi"
6
6
 
7
7
  [project]
8
8
  name = "arborize"
9
- version = "7.2.2"
9
+ version = "7.3.0"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10,<4"
12
12
  dynamic = [ "description" ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes