diff --git a/OpenEphys.Onix1.Design/ChannelConfigurationDialog.cs b/OpenEphys.Onix1.Design/ChannelConfigurationDialog.cs index b69d801e..85d2749b 100644 --- a/OpenEphys.Onix1.Design/ChannelConfigurationDialog.cs +++ b/OpenEphys.Onix1.Design/ChannelConfigurationDialog.cs @@ -311,10 +311,8 @@ internal bool ValidateProbeGroup(ProbeGroup newConfiguration) } else { - MessageBox.Show($"Error: Number of contacts does not match; expected {ProbeGroup.NumberOfContacts} contacts" + - $", but found {newConfiguration.NumberOfContacts} contacts", "Contact Number Mismatch"); - - return false; + throw new InvalidOperationException($"Number of contacts does not match; expected {ProbeGroup.NumberOfContacts} contacts" + + $", but found {newConfiguration.NumberOfContacts} contacts. Ensure the file contains the correct probe group type."); } } diff --git a/OpenEphys.Onix1.Design/INeuropixelsV2ProbeInfo.cs b/OpenEphys.Onix1.Design/INeuropixelsV2ProbeInfo.cs index 19e436e6..8ac2c02c 100644 --- a/OpenEphys.Onix1.Design/INeuropixelsV2ProbeInfo.cs +++ b/OpenEphys.Onix1.Design/INeuropixelsV2ProbeInfo.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; namespace OpenEphys.Onix1.Design { interface INeuropixelsV2ProbeInfo { + public IEnumerable Electrodes { get; init; } + Array GetReferenceEnumValues(); Array GetComboBoxChannelPresets(); diff --git a/OpenEphys.Onix1.Design/NeuropixelsV2QuadShankInfo.cs b/OpenEphys.Onix1.Design/NeuropixelsV2QuadShankInfo.cs index 0d96496c..95a74b39 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV2QuadShankInfo.cs +++ b/OpenEphys.Onix1.Design/NeuropixelsV2QuadShankInfo.cs @@ -43,7 +43,7 @@ enum QuadShankChannelPreset None } - IEnumerable Electrodes { get; init; } + public IEnumerable Electrodes { get; init; } public NeuropixelsV2QuadShankInfo(NeuropixelsV2QuadShankProbeConfiguration probeConfiguration) { diff --git a/OpenEphys.Onix1.Design/NeuropixelsV2SingleShankInfo.cs b/OpenEphys.Onix1.Design/NeuropixelsV2SingleShankInfo.cs new file mode 100644 index 00000000..206e69dd --- /dev/null +++ b/OpenEphys.Onix1.Design/NeuropixelsV2SingleShankInfo.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace OpenEphys.Onix1.Design +{ + internal class NeuropixelsV2SingleShankInfo : INeuropixelsV2ProbeInfo + { + const int BankDStartIndex = 896; + + enum SingleShankChannelPreset + { + BankA, + BankB, + BankC, + BankD, + None + } + + public IEnumerable Electrodes { get; init; } + + public NeuropixelsV2SingleShankInfo(NeuropixelsV2SingleShankProbeConfiguration probeConfiguration) + { + Electrodes = probeConfiguration.ProbeGroup.ToElectrodes(); + } + + public Array GetReferenceEnumValues() + { + return Enum.GetValues(typeof(NeuropixelsV2SingleShankReference)); + } + + public Array GetComboBoxChannelPresets() + { + return Enum.GetValues(typeof(SingleShankChannelPreset)); + } + + public Enum CheckForExistingChannelPreset(NeuropixelsV2Electrode[] channelMap) + { + if (channelMap.All(e => e.Bank == NeuropixelsV2Bank.A)) + { + return SingleShankChannelPreset.BankA; + } + else if (channelMap.All(e => e.Bank == NeuropixelsV2Bank.B)) + { + return SingleShankChannelPreset.BankB; + } + else if (channelMap.All(e => e.Bank == NeuropixelsV2Bank.C)) + { + return SingleShankChannelPreset.BankC; + } + else if (channelMap.All(e => e.Bank == NeuropixelsV2Bank.D || + (e.Bank == NeuropixelsV2Bank.C && e.Index >= BankDStartIndex))) + { + return SingleShankChannelPreset.BankD; + } + else + { + return SingleShankChannelPreset.None; + } + } + + public NeuropixelsV2Electrode[] GetChannelPreset(Enum channelPreset) + { + var preset = (SingleShankChannelPreset)channelPreset; + + return preset switch + { + SingleShankChannelPreset.BankA => Electrodes.Where(e => e.Bank == NeuropixelsV2Bank.A).ToArray(), + SingleShankChannelPreset.BankB => Electrodes.Where(e => e.Bank == NeuropixelsV2Bank.B).ToArray(), + SingleShankChannelPreset.BankC => Electrodes.Where(e => e.Bank == NeuropixelsV2Bank.C).ToArray(), + SingleShankChannelPreset.BankD => Electrodes.Where(e => e.Bank == NeuropixelsV2Bank.D || (e.Bank == NeuropixelsV2Bank.C && e.Index >= BankDStartIndex)).ToArray(), + SingleShankChannelPreset.None => Array.Empty(), + _ => throw new InvalidEnumArgumentException($"Unknown value of {nameof(SingleShankChannelPreset)}: {channelPreset}") + }; + } + } +} diff --git a/OpenEphys.Onix1.Design/NeuropixelsV2eChannelConfigurationDialog.cs b/OpenEphys.Onix1.Design/NeuropixelsV2eChannelConfigurationDialog.cs index 7c93f8e1..20efd819 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV2eChannelConfigurationDialog.cs +++ b/OpenEphys.Onix1.Design/NeuropixelsV2eChannelConfigurationDialog.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Reflection; using System.Windows.Forms; using OpenEphys.ProbeInterface.NET; using ZedGraph; @@ -15,9 +14,21 @@ public partial class NeuropixelsV2eChannelConfigurationDialog : ChannelConfigura internal event EventHandler OnZoom; internal event EventHandler OnFileLoad; - internal NeuropixelsV2ProbeConfiguration ProbeConfiguration; + NeuropixelsV2ProbeConfiguration probeConfiguration; - readonly Func GetChannelNumberFunc; + internal NeuropixelsV2ProbeConfiguration ProbeConfiguration + { + get => probeConfiguration; + set + { + probeConfiguration = value; + ProbeGroup = value.ProbeGroup; + + GetChannelNumberFunc = ProbeConfiguration.GetChannelNumberFunc(); + } + } + + Func GetChannelNumberFunc { get; set; } /// /// Initializes a new instance of . @@ -32,9 +43,6 @@ public NeuropixelsV2eChannelConfigurationDialog(NeuropixelsV2ProbeConfiguration zedGraphChannels.ZoomStepFraction = 0.5; ProbeConfiguration = Activator.CreateInstance(probeConfiguration.GetType(), probeConfiguration) as NeuropixelsV2ProbeConfiguration; - ProbeConfiguration.ProbeGroup = (NeuropixelsV2eProbeGroup)ProbeGroup; - - GetChannelNumberFunc = ProbeConfiguration.ChannelMap[0].GetChannelNumberFunc(); HighlightEnabledContacts(); UpdateContactLabels(); diff --git a/OpenEphys.Onix1.Design/NeuropixelsV2eDialog.cs b/OpenEphys.Onix1.Design/NeuropixelsV2eDialog.cs index b80302d9..fcc68ba4 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV2eDialog.cs +++ b/OpenEphys.Onix1.Design/NeuropixelsV2eDialog.cs @@ -26,10 +26,13 @@ public NeuropixelsV2eDialog(IConfigureNeuropixelsV2 configureNode) InitializeComponent(); Shown += FormShown; + bool isBeta = false; + if (configureNode is ConfigureNeuropixelsV2eBeta configureV2eBeta) { ConfigureNode = new ConfigureNeuropixelsV2eBeta(configureV2eBeta); Text = Text.Replace("NeuropixelsV2e ", "NeuropixelsV2eBeta "); + isBeta = true; } else if (configureNode is ConfigureNeuropixelsV2e configureV2e) { @@ -38,7 +41,7 @@ public NeuropixelsV2eDialog(IConfigureNeuropixelsV2 configureNode) ProbeConfigurations = new List { - new(ConfigureNode.ProbeConfigurationA, ConfigureNode.GainCalibrationFileA, ConfigureNode.InvertPolarity) + new(ConfigureNode.ProbeConfigurationA, ConfigureNode.GainCalibrationFileA, ConfigureNode.InvertPolarity, isBeta) { TopLevel = false, FormBorderStyle = FormBorderStyle.None, @@ -46,7 +49,7 @@ public NeuropixelsV2eDialog(IConfigureNeuropixelsV2 configureNode) Parent = this, Tag = NeuropixelsV2Probe.ProbeA }, - new(ConfigureNode.ProbeConfigurationB, ConfigureNode.GainCalibrationFileB, ConfigureNode.InvertPolarity) + new(ConfigureNode.ProbeConfigurationB, ConfigureNode.GainCalibrationFileB, ConfigureNode.InvertPolarity, isBeta) { TopLevel = false, FormBorderStyle = FormBorderStyle.None, diff --git a/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationDialog.Designer.cs b/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationDialog.Designer.cs index ef693b4a..bc8ccb16 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationDialog.Designer.cs +++ b/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationDialog.Designer.cs @@ -34,6 +34,7 @@ private void InitializeComponent() System.Windows.Forms.Label labelPresets; System.Windows.Forms.Label label1; System.Windows.Forms.Label invertPolarity; + System.Windows.Forms.Label labelProbeType; System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(NeuropixelsV2eProbeConfigurationDialog)); this.toolStripLabelGainCalibrationSN = new System.Windows.Forms.ToolStripStatusLabel(); this.menuStrip = new System.Windows.Forms.MenuStrip(); @@ -57,11 +58,13 @@ private void InitializeComponent() this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel(); this.statusStrip1 = new System.Windows.Forms.StatusStrip(); this.toolStripGainCalSN = new System.Windows.Forms.ToolStripStatusLabel(); + this.comboBoxProbeType = new System.Windows.Forms.ComboBox(); probeCalibrationFile = new System.Windows.Forms.Label(); Reference = new System.Windows.Forms.Label(); labelPresets = new System.Windows.Forms.Label(); label1 = new System.Windows.Forms.Label(); invertPolarity = new System.Windows.Forms.Label(); + labelProbeType = new System.Windows.Forms.Label(); this.menuStrip.SuspendLayout(); this.panelTrackBar.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.trackBarProbePosition)).BeginInit(); @@ -148,11 +151,11 @@ private void InitializeComponent() // this.buttonEnableContacts.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.buttonEnableContacts.Location = new System.Drawing.Point(15, 232); + this.buttonEnableContacts.Location = new System.Drawing.Point(15, 264); this.buttonEnableContacts.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); this.buttonEnableContacts.Name = "buttonEnableContacts"; this.buttonEnableContacts.Size = new System.Drawing.Size(280, 44); - this.buttonEnableContacts.TabIndex = 11; + this.buttonEnableContacts.TabIndex = 13; this.buttonEnableContacts.Text = "Enable Selected Electrodes"; this.toolTip.SetToolTip(this.buttonEnableContacts, "Click and drag to select electrodes in the probe view. \r\nPress this button to ena" + "ble the selected electrodes. \r\nNot all electrode combinations are possible."); @@ -163,11 +166,11 @@ private void InitializeComponent() // this.buttonClearSelections.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.buttonClearSelections.Location = new System.Drawing.Point(15, 282); + this.buttonClearSelections.Location = new System.Drawing.Point(15, 314); this.buttonClearSelections.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); this.buttonClearSelections.Name = "buttonClearSelections"; this.buttonClearSelections.Size = new System.Drawing.Size(280, 44); - this.buttonClearSelections.TabIndex = 12; + this.buttonClearSelections.TabIndex = 14; this.buttonClearSelections.Text = "Clear Electrode Selection"; this.toolTip.SetToolTip(this.buttonClearSelections, "Deselect all electrodes in the probe view. \r\nNote that this does not disable elec" + "trodes, but simply deselects them."); @@ -229,6 +232,8 @@ private void InitializeComponent() this.panelChannelOptions.AutoSize = true; this.panelChannelOptions.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; this.panelChannelOptions.BackColor = System.Drawing.SystemColors.ControlLightLight; + this.panelChannelOptions.Controls.Add(this.comboBoxProbeType); + this.panelChannelOptions.Controls.Add(labelProbeType); this.panelChannelOptions.Controls.Add(this.checkBoxInvertPolarity); this.panelChannelOptions.Controls.Add(invertPolarity); this.panelChannelOptions.Controls.Add(this.textBoxGainCorrection); @@ -390,6 +395,27 @@ private void InitializeComponent() this.toolStripGainCalSN.Text = "No file selected."; this.toolStripGainCalSN.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // + // comboBoxProbeType + // + this.comboBoxProbeType.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.comboBoxProbeType.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.comboBoxProbeType.FormattingEnabled = true; + this.comboBoxProbeType.Location = new System.Drawing.Point(107, 227); + this.comboBoxProbeType.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.comboBoxProbeType.Name = "comboBoxProbeType"; + this.comboBoxProbeType.Size = new System.Drawing.Size(188, 24); + this.comboBoxProbeType.TabIndex = 12; + // + // labelProbeType + // + labelProbeType.AutoSize = true; + labelProbeType.Location = new System.Drawing.Point(15, 231); + labelProbeType.Name = "labelProbeType"; + labelProbeType.Size = new System.Drawing.Size(82, 16); + labelProbeType.TabIndex = 11; + labelProbeType.Text = "Probe Type:"; + // // NeuropixelsV2eProbeConfigurationDialog // this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); @@ -445,5 +471,6 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripStatusLabel toolStripGainCalSN; private System.Windows.Forms.ToolStripStatusLabel toolStripLabelGainCalibrationSN; private System.Windows.Forms.CheckBox checkBoxInvertPolarity; + private System.Windows.Forms.ComboBox comboBoxProbeType; } } diff --git a/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationDialog.cs b/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationDialog.cs index 6a8112de..072b026f 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationDialog.cs +++ b/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationDialog.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; @@ -27,7 +28,15 @@ public NeuropixelsV2ProbeConfiguration ProbeConfiguration /// public bool InvertPolarity { get; set; } - INeuropixelsV2ProbeInfo ProbeData { get; set; } + INeuropixelsV2ProbeInfo ProbeInfo { get; set; } + + readonly Dictionary probeConfigurations; + + enum ProbeType + { + SingleShank = 0, + QuadShank + } /// /// Initializes a new instance of . @@ -35,16 +44,29 @@ public NeuropixelsV2ProbeConfiguration ProbeConfiguration /// A object holding the current configuration settings. /// String containing the path to the calibration file for this probe. /// Boolean denoting whether or not to invert the polarity of neural data. - public NeuropixelsV2eProbeConfigurationDialog(NeuropixelsV2ProbeConfiguration configuration, string calibrationFile, bool invertPolarity) + /// Boolean denoting if this probe is a beta probe or not. + public NeuropixelsV2eProbeConfigurationDialog(NeuropixelsV2ProbeConfiguration configuration, string calibrationFile, bool invertPolarity, bool isBeta) { InitializeComponent(); Shown += FormShown; textBoxProbeCalibrationFile.Text = calibrationFile; + probeConfigurations = new() + { + [ProbeType.SingleShank] = configuration is NeuropixelsV2SingleShankProbeConfiguration + ? configuration.Clone() + : new NeuropixelsV2SingleShankProbeConfiguration(configuration.Probe, NeuropixelsV2SingleShankReference.External), + [ProbeType.QuadShank] = configuration is NeuropixelsV2QuadShankProbeConfiguration + ? configuration.Clone() + : new NeuropixelsV2QuadShankProbeConfiguration(configuration.Probe, NeuropixelsV2QuadShankReference.External) + }; + InvertPolarity = invertPolarity; - ChannelConfiguration = new(configuration); + var currentProbeType = GetCurrentProbeType(configuration); + + ChannelConfiguration = new(probeConfigurations[currentProbeType]); ChannelConfiguration.SetChildFormProperties(this).AddDialogToPanel(panelProbe); this.AddMenuItemsFromDialogToFileOption(ChannelConfiguration); @@ -52,13 +74,18 @@ public NeuropixelsV2eProbeConfigurationDialog(NeuropixelsV2ProbeConfiguration co ChannelConfiguration.OnZoom += UpdateTrackBarLocation; ChannelConfiguration.OnFileLoad += OnFileLoadEvent; - ProbeData = ProbeDataFactory(configuration); + comboBoxProbeType.DataSource = Enum.GetValues(typeof(ProbeType)); + comboBoxProbeType.SelectedItem = currentProbeType; + comboBoxProbeType.SelectedIndexChanged += ProbeTypeChanged; + comboBoxProbeType.Enabled = !isBeta; // NB: Beta probes cannot be a single-shank probe + + ProbeInfo = ProbeDataFactory(ProbeConfiguration); - comboBoxReference.DataSource = ProbeData.GetReferenceEnumValues(); + comboBoxReference.DataSource = ProbeInfo.GetReferenceEnumValues(); comboBoxReference.SelectedItem = ProbeConfiguration.Reference; comboBoxReference.SelectedIndexChanged += SelectedReferenceChanged; - comboBoxChannelPresets.DataSource = ProbeData.GetComboBoxChannelPresets(); + comboBoxChannelPresets.DataSource = ProbeInfo.GetComboBoxChannelPresets(); comboBoxChannelPresets.SelectedIndexChanged += SelectedChannelPresetChanged; checkBoxInvertPolarity.Checked = InvertPolarity; @@ -71,12 +98,31 @@ public NeuropixelsV2eProbeConfigurationDialog(NeuropixelsV2ProbeConfiguration co Text += ": " + ProbeConfiguration.Probe.ToString(); } + ProbeType GetCurrentProbeType(NeuropixelsV2ProbeConfiguration configuration) + { + if (configuration is NeuropixelsV2SingleShankProbeConfiguration) + return ProbeType.SingleShank; + else if (configuration is NeuropixelsV2QuadShankProbeConfiguration) + return ProbeType.QuadShank; + + throw new InvalidEnumArgumentException($"Unknown {nameof(NeuropixelsV2ProbeConfiguration)} type: {configuration.GetType()}"); + } + + private void ProbeTypeChanged(object sender, EventArgs e) + { + UpdateProbeConfiguration(); + } + static INeuropixelsV2ProbeInfo ProbeDataFactory(NeuropixelsV2ProbeConfiguration configuration) { if (configuration is NeuropixelsV2QuadShankProbeConfiguration quadShankConfiguration) { return new NeuropixelsV2QuadShankInfo(quadShankConfiguration); } + else if (configuration is NeuropixelsV2SingleShankProbeConfiguration singleShankConfiguration) + { + return new NeuropixelsV2SingleShankInfo(singleShankConfiguration); + } throw new NotImplementedException("Unknown configuration found."); } @@ -87,6 +133,33 @@ private void InvertPolarityIndexChanged(object sender, EventArgs e) OnInvertPolarityChangedHandler(); } + void UpdateProbeConfiguration() + { + var probeType = (ProbeType)comboBoxProbeType.SelectedItem; + + ChannelConfiguration.ProbeConfiguration = probeConfigurations[probeType]; + + ProbeInfo = ProbeDataFactory(ProbeConfiguration); + + ChannelConfiguration.DrawProbeGroup(); + ChannelConfiguration.ResetZoom(); + ChannelConfiguration.RefreshZedGraph(); + + // NB: Temporarily detach handlers so the updated information is respected + comboBoxChannelPresets.SelectedIndexChanged -= SelectedChannelPresetChanged; + comboBoxChannelPresets.DataSource = ProbeInfo.GetComboBoxChannelPresets(); + comboBoxChannelPresets.SelectedIndexChanged += SelectedChannelPresetChanged; + + comboBoxReference.SelectedIndexChanged -= SelectedReferenceChanged; + comboBoxReference.DataSource = ProbeInfo.GetReferenceEnumValues(); + comboBoxReference.SelectedItem = ProbeConfiguration.Reference; + comboBoxReference.SelectedIndexChanged += SelectedReferenceChanged; + + // TODO: Update InvertPolarity here once it is attached to the ProbeConfiguration class. + + CheckForExistingChannelPreset(); + } + /// /// Set the value to the given boolean. /// @@ -124,7 +197,7 @@ private void SelectedReferenceChanged(object sender, EventArgs e) private void SelectedChannelPresetChanged(object sender, EventArgs e) { Enum channelPreset = ((ComboBox)sender).SelectedItem as Enum ?? throw new InvalidEnumArgumentException("Invalid argument given for the channel preset."); - ProbeConfiguration.SelectElectrodes(ProbeData.GetChannelPreset(channelPreset)); + ProbeConfiguration.SelectElectrodes(ProbeInfo.GetChannelPreset(channelPreset)); ChannelConfiguration.HighlightEnabledContacts(); ChannelConfiguration.HighlightSelectedContacts(); @@ -134,12 +207,16 @@ private void SelectedChannelPresetChanged(object sender, EventArgs e) void CheckForExistingChannelPreset() { - comboBoxChannelPresets.SelectedItem = ProbeData.CheckForExistingChannelPreset(ProbeConfiguration.ChannelMap); + comboBoxChannelPresets.SelectedItem = ProbeInfo.CheckForExistingChannelPreset(ProbeConfiguration.ChannelMap); } private void OnFileLoadEvent(object sender, EventArgs e) { - CheckForExistingChannelPreset(); + var currentProbeType = GetCurrentProbeType(ProbeConfiguration); + + probeConfigurations[currentProbeType].ProbeGroup = (NeuropixelsV2eProbeGroup)ChannelConfiguration.ProbeGroup; + + UpdateProbeConfiguration(); } private void FileTextChanged(object sender, EventArgs e) diff --git a/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationDialog.resx b/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationDialog.resx index 05161bfb..05fb9057 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationDialog.resx +++ b/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationDialog.resx @@ -138,6 +138,9 @@ 274, 17 + + False + 382, 17 diff --git a/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationEditor.cs b/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationEditor.cs index e613d2c2..8db2a3d4 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationEditor.cs +++ b/OpenEphys.Onix1.Design/NeuropixelsV2eProbeConfigurationEditor.cs @@ -35,7 +35,7 @@ public override object EditValue(ITypeDescriptorContext context, IServiceProvide bool isBeta = instance is ConfigureNeuropixelsV2eBeta; - using var editorDialog = new NeuropixelsV2eProbeConfigurationDialog(configuration, calibrationFile, instance.InvertPolarity); + using var editorDialog = new NeuropixelsV2eProbeConfigurationDialog(configuration, calibrationFile, instance.InvertPolarity, isBeta); if (isBeta) { diff --git a/OpenEphys.Onix1/ConfigureNeuropixelsV2e.cs b/OpenEphys.Onix1/ConfigureNeuropixelsV2e.cs index 1ab21793..19e53daf 100644 --- a/OpenEphys.Onix1/ConfigureNeuropixelsV2e.cs +++ b/OpenEphys.Onix1/ConfigureNeuropixelsV2e.cs @@ -58,6 +58,7 @@ public ConfigureNeuropixelsV2e(ConfigureNeuropixelsV2e configureNode) [Description("Probe A configuration.")] [Editor("OpenEphys.Onix1.Design.NeuropixelsV2eProbeConfigurationEditor, OpenEphys.Onix1.Design", typeof(UITypeEditor))] [XmlElement(nameof(ProbeConfigurationA), typeof(NeuropixelsV2QuadShankProbeConfiguration))] + [XmlElement(NeuropixelsV2SingleShankProbeConfiguration.XmlTypeName + nameof(ProbeConfigurationA), typeof(NeuropixelsV2SingleShankProbeConfiguration))] public NeuropixelsV2ProbeConfiguration ProbeConfigurationA { get; set; } = new NeuropixelsV2QuadShankProbeConfiguration(NeuropixelsV2Probe.ProbeA, NeuropixelsV2QuadShankReference.External); /// @@ -72,6 +73,7 @@ public ConfigureNeuropixelsV2e(ConfigureNeuropixelsV2e configureNode) [Description("Probe B configuration.")] [Editor("OpenEphys.Onix1.Design.NeuropixelsV2eProbeConfigurationEditor, OpenEphys.Onix1.Design", typeof(UITypeEditor))] [XmlElement(nameof(ProbeConfigurationB), typeof(NeuropixelsV2QuadShankProbeConfiguration))] + [XmlElement(NeuropixelsV2SingleShankProbeConfiguration.XmlTypeName + nameof(ProbeConfigurationB), typeof(NeuropixelsV2SingleShankProbeConfiguration))] public NeuropixelsV2ProbeConfiguration ProbeConfigurationB { get; set; } = new NeuropixelsV2QuadShankProbeConfiguration(NeuropixelsV2Probe.ProbeB, NeuropixelsV2QuadShankReference.External); /// diff --git a/OpenEphys.Onix1/ConfigureNeuropixelsV2eBeta.cs b/OpenEphys.Onix1/ConfigureNeuropixelsV2eBeta.cs index 8687ffc1..e9e056cd 100644 --- a/OpenEphys.Onix1/ConfigureNeuropixelsV2eBeta.cs +++ b/OpenEphys.Onix1/ConfigureNeuropixelsV2eBeta.cs @@ -73,6 +73,7 @@ public ConfigureNeuropixelsV2eBeta(ConfigureNeuropixelsV2eBeta configureNode) [Description("Probe A configuration.")] [Editor("OpenEphys.Onix1.Design.NeuropixelsV2eProbeConfigurationEditor, OpenEphys.Onix1.Design", typeof(UITypeEditor))] [XmlElement(nameof(ProbeConfigurationA), typeof(NeuropixelsV2QuadShankProbeConfiguration))] + [XmlElement(NeuropixelsV2SingleShankProbeConfiguration.XmlTypeName + nameof(ProbeConfigurationA), typeof(NeuropixelsV2SingleShankProbeConfiguration))] public NeuropixelsV2ProbeConfiguration ProbeConfigurationA { get; set; } = new NeuropixelsV2QuadShankProbeConfiguration(NeuropixelsV2Probe.ProbeA, NeuropixelsV2QuadShankReference.External); /// @@ -87,6 +88,7 @@ public ConfigureNeuropixelsV2eBeta(ConfigureNeuropixelsV2eBeta configureNode) [Description("Probe B configuration.")] [Editor("OpenEphys.Onix1.Design.NeuropixelsV2eProbeConfigurationEditor, OpenEphys.Onix1.Design", typeof(UITypeEditor))] [XmlElement(nameof(ProbeConfigurationB), typeof(NeuropixelsV2QuadShankProbeConfiguration))] + [XmlElement(NeuropixelsV2SingleShankProbeConfiguration.XmlTypeName + nameof(ProbeConfigurationB), typeof(NeuropixelsV2SingleShankProbeConfiguration))] public NeuropixelsV2ProbeConfiguration ProbeConfigurationB { get; set; } = new NeuropixelsV2QuadShankProbeConfiguration(NeuropixelsV2Probe.ProbeB, NeuropixelsV2QuadShankReference.External); /// diff --git a/OpenEphys.Onix1/NeuropixelsV2Electrode.cs b/OpenEphys.Onix1/NeuropixelsV2Electrode.cs index 19c98216..420e0a94 100644 --- a/OpenEphys.Onix1/NeuropixelsV2Electrode.cs +++ b/OpenEphys.Onix1/NeuropixelsV2Electrode.cs @@ -6,6 +6,7 @@ namespace OpenEphys.Onix1 /// /// Class defining a . /// + [XmlInclude(typeof(NeuropixelsV2SingleShankElectrode))] [XmlInclude(typeof(NeuropixelsV2QuadShankElectrode))] [XmlType(Namespace = Constants.XmlNamespace)] public abstract class NeuropixelsV2Electrode : Electrode @@ -27,7 +28,5 @@ public abstract class NeuropixelsV2Electrode : Electrode /// [XmlIgnore] public int BlockIndex { get; init; } - - internal abstract Func GetChannelNumberFunc(); } } diff --git a/OpenEphys.Onix1/NeuropixelsV2ProbeConfiguration.cs b/OpenEphys.Onix1/NeuropixelsV2ProbeConfiguration.cs index aedae6e8..dcc3488b 100644 --- a/OpenEphys.Onix1/NeuropixelsV2ProbeConfiguration.cs +++ b/OpenEphys.Onix1/NeuropixelsV2ProbeConfiguration.cs @@ -42,6 +42,7 @@ public enum NeuropixelsV2Bank /// Defines a configuration for Neuropixels 2.0 and 2.0-beta probes. /// [XmlInclude(typeof(NeuropixelsV2QuadShankProbeConfiguration))] + [XmlInclude(typeof(NeuropixelsV2SingleShankProbeConfiguration))] [XmlType(Namespace = Constants.XmlNamespace)] public abstract class NeuropixelsV2ProbeConfiguration { @@ -82,7 +83,27 @@ public abstract class NeuropixelsV2ProbeConfiguration /// Update the with the selected electrodes. /// /// List of selected electrodes that are being added to the - public abstract void SelectElectrodes(NeuropixelsV2Electrode[] electrodes); + public void SelectElectrodes(NeuropixelsV2Electrode[] electrodes) + { + if (electrodes.Length == 0) return; + + var channelMap = ChannelMap; + + foreach (var e in electrodes) + { + try + { + channelMap[e.Channel] = e; + } + catch (IndexOutOfRangeException ex) + { + throw new IndexOutOfRangeException($"Electrode {e.Index} specifies channel {e.Channel} but only channels " + + $"0 to {channelMap.Length - 1} are supported.", ex); + } + } + + ProbeGroup.UpdateDeviceChannelIndices(channelMap); + } /// /// Gets the channel configuration for this probe. @@ -102,10 +123,40 @@ public abstract class NeuropixelsV2ProbeConfiguration [XmlElement(nameof(ProbeGroup))] public abstract string ProbeGroupString { get; set; } + const int ReferencePixelCount = 4; + const int DummyRegisterCount = 4; + + /// + /// Number of registers per shank. + /// + protected const int RegistersPerShank = NeuropixelsV2.ElectrodePerShank + ReferencePixelCount + DummyRegisterCount; + + /// + /// Index of the shift register bit for external electrode 0. + /// + protected const int ShiftRegisterBitExternalElectrode0 = 1285; + /// + /// Index of the shift register bit for external electrode 1. + /// + protected const int ShiftRegisterBitExternalElectrode1 = 2; + + /// + /// Index of the shift register bit for tip electrode 0. + /// + protected const int ShiftRegisterBitTipElectrode0 = 644; + /// + /// Index of the shift register bit for tip electrode 1. + /// + protected const int ShiftRegisterBitTipElectrode1 = 643; + internal abstract BitArray[] CreateShankBits(Enum reference); internal abstract int GetReferenceBit(Enum reference); internal abstract bool IsGroundReference(); + + internal abstract NeuropixelsV2ProbeConfiguration Clone(); + + internal abstract Func GetChannelNumberFunc(); } } diff --git a/OpenEphys.Onix1/NeuropixelsV2QuadShankElectrode.cs b/OpenEphys.Onix1/NeuropixelsV2QuadShankElectrode.cs index 66071568..1d0f6160 100644 --- a/OpenEphys.Onix1/NeuropixelsV2QuadShankElectrode.cs +++ b/OpenEphys.Onix1/NeuropixelsV2QuadShankElectrode.cs @@ -42,11 +42,6 @@ static PointF GetPosition(int index) return new PointF(x: position[0], y: position[1]); } - internal override Func GetChannelNumberFunc() - { - return GetChannelNumber; - } - /// /// Gets the channel number of a given electrode. /// diff --git a/OpenEphys.Onix1/NeuropixelsV2QuadShankProbeConfiguration.cs b/OpenEphys.Onix1/NeuropixelsV2QuadShankProbeConfiguration.cs index 44eec4bf..a7be392d 100644 --- a/OpenEphys.Onix1/NeuropixelsV2QuadShankProbeConfiguration.cs +++ b/OpenEphys.Onix1/NeuropixelsV2QuadShankProbeConfiguration.cs @@ -46,7 +46,7 @@ public enum NeuropixelsV2QuadShankReference : uint [XmlType(TypeName = XmlTypeName, Namespace = Constants.XmlNamespace)] public class NeuropixelsV2QuadShankProbeConfiguration : NeuropixelsV2ProbeConfiguration { - const string XmlTypeName = "QuadShank"; + internal const string XmlTypeName = "QuadShank"; /// /// Initializes a default instance of the class. @@ -91,6 +91,11 @@ public NeuropixelsV2QuadShankProbeConfiguration(NeuropixelsV2eQuadShankProbeGrou Probe = probe; } + internal override NeuropixelsV2ProbeConfiguration Clone() + { + return new NeuropixelsV2QuadShankProbeConfiguration(this); + } + /// /// Gets the existing channel map listing all currently enabled electrodes. /// @@ -121,42 +126,8 @@ public override string ProbeGroupString } } - /// - /// Update the with the selected electrodes. - /// - /// List of selected electrodes that are being added to the - public override void SelectElectrodes(NeuropixelsV2Electrode[] electrodes) - { - var channelMap = ChannelMap; - - foreach (var e in electrodes) - { - try - { - channelMap[e.Channel] = e; - } - catch (IndexOutOfRangeException ex) - { - throw new IndexOutOfRangeException($"Electrode {e.Index} specifies channel {e.Channel} but only channels " + - $"0 to {channelMap.Length - 1} are supported.", ex); - } - } - - ProbeGroup.UpdateDeviceChannelIndices(channelMap); - } - internal override BitArray[] CreateShankBits(Enum reference) { - const int ReferencePixelCount = 4; - const int DummyRegisterCount = 4; - const int RegistersPerShank = NeuropixelsV2.ElectrodePerShank + ReferencePixelCount + DummyRegisterCount; - - const int ShiftRegisterBitExternalElectrode0 = 1285; - const int ShiftRegisterBitExternalElectrode1 = 2; - - const int ShiftRegisterBitTipElectrode0 = 644; - const int ShiftRegisterBitTipElectrode1 = 643; - var shankBits = new BitArray[] { new(RegistersPerShank, false), @@ -175,7 +146,7 @@ internal override BitArray[] CreateShankBits(Enum reference) NeuropixelsV2QuadShankReference.Tip2 => 1, NeuropixelsV2QuadShankReference.Tip3 => 2, NeuropixelsV2QuadShankReference.Tip4 => 3, - _ => throw new InvalidOperationException($"Invalid reference chosen for quad-shank probe.") + _ => throw new InvalidEnumArgumentException("Invalid reference chosen for quad-shank probe.") }; // If tip reference is used, activate the tip electrode @@ -240,5 +211,10 @@ public override string ReferenceSerialized : NeuropixelsV2QuadShankReference.External; } } + + internal override Func GetChannelNumberFunc() + { + return NeuropixelsV2QuadShankElectrode.GetChannelNumber; + } } } diff --git a/OpenEphys.Onix1/NeuropixelsV2SingleShankElectrode.cs b/OpenEphys.Onix1/NeuropixelsV2SingleShankElectrode.cs new file mode 100644 index 00000000..f8eda982 --- /dev/null +++ b/OpenEphys.Onix1/NeuropixelsV2SingleShankElectrode.cs @@ -0,0 +1,105 @@ +using System; +using System.Drawing; +using System.Xml.Serialization; + +namespace OpenEphys.Onix1 +{ + /// + /// Class defining a . + /// + public class NeuropixelsV2SingleShankElectrode : NeuropixelsV2Electrode + { + /// + /// Gets the zero-indexed column index of this electrode. + /// + [XmlIgnore] + public int ColumnIndex { get; set; } + + /// + /// Class defining a Neuropixels 2.0 single-shank electrode. + /// + /// Global index of the electrode. + public NeuropixelsV2SingleShankElectrode(int index) + { + Index = index; + Shank = 0; + IntraShankElectrodeIndex = index; + Bank = GetBank(index); + Block = GetBlock(index); + BlockIndex = GetBlockIndex(index); + Position = GetPosition(index); + ColumnIndex = GetColumnIndex(index); + Channel = GetSingleShankChannelNumber(Bank, Block, GetRow(index), ColumnIndex); + } + + static NeuropixelsV2Bank GetBank(int index) => (NeuropixelsV2Bank)(index / NeuropixelsV2.ChannelCount); + + const int ElectrodesPerBlock = 32; + + static int GetBankIndex(int index) => index % NeuropixelsV2.ChannelCount; + + static int GetBlock(int index) => GetBankIndex(index) / ElectrodesPerBlock; + + static int GetBlockIndex(int index) => index % ElectrodesPerBlock; + + const int ElectrodesPerRow = 2; + + static int GetRow(int index) => GetBankIndex(index) % ElectrodesPerBlock / ElectrodesPerRow; + + static PointF GetPosition(int index) + { + var position = NeuropixelsV2eProbeGroup.DefaultContactPosition(index); + return new PointF(x: position[0], y: position[1]); + } + + static int GetColumnIndex(int index) + { + return index % 2; + } + + /// + /// Gets the channel number of a given electrode. + /// + /// Integer defining the index of the electrode in the probe. + /// An integer between 0 and 383 defining the channel number. + public static int GetChannelNumber(int index) + { + var bank = GetBank(index); + var block = GetBlock(index); + var row = GetRow(index); + var columnIndex = GetColumnIndex(index); + + return GetSingleShankChannelNumber(bank, block, row, columnIndex); + } + + static int GetSingleShankChannelNumber(NeuropixelsV2Bank bank, int block, int row, int columnIndex) + { + const int MaxBlockValue = 11; + const int MaxRowValue = 15; + + if (block > MaxBlockValue || block < 0) + throw new ArgumentOutOfRangeException($"Block value is out of range. Expected to be between 0 and {MaxBlockValue}, but value is {block}"); + + if (row > MaxRowValue || row < 0) + throw new ArgumentOutOfRangeException($"Row value is out of range. Expected to be between 0 and {MaxRowValue}, but value is {row}"); + + if (columnIndex > 1 || columnIndex < 0) + throw new ArgumentOutOfRangeException($"Column index value is out of range. Expected to be between 0 and 1, but value was {columnIndex}."); + + const int HalfBlock = 16; + + return (bank, columnIndex) switch + { + (NeuropixelsV2Bank.A, 0) => row * ElectrodesPerRow + block * ElectrodesPerBlock, + (NeuropixelsV2Bank.A, 1) => row * ElectrodesPerRow + block * ElectrodesPerBlock + 1, + (NeuropixelsV2Bank.B, 0) => (row * 7 % HalfBlock) * ElectrodesPerRow + block * ElectrodesPerBlock, + (NeuropixelsV2Bank.B, 1) => ((row * 7 + 4) % HalfBlock) * ElectrodesPerRow + block * ElectrodesPerBlock + 1, + (NeuropixelsV2Bank.C, 0) => (row * 5 % HalfBlock) * ElectrodesPerRow + block * ElectrodesPerBlock, + (NeuropixelsV2Bank.C, 1) => ((row * 5 + 8) % HalfBlock) * ElectrodesPerRow + block * ElectrodesPerBlock + 1, + (NeuropixelsV2Bank.D, 0) => (row * 3 % HalfBlock) * ElectrodesPerRow + block * ElectrodesPerBlock, + (NeuropixelsV2Bank.D, 1) => ((row * 3 + 12) % HalfBlock) * ElectrodesPerRow + block * ElectrodesPerBlock + 1, + _ => throw new NotImplementedException($"Invalid {nameof(NeuropixelsV2Bank)} value given.") + }; + } + } +} diff --git a/OpenEphys.Onix1/NeuropixelsV2SingleShankProbeConfiguration.cs b/OpenEphys.Onix1/NeuropixelsV2SingleShankProbeConfiguration.cs new file mode 100644 index 00000000..aa32c734 --- /dev/null +++ b/OpenEphys.Onix1/NeuropixelsV2SingleShankProbeConfiguration.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections; +using System.ComponentModel; +using System.Text; +using System.Xml.Serialization; +using Bonsai; +using Newtonsoft.Json; + +namespace OpenEphys.Onix1 +{ + /// + /// Specifies the reference for a Neuropixels 2.0 probe. + /// + public enum NeuropixelsV2SingleShankReference : uint + { + /// + /// Specifies that the External reference will be used. + /// + External, + /// + /// Specifies that the tip reference will be used. + /// + Tip, + /// + /// Specifies that the Ground reference will be used. + /// + Ground + } + + /// + /// Defines a configuration for single-shank, Neuropixels 2.0 and 2.0-beta probes. + /// + [DisplayName(XmlTypeName)] + [XmlType(TypeName = XmlTypeName, Namespace = Constants.XmlNamespace)] + public class NeuropixelsV2SingleShankProbeConfiguration : NeuropixelsV2ProbeConfiguration + { + internal const string XmlTypeName = "SingleShank"; + + /// + /// Initializes a default instance of the class. + /// + public NeuropixelsV2SingleShankProbeConfiguration() + { + } + + /// + /// Initializes a new instance of the class. + /// + public NeuropixelsV2SingleShankProbeConfiguration(NeuropixelsV2Probe probe, NeuropixelsV2SingleShankReference reference) + { + Probe = probe; + Reference = reference; + ProbeGroup = new NeuropixelsV2eSingleShankProbeGroup(); + } + + /// + /// Copy constructor for the class. + /// + /// The existing object to copy. + public NeuropixelsV2SingleShankProbeConfiguration(NeuropixelsV2SingleShankProbeConfiguration probeConfiguration) + { + Reference = probeConfiguration.Reference; + ProbeGroup = probeConfiguration.ProbeGroup.Clone(); + Probe = probeConfiguration.Probe; + } + + /// + /// Initializes a new instance of the class with the given + /// channel configuration. + /// + /// The existing instance to use. + /// The reference value. + /// The for this probe. + [JsonConstructor] + public NeuropixelsV2SingleShankProbeConfiguration(NeuropixelsV2eSingleShankProbeGroup probeGroup, NeuropixelsV2Probe probe, NeuropixelsV2SingleShankReference reference) + { + ProbeGroup = probeGroup.Clone(); + Reference = reference; + Probe = probe; + } + + internal override NeuropixelsV2ProbeConfiguration Clone() + { + return new NeuropixelsV2SingleShankProbeConfiguration(this); + } + + /// + /// Gets the existing channel map listing all currently enabled electrodes. + /// + /// + /// The channel map will always be 384 channels, and will return the 384 enabled electrodes. + /// + [XmlIgnore] + public override NeuropixelsV2Electrode[] ChannelMap + { + get => NeuropixelsV2eSingleShankProbeGroup.ToChannelMap((NeuropixelsV2eSingleShankProbeGroup)ProbeGroup); + } + + /// + [Browsable(false)] + [Externalizable(false)] + [XmlElement(nameof(ProbeGroup))] + public override string ProbeGroupString + { + get + { + var jsonString = JsonConvert.SerializeObject(ProbeGroup); + return Convert.ToBase64String(Encoding.UTF8.GetBytes(jsonString)); + } + set + { + var jsonString = Encoding.UTF8.GetString(Convert.FromBase64String(value)); + ProbeGroup = JsonConvert.DeserializeObject(jsonString); + } + } + + internal override BitArray[] CreateShankBits(Enum reference) + { + BitArray[] shankBits; + + shankBits = new BitArray[] + { + new(RegistersPerShank, false) + }; + const int Shank = 0; + + NeuropixelsV2SingleShankReference singleShankReference = (NeuropixelsV2SingleShankReference)reference; + + if (singleShankReference == NeuropixelsV2SingleShankReference.Tip) + { + shankBits[Shank][ShiftRegisterBitTipElectrode1] = true; + shankBits[Shank][ShiftRegisterBitTipElectrode0] = true; + } + else if (singleShankReference == NeuropixelsV2SingleShankReference.External) + { + shankBits[Shank][ShiftRegisterBitExternalElectrode0] = true; + shankBits[Shank][ShiftRegisterBitExternalElectrode1] = true; + } + + return shankBits; + } + + internal override int GetReferenceBit(Enum reference) + { + var singleShankReference = (NeuropixelsV2SingleShankReference)reference; + + return singleShankReference switch + { + NeuropixelsV2SingleShankReference.External => 1, + NeuropixelsV2SingleShankReference.Tip => 2, + NeuropixelsV2SingleShankReference.Ground => 3, + _ => throw new InvalidOperationException("Invalid reference selection."), + }; + } + + internal override bool IsGroundReference() => (NeuropixelsV2SingleShankReference)Reference == NeuropixelsV2SingleShankReference.Ground; + + /// + [XmlElement(nameof(Reference))] + [Browsable(false)] + [Externalizable(false)] + public override string ReferenceSerialized + { + get => Reference.ToString(); + set + { + if (string.IsNullOrEmpty(value)) + { + Reference = NeuropixelsV2SingleShankReference.External; + return; + } + + Reference = Enum.TryParse(value, out var result) + ? result + : NeuropixelsV2SingleShankReference.External; + } + } + + internal override Func GetChannelNumberFunc() + { + return NeuropixelsV2SingleShankElectrode.GetChannelNumber; + } + } +} diff --git a/OpenEphys.Onix1/NeuropixelsV2eProbeGroup.cs b/OpenEphys.Onix1/NeuropixelsV2eProbeGroup.cs index 3b2326c8..62a92d5d 100644 --- a/OpenEphys.Onix1/NeuropixelsV2eProbeGroup.cs +++ b/OpenEphys.Onix1/NeuropixelsV2eProbeGroup.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Xml.Serialization; using OpenEphys.ProbeInterface.NET; @@ -10,9 +9,25 @@ namespace OpenEphys.Onix1 /// Abstract class that defines a Neuropixels 2.0 Probe Group /// [XmlInclude(typeof(NeuropixelsV2eQuadShankProbeGroup))] + [XmlInclude(typeof(NeuropixelsV2eSingleShankProbeGroup))] [XmlType(Namespace = Constants.XmlNamespace)] public abstract class NeuropixelsV2eProbeGroup : ProbeGroup { + /// + /// Shank X-axis offset. + /// + protected const float shankOffsetX = 200f; + + /// + /// Shank X-axis width. + /// + protected const float shankWidthX = 70f; + + /// + /// Shank X-axis pitch. + /// + protected const float shankPitchX = 250f; + /// /// Initializes a new instance of a . /// @@ -50,6 +65,74 @@ internal void UpdateDeviceChannelIndices(NeuropixelsV2Electrode[] channelMap) UpdateDeviceChannelIndices(0, newDeviceChannelIndices); } + /// + /// Generates a 2D array of default contact positions based on the given number of channels. + /// + /// Value defining the number of contacts to create positions for. + /// + /// 2D array of floats [N x 2], where the first dimension is the contact index [N] and the second dimension [2] + /// contains the X and Y values, respectively. + /// + public static float[][] DefaultContactPositions(int numberOfContacts) + { + float[][] contactPositions = new float[numberOfContacts][]; + + for (int i = 0; i < numberOfContacts; i++) + { + contactPositions[i] = DefaultContactPosition(i); + } + + return contactPositions; + } + + /// + /// Generates a float array containing the X and Y position of a single contact. + /// + /// Index of the contact. + /// A float array of size [2 x 1] with the X and Y coordinates, respectively. + public static float[] DefaultContactPosition(int contactIndex) + { + return new float[2] { ContactPositionX(contactIndex), contactIndex % NeuropixelsV2.ElectrodePerShank / 2 * 15 + 170 }; + } + + private static float ContactPositionX(int index) + { + var shank = index / NeuropixelsV2.ElectrodePerShank; + var offset = shankOffsetX + (shankWidthX + shankPitchX) * shank + 11; + + return (index % 2) switch + { + 0 => offset + 8.0f, + 1 => offset + 40.0f, + _ => throw new ArgumentException("Invalid index given.") + }; + } + + /// + /// Generates an array of strings with the shank value as the default shank ID. + /// + /// Number of contacts in a single probe. + /// + public static string[] DefaultShankIds(int numberOfContacts) + { + string[] contactIds = new string[numberOfContacts]; + + for (int i = 0; i < numberOfContacts; i++) + { + var shank = i / NeuropixelsV2.ElectrodePerShank; + contactIds[i] = shank switch + { + 0 => "0", + 1 => "1", + 2 => "2", + 3 => "3", + _ => throw new InvalidOperationException($"Too many shanks; expected four shanks, but received {shank} as an index.") + }; + } + + return contactIds; + } + /// /// Convert a ProbeInterface object to a list of electrodes, which includes all possible electrodes. /// diff --git a/OpenEphys.Onix1/NeuropixelsV2eQuadShankProbeGroup.cs b/OpenEphys.Onix1/NeuropixelsV2eQuadShankProbeGroup.cs index 0a7d9c6e..e46aba8e 100644 --- a/OpenEphys.Onix1/NeuropixelsV2eQuadShankProbeGroup.cs +++ b/OpenEphys.Onix1/NeuropixelsV2eQuadShankProbeGroup.cs @@ -11,10 +11,6 @@ namespace OpenEphys.Onix1 /// public class NeuropixelsV2eQuadShankProbeGroup : NeuropixelsV2eProbeGroup { - const float shankOffsetX = 200f; - const float shankWidthX = 70f; - const float shankPitchX = 250f; - /// /// Initializes a new instance of the class. /// @@ -79,49 +75,6 @@ internal override NeuropixelsV2eProbeGroup Clone() return new NeuropixelsV2eQuadShankProbeGroup(Specification, Version, Probes.Select(probe => new Probe(probe)).ToArray()); } - /// - /// Generates a 2D array of default contact positions based on the given number of channels. - /// - /// Value defining the number of contacts to create positions for. - /// - /// 2D array of floats [N x 2], where the first dimension is the contact index [N] and the second dimension [2] - /// contains the X and Y values, respectively. - /// - public static float[][] DefaultContactPositions(int numberOfContacts) - { - float[][] contactPositions = new float[numberOfContacts][]; - - for (int i = 0; i < numberOfContacts; i++) - { - contactPositions[i] = DefaultContactPosition(i); - } - - return contactPositions; - } - - /// - /// Generates a float array containing the X and Y position of a single contact. - /// - /// Index of the contact. - /// A float array of size [2 x 1] with the X and Y coordinates, respectively. - public static float[] DefaultContactPosition(int contactIndex) - { - return new float[2] { ContactPositionX(contactIndex), contactIndex % NeuropixelsV2.ElectrodePerShank / 2 * 15 + 170 }; - } - - private static float ContactPositionX(int index) - { - var shank = index / NeuropixelsV2.ElectrodePerShank; - var offset = shankOffsetX + (shankWidthX + shankPitchX) * shank + 11; - - return (index % 2) switch - { - 0 => offset + 8.0f, - 1 => offset + 40.0f, - _ => throw new ArgumentException("Invalid index given.") - }; - } - /// /// Generates a default planar contour for the type of probe that is given. /// @@ -179,31 +132,6 @@ public static int[] DefaultDeviceChannelIndices(int channelCount, int electrodeC return deviceChannelIndices; } - /// - /// Generates an array of strings with the shank value as the default shank ID. - /// - /// Number of contacts in a single probe. - /// - public static string[] DefaultShankIds(int numberOfContacts) - { - string[] contactIds = new string[numberOfContacts]; - - for (int i = 0; i < numberOfContacts; i++) - { - var shank = i / NeuropixelsV2.ElectrodePerShank; - contactIds[i] = shank switch - { - 0 => "0", - 1 => "1", - 2 => "2", - 3 => "3", - _ => throw new InvalidOperationException($"Too many shanks; expected four shanks, but received {shank} as an index.") - }; - } - - return contactIds; - } - /// /// Convert a ProbeInterface object to a list of electrodes, which includes all possible electrodes. /// diff --git a/OpenEphys.Onix1/NeuropixelsV2eSingleShankProbeGroup.cs b/OpenEphys.Onix1/NeuropixelsV2eSingleShankProbeGroup.cs new file mode 100644 index 00000000..f69cc954 --- /dev/null +++ b/OpenEphys.Onix1/NeuropixelsV2eSingleShankProbeGroup.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using OpenEphys.ProbeInterface.NET; + +namespace OpenEphys.Onix1 +{ + /// + /// A class for NeuropixelsV2e. + /// + public class NeuropixelsV2eSingleShankProbeGroup : NeuropixelsV2eProbeGroup + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The default constructor will initialize the new with + /// the default settings for all contacts, including their positions, shapes, and IDs. + /// + public NeuropixelsV2eSingleShankProbeGroup() + : base("probeinterface", "0.2.21", DefaultProbes()) + { + } + + static Probe[] DefaultProbes() + { + var probe = new Probe[1]; + + const int numberOfShanks = 1; + + probe[0] = new(ProbeNdim.Two, + ProbeSiUnits.um, + new ProbeAnnotations("Neuropixels 2.0 - single shank", "IMEC"), + null, + DefaultContactPositions(NeuropixelsV2.ElectrodePerShank * numberOfShanks), + Probe.DefaultContactPlaneAxes(NeuropixelsV2.ElectrodePerShank * numberOfShanks), + Probe.DefaultContactShapes(NeuropixelsV2.ElectrodePerShank * numberOfShanks, ContactShape.Square), + Probe.DefaultSquareParams(NeuropixelsV2.ElectrodePerShank * numberOfShanks, 12.0f), + DefaultProbePlanarContour(), + DefaultDeviceChannelIndices(NeuropixelsV2.ChannelCount, NeuropixelsV2.ElectrodePerShank * numberOfShanks), + Probe.DefaultContactIds(NeuropixelsV2.ElectrodePerShank * numberOfShanks), + DefaultShankIds(NeuropixelsV2.ElectrodePerShank * numberOfShanks)); + + return probe; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// This constructor is marked with the , and is the + /// entry point for deserializing the JSON data into a C# class. + /// + /// String defining the . + /// String defining the . + /// Array of Probes. + [JsonConstructor] + public NeuropixelsV2eSingleShankProbeGroup(string specification, string version, Probe[] probes) + : base(specification, version, probes) + { + } + + /// + /// Copy constructor that initializes a copied instance of the class. + /// + /// An existing object. + public NeuropixelsV2eSingleShankProbeGroup(NeuropixelsV2eSingleShankProbeGroup probeGroup) + : base(probeGroup) + { + } + + internal override NeuropixelsV2eProbeGroup Clone() + { + return new NeuropixelsV2eSingleShankProbeGroup(Specification, Version, Probes.Select(probe => new Probe(probe)).ToArray()); + } + + /// + /// Generates a default planar contour for the probe, based on the given probe index + /// + /// + public static float[][] DefaultProbePlanarContour() + { + const float shankTipY = 0f; + const float shankBaseY = 155f; + const float shankLengthY = 9770f; + const float shankWidthX = 70f; + const float shankMidX = 35f; + + float[][] probePlanarContour = new float[6][]; + + probePlanarContour[0] = new float[2] { shankOffsetX + 0f, shankBaseY }; + probePlanarContour[1] = new float[2] { shankOffsetX + shankMidX, shankTipY }; + probePlanarContour[2] = new float[2] { shankOffsetX + shankWidthX, shankBaseY }; + probePlanarContour[3] = new float[2] { shankOffsetX + shankWidthX, shankLengthY }; + probePlanarContour[4] = new float[2] { shankOffsetX + 0f, shankLengthY }; + probePlanarContour[5] = new float[2] { shankOffsetX + 0f, shankBaseY }; + + return probePlanarContour; + } + + /// + /// Generates the first default device channel indices, and + /// marks the rest as -1 to indicate they are not actively recorded. + /// + /// Number of contacts that are connected for recording. + /// Total number of physical contacts on the probe. + /// + public static int[] DefaultDeviceChannelIndices(int channelCount, int electrodeCount) + { + int[] deviceChannelIndices = new int[electrodeCount]; + + for (int i = 0; i < channelCount; i++) + { + deviceChannelIndices[i] = NeuropixelsV2SingleShankElectrode.GetChannelNumber(i); + } + + for (int i = channelCount; i < electrodeCount; i++) + { + deviceChannelIndices[i] = -1; + } + + return deviceChannelIndices; + } + + /// + /// Convert a ProbeInterface object to a list of electrodes, which includes all possible electrodes. + /// + /// List of electrodes. + public override List ToElectrodes() + { + List electrodes = new(); + + foreach (var c in GetContacts()) + { + electrodes.Add(new NeuropixelsV2SingleShankElectrode(c.Index)); + } + + return electrodes; + } + + /// + /// Convert a object to a list of electrodes, + /// which only includes currently enabled electrodes. + /// + /// A object. + /// List of electrodes that are enabled. + public static NeuropixelsV2SingleShankElectrode[] ToChannelMap(NeuropixelsV2eSingleShankProbeGroup probeGroup) + { + var enabledContacts = probeGroup.GetContacts().Where(c => c.DeviceId != -1); + + if (enabledContacts.Count() != NeuropixelsV2.ChannelCount) + { + throw new InvalidOperationException($"Channel configuration must have {NeuropixelsV2.ChannelCount} contacts enabled." + + $"Instead there are {enabledContacts.Count()} contacts enabled. Enabled contacts are designated by a device channel" + + $"index >= 0."); + } + + return enabledContacts.Select(c => new NeuropixelsV2SingleShankElectrode(c.Index)) + .OrderBy(e => e.Channel) + .ToArray(); + } + } +} diff --git a/OpenEphys.Onix1/ProbeInterfaceHelper.cs b/OpenEphys.Onix1/ProbeInterfaceHelper.cs index df968485..c77be6a5 100644 --- a/OpenEphys.Onix1/ProbeInterfaceHelper.cs +++ b/OpenEphys.Onix1/ProbeInterfaceHelper.cs @@ -15,7 +15,7 @@ public static ProbeGroup LoadExternalProbeInterfaceFile(string probeInterfaceFil throw new ArgumentNullException(nameof(probeInterfaceFileName), "ProbeInterface file path cannot be null or empty."); } - if (!type.IsAssignableFrom(typeof(ProbeGroup))) throw new InvalidDataException($"Invalid type given: {type.Name} is not a valid {nameof(ProbeGroup)} type."); + if (!typeof(ProbeGroup).IsAssignableFrom(type)) throw new InvalidDataException($"Invalid type given: {type.Name} is not a valid {nameof(ProbeGroup)} type."); if (!File.Exists(probeInterfaceFileName)) {