neurogears / onix-refactor

A project for refactoring Bonsai.ONIX
MIT License
2 stars 3 forks source link

Abstract device combinations #122

Open jonnew opened 6 days ago

jonnew commented 6 days ago

Some ONI devices a best treated as groups at the level of this library. A straightforward example are the two RHS2116 chips on headstage-rhs2116. These chips are synchronously sampled and transmitted at the level of headstage hardware. They go to a single connector, which is generally plugged into one probe. They are effectively a single 32-channel chip. Yet in this library, they are treated separately. This is completely fine for detailed manual configuration, but by the time the user is putting ConfigureHeadstageRhs2116 in their workflow, it would be best to expose only a single, virtual, 32-channel ConfigureRhs2116Array. Settings would be shared between the two chips using a bit of software muxing to select the underlaying ConfigureRhs2116 objects. A GroupedDeviceNameConverter would be needed create names of applicable groups of devices. Data would be combined in a non-stupid version of the following (which actually works so long as both chips are enabled):

using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using Bonsai;
using OpenCV.Net;

namespace OpenEphys.Onix
{
    public class Rhs2116DualData : Source<Rhs2116DataFrame>
    {

        private Rhs2116Data rhsA { get; set; } = new Rhs2116Data();
        private Rhs2116Data rhsB { get; set; } = new Rhs2116Data();

        [TypeConverter(typeof(Rhs2116.NameConverter))]
        public string DeviceName 
        { 
            get 
            { 
                return rhsA.DeviceName.Remove(rhsA.DeviceName.Length - 1); 
            } 
            set 
            { 
                rhsA.DeviceName = value.Remove(value.Length - 1) + 'A';
                rhsB.DeviceName = value.Remove(value.Length - 1) + 'B';
            } 
        }

        public int BufferSize
        {
            get
            {
                return rhsA.BufferSize;
            }
            set
            {
                rhsA.BufferSize = value;
                rhsB.BufferSize = value;
            }
        }

        public unsafe override IObservable<Rhs2116DataFrame> Generate()
        {
            var a = rhsA.Generate();
            var b = rhsB.Generate();

            return a.Zip(b, (lhs, rhs) =>
            {
                var ampMat = new Mat(2 * lhs.AmplifierData.Rows, lhs.AmplifierData.Cols, lhs.AmplifierData.Depth, lhs.AmplifierData.Channels);
                var subRect = ampMat.GetSubRect(new Rect(0, 0, lhs.AmplifierData.Cols, lhs.AmplifierData.Rows));
                CV.Copy(lhs.AmplifierData, subRect);

                subRect = ampMat.GetSubRect(new Rect(0, lhs.AmplifierData.Rows, lhs.AmplifierData.Cols, lhs.AmplifierData.Rows));
                CV.Copy(rhs.AmplifierData, subRect);

                var dcMat = new Mat(2 * lhs.DCData.Rows, lhs.DCData.Cols, lhs.DCData.Depth, lhs.DCData.Channels);
                subRect = dcMat.GetSubRect(new Rect(0, 0, lhs.AmplifierData.Cols, lhs.AmplifierData.Rows));
                CV.Copy(lhs.DCData, subRect);

                subRect = dcMat.GetSubRect(new Rect(0, lhs.AmplifierData.Rows, lhs.AmplifierData.Cols, lhs.AmplifierData.Rows));
                CV.Copy(rhs.DCData, subRect);

                return new Rhs2116DataFrame(lhs.Clock, lhs.HubClock, ampMat, dcMat);
            });

        }
    }
}
glopesdev commented 2 days ago

@jonnew I think this is already possible, using the exact same approach we used to register multiple devices going through the same passthrough link. Here is the sketch of a complete implementation of what it might look like for the Rhs2116Dual device, including how to handle the data nodes:

using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Bonsai;

namespace OpenEphys.Onix
{
    internal class ConfigureRhs2116Dual : SingleDeviceFactory
    {
        public ConfigureRhs2116Dual(Type deviceType)
            : base(deviceType)
        {
        }

        public override IObservable<ContextTask> Process(IObservable<ContextTask> source)
        {
            var deviceName = DeviceName;
            var deviceAddress = DeviceAddress;
            return source.ConfigureDevice(context =>
            {
                var rhs2116A = context.GetDeviceContext(deviceAddress + Rhs2116Dual.Rhs2116A, Rhs2116.ID);
                var rhs2116B = context.GetDeviceContext(deviceAddress + Rhs2116Dual.Rhs2116A, Rhs2116.ID);

                // configure stuff or assume they come preconfigured ...
                var deviceInfo = new Rhs2116DualDeviceInfo(
                    context,
                    rhs2116A,
                    rhs2116B,
                    typeof(Rhs2116Dual),
                    deviceAddress);
                return DeviceManager.RegisterDevice(deviceName, deviceInfo);
            });
        }
    }

    class Rhs2116DualDeviceInfo : DeviceInfo
    {
        public Rhs2116DualDeviceInfo(
            ContextTask context,
            DeviceContext rhs2116A,
            DeviceContext rhs2116B,
            Type deviceType,
            uint deviceAddress)
            : base(context, deviceType, deviceAddress)
        {
            Rhs2116A = rhs2116A;
            Rhs2116B = rhs2116B;
        }

        public DeviceContext Rhs2116A { get; }

        public DeviceContext Rhs2116B { get; }
    }

    static class Rhs2116Dual
    {
        public const int Rhs2116A = 0;
        public const int Rhs2116B = 1;

        internal class NameConverter : DeviceNameConverter
        {
            public NameConverter()
                : base(typeof(Rhs2116Dual))
            {
            }
        }
    }

    class Rhs2116DualData : Source<Rhs2116DualDataFrame>
    {
        [TypeConverter(typeof(Rhs2116Dual.NameConverter))]
        public string DeviceName { get; set; }

        public override IObservable<Rhs2116DualDataFrame> Generate()
        {
            return Observable.Using(
                () => DeviceManager.ReserveDevice(DeviceName),
                disposable => disposable.Subject.SelectMany(deviceInfo =>
                {
                    var dualInfo = (Rhs2116DualDeviceInfo)deviceInfo;
                    var rhs2116A = dualInfo.Rhs2116A;
                    var rhs2116B = dualInfo.Rhs2116B;

                    return Observable.Create<Rhs2116DualDataFrame>(observer =>
                    {
                        // Do whatever you want with the probes to get the dual data frame
                        return Disposable.Empty;
                    });
                }));
        }
    }

    class Rhs2116DualDataFrame
    {
        // TBD
    }
}

The cool thing about this approach is that the "Dual" is really its own device, down to the name selection drop-down and type validation. They go down together as if they were entirely a single device. Takes perhaps a bit of care to refactor things to avoid repetition of config, but should be doable with existing infrastructure.

jonnew commented 1 day ago

Very cool, thank you for the guide. Looks like exactly what I want. Ill leave this open until I give a shot at implementing when I return from vacation and hopefully close in a commit.