arbor
arbor copied to clipboard
Arbor Instant 🐛 🪦 Mode
Describe the feature you need Arbor Instant Bug Squash Mode: Turn on a flag, and run a dryrun of the model building phase, where errors like this are immediately raised when the instruction is given, so that a useful stack trace is obtained, and one can actually debug their model building code:
Traceback (most recent call last):
File "/users/bp000347/arbenv/bin/bsb", line 33, in <module>
sys.exit(load_entry_point('bsb', 'console_scripts', 'bsb')())
File "/users/bp000347/arbenv/bsb/bsb/cli.py", line 26, in scaffold_cli
start_cli()
File "/users/bp000347/arbenv/bsb/bsb/cli.py", line 255, in start_cli
scaffoldInstance.run_simulation(cl_args.simulation, quit=True)
File "/users/bp000347/arbenv/bsb/bsb/core.py", line 386, in run_simulation
simulation, simulator = self.prepare_simulation(simulation_name)
File "/users/bp000347/arbenv/bsb/bsb/core.py", line 419, in prepare_simulation
simulator = simulation.prepare()
File "/users/bp000347/arbenv/bsb/bsb/simulators/arbor/adapter.py", line 456, in prepare
simulation = arbor.simulation(recipe, self.domain, context)
RuntimeError: Model building error on cell 26: connection endpoint label "comp_0": label does not exist.
Explain what it is supposed to enable I don't like having to emulate a debugger :) I need to know where in my code this instruction was given, and how I got there.
Additional context 🐝
I think this is impossible given our current model building: For example, label resolution is performed during simulation construction.
Well the feature request is an alternative to actual model building, which'll check whether model building would fail, and raises the exception at the arbor.*
method that gave the instruction.
Actually also a good candidate for a separate package.
I have felt that frustration before, but I fear I see no way out of it.
That said, and to justify my 'ton of work' comment: simulation construction is when morphology, decor, connectivity, and (morphology) labels come together. I'd argue that it's almost the earliest point we can throw these errors. Let's go over the components one by one
- Morphology labels: we are allowing
Similar, we could construct expressions that are erroneous give a concrete morphology. What would you propose do here?d = label_dict({'foo': '(restrict (terminal) (region "bar"))'}) # Can we throw here? Maybe, but: d['bar'] = '(all)'
- Labels and decor: It's easy to see how
paint
/place
can access labels that do not exist. At this point, we have to merge morphology, labels, and decor. - Finally, connectivity. Needs a decor where the connection endpoints have been (correctly?) placed, thus morphology and labels. Worse, synaptic connections need remote labels. Now we are effectively merging all of the construction, making it synchronous across all nodes, and evaluate everything eagerly. Moreover, scripts that are valid in 'production' mode are now invalid in 'check' mode.
So, sadly I see no way of implementing your request without changing Arbor's way of doing things by 180º.
In check mode you tag all objects with the traces that placed the instructions on the object. Even for (3) this works, the conn endpoints are checked across nodes at some point.
You can raise, capture and store Exceptions when things like arbor.label_dict
(1), arbor.decor.paint
(2), or arbor.connection
(3) are called, to then reraise them when their instruction turns out to be involved in an error.
I'll see how far I get using wrapped objects, maybe I can provide a debug
module for something like import arbor.debug as arbor
;p My solution would have to parse error messages though, and be coupled to them.
solution(now) < ton_of_work
If doable, that would be pretty cool, go for it. It'll require tagging this
decor.place(..., ..., 'my_tåg')
connection(..., 'my_tag') # attach trace here silently.
in Python with a stacktrace (which you can get quite easily, I think) and then passing that through to C++ as a type erased thingy (technical term). This thingy™ would have to be attached to the thrown C++ exception and converted back into something useful in Python land before re-raising. Note that the exception is thrown in simulation construction by neccessity, as outlined above. Or do I misunderstand your proposal?
Yea, I'm thinking:
class PaintException(Exception):
pass
def trace(exc, *args, **kwargs):
try:
raise exc(args, kwargs)
except Exception as e:
return e
class TracedDecor(arbor.decor):
def place(self, location, mech, labels):
self.__dict__.setdefault(
"_mech_not_found",
defaultdict(list)
)[mech.name].append(
trace(PaintException, mech)
)
super().place(location, mech, labels)
def simulate(*args):
try:
arbor.simulate(*args)
except Exception as e:
parse_exception(e)
And in parse exception figure out which traced object we need to get, what property to look up for it and which offending trace to show. If we pass into CPP we could prevent having to parse the exception.
Ok, that seems quite brittle, eg if we change the exception wording everything will break?
My solution would have to parse error messages though, and be coupled to them.
Yes :[ That's the best I can do from Python, I'll make a PoC for 1 exception type, then we can think about a solution with CPP. Perhaps it can be enough that the arguments that are formatted into the error message are also present in a structured way as arguments on the exception so that I can use e.args[1:]
instead of having to parse anything, otherwise, you let me know what can be done from CPP.