romainbrette / clampy

4 stars 0 forks source link

Acquisition API #14

Closed romainbrette closed 5 years ago

romainbrette commented 5 years ago

For the moment we have two systems:

  1. Board.acquire() does an acquisition and scales the signals appropriately.
  2. Axoclamp900A.acquire() (and Multiclamp) first identifies the signal names, sets the signals on the amplifier, then calls Board.acquire(). It also automatically sets the mode.

The reason for this is that the output channels of the amplifiers can convey various signals and gains, which are programmable. So there is no fixed wiring between say "Vm" and a channel on the board. One problem is that we might want to simultaneously acquire signals not on the amplifier (eg camera signal, another amplifier). In that case it seems that the initialization would need to be done before. I see two options:

  1. We do everything explicitly. eg Axoclamp900.set_output1('V1') ; Board.acquire('output1')
  2. The board has virtual channels with a link to the amplifier for initialization.

I like the virtual channel idea. The setup of the board would consist of setting up physical channels and virtual channels ('V1'). For example:

board.set_virtual_output('output1', channel=0, signals = ['V1','I1'], amplifier)

and the amplifier gets called when 'V1' or 'I1' is acquired.

Mode setting could perhaps be not automatic (current_clamp etc).

There's also the issue of settings gains.

romainbrette commented 5 years ago
  1. Imagine we want to use two amplifiers simultaneously. Those would be different objects. We cannot make a synchronous acquisition if the acquire() method is in those objects. It would seem better thus to start the acquisition from the board, which is in charge of coordinating the different devices. There could be also a camera trigger, or some other stuff. Thus we must choose board.acquire().

  2. We want to be able to run the same script, with possibly different init codes, on a Brian model. For this, we don't want to have code looking like board.acquire('output1'), with additional code to set the wiring of output1. So if possible the rewiring should be automated, so that board.acquire('V') automatically calls an amplifier method. This means that the board must be aware of the amplifier. The converse may not be necessary.

  3. Filters, gains, etc, could be put in the init code.

  4. The amplifier cannot have an acquire method, because we want a single global acquisition from the board. Rather, the amplifier must take care of setting the correct channel/signal wiring, and return the gains. It might also automatically take care of setting the correct acquisition mode.

romainbrette commented 5 years ago

Use example on the Axoclamp 900A (two channels):

V = board.acquire('V1', Ic1 = ...)
  1. Here Ic1 is a physical channel. But the gain is set by the amplifier. So the board should call the amplifier to get the gain. So we init something like this in the init code:

    board.set_analog_output('Ic1', channel=0, gain = amplifier.gain_Ic1)

and this could be a property, so that it can be dynamic. On some amplifiers, there are "gain telegraphs", which I think are BNC outputs with gain information, updated when the user turns the knobs. It would be possible to use the mechanism above, where the property calls a method which reads from the correct channel in the board. This however will not work because the function gain_Ic1 will only be called at init time! So an alternative could be to pass a function, and then it is called every time. We could then use something like this:

board.set_analog_output('Ic1', channel=0, gain = lambda: amplifier.gain['Ic1'])

or with a wrapper:

board.set_analog_output('Ic1', channel=0, gain = amplifier.gain_function('Ic1'))

Perhaps the method could detect whether the argument is a value or a callable object.

  1. Then V1 is a virtual channel. On the Axoclamp, both channels can carry the voltage of headstage 1. So before doing the acquisition, an Axoclamp channel must be rewired to carry the V1 signal. Thus, we need init code of the form:

    board.set_analog_input('output1', channel=0) board.set_analog_input('output2', channel=1) board.set_virtual_input('V1', channel = ('output1', 'output2'), select=amplifier.select_signal_V1)

When acquisition is started, the board selects the first available channel (here output1), then calls amplifier.select_signal_V1('output1'), which returns the gain.

Note that for this to work, the name 'output1' must be fixed by the amplifier. This could be a problem is, for example, there are two amplifiers with the same channel names. One possibility is this:

board.set_analog_input('output1', channel=0, device_ID=amplifier.output1) # or a string 'OUTPUT1'
romainbrette commented 5 years ago

Then there is the issue of automatic mode switching. We have two alternatives: either we let the user do this explicitly, or we add some automatic switching based on the acquisition keywords. A few issues to consider:

For now, I would suggest to simply not implement automatic mode switching.

romainbrette commented 5 years ago

Ok, let's go for:

board.set_analog_output('Ic1', channel=0, gain=some_number) # fixed gain
board.set_analog_output('Ic1', channel=0, device_ID=Axoclamp_Ic1, gain=amplifier.get_gain) # variable gain
board.set_analog_input('output1', channel=0, device_ID=Axoclamp_Ioutput1)

or something like that.

romainbrette commented 5 years ago

This is implemented now and seems to be working. I need to simplify a little bit.

romainbrette commented 5 years ago

One thing that could be added, perhaps, is aliases. So that for example 'V' could mean 'V1'. Alternatively, we could just add a channel with V, copying the information.

romainbrette commented 5 years ago

Also the Multiclamp class needs to be updated.

romainbrette commented 5 years ago

I added aliases and auto configuration of the virtual channels.