From 4f4b0b95261689a82398b9a619acad9709d2fba8 Mon Sep 17 00:00:00 2001 From: bparks13 Date: Tue, 29 Oct 2024 15:58:41 -0400 Subject: [PATCH] Algorithmically find optimal step size - Find the step size with the minimum maximum error, including all current and new amplitudes - Remove messages warning about modifying all channels - Add verbose error message when trying to apply stimulation parameters that will change the existing amplitude values --- .../Rhs2116StimulusSequenceDialog.Designer.cs | 44 ++-- .../Rhs2116StimulusSequenceDialog.cs | 212 +++++++++++------- 2 files changed, 155 insertions(+), 101 deletions(-) diff --git a/OpenEphys.Onix1.Design/Rhs2116StimulusSequenceDialog.Designer.cs b/OpenEphys.Onix1.Design/Rhs2116StimulusSequenceDialog.Designer.cs index a73db59..393d7a0 100644 --- a/OpenEphys.Onix1.Design/Rhs2116StimulusSequenceDialog.Designer.cs +++ b/OpenEphys.Onix1.Design/Rhs2116StimulusSequenceDialog.Designer.cs @@ -32,7 +32,6 @@ private void InitializeComponent() System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Rhs2116StimulusSequenceDialog)); this.buttonCancel = new System.Windows.Forms.Button(); this.statusStrip = new System.Windows.Forms.StatusStrip(); - this.toolStripStatusIsValid = new System.Windows.Forms.ToolStripStatusLabel(); this.toolStripStatusSlotsUsed = new System.Windows.Forms.ToolStripStatusLabel(); this.buttonOk = new System.Windows.Forms.Button(); this.panelParameters = new System.Windows.Forms.Panel(); @@ -76,6 +75,7 @@ private void InitializeComponent() this.groupBox1 = new System.Windows.Forms.GroupBox(); this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel(); this.toolTip1 = new System.Windows.Forms.ToolTip(this.components); + this.toolStripStatusIsValid = new System.Windows.Forms.ToolStripStatusLabel(); this.statusStrip.SuspendLayout(); this.panelParameters.SuspendLayout(); this.groupBoxCathode.SuspendLayout(); @@ -117,17 +117,6 @@ private void InitializeComponent() this.statusStrip.TabIndex = 1; this.statusStrip.Text = "statusStrip1"; // - // toolStripStatusIsValid - // - this.toolStripStatusIsValid.BorderSides = System.Windows.Forms.ToolStripStatusLabelBorderSides.Right; - this.toolStripStatusIsValid.BorderStyle = System.Windows.Forms.Border3DStyle.Raised; - this.toolStripStatusIsValid.Image = global::OpenEphys.Onix1.Design.Properties.Resources.StatusReadyImage; - this.toolStripStatusIsValid.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; - this.toolStripStatusIsValid.Name = "toolStripStatusIsValid"; - this.toolStripStatusIsValid.Size = new System.Drawing.Size(186, 20); - this.toolStripStatusIsValid.Text = "Valid stimulus sequence"; - this.toolStripStatusIsValid.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; - // // toolStripStatusSlotsUsed // this.toolStripStatusSlotsUsed.Name = "toolStripStatusSlotsUsed"; @@ -460,7 +449,7 @@ private void InitializeComponent() this.tabControlVisualization.Name = "tabControlVisualization"; this.tableLayoutPanel1.SetRowSpan(this.tabControlVisualization, 2); this.tabControlVisualization.SelectedIndex = 0; - this.tabControlVisualization.Size = new System.Drawing.Size(1090, 675); + this.tabControlVisualization.Size = new System.Drawing.Size(1090, 673); this.tabControlVisualization.TabIndex = 6; // // tabPageWaveform @@ -470,7 +459,7 @@ private void InitializeComponent() this.tabPageWaveform.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); this.tabPageWaveform.Name = "tabPageWaveform"; this.tabPageWaveform.Padding = new System.Windows.Forms.Padding(3, 2, 3, 2); - this.tabPageWaveform.Size = new System.Drawing.Size(1082, 646); + this.tabPageWaveform.Size = new System.Drawing.Size(1082, 644); this.tabPageWaveform.TabIndex = 0; this.tabPageWaveform.Text = "Stimulus Waveform"; this.tabPageWaveform.UseVisualStyleBackColor = true; @@ -488,7 +477,7 @@ private void InitializeComponent() this.zedGraphWaveform.ScrollMinX = 0D; this.zedGraphWaveform.ScrollMinY = 0D; this.zedGraphWaveform.ScrollMinY2 = 0D; - this.zedGraphWaveform.Size = new System.Drawing.Size(1076, 642); + this.zedGraphWaveform.Size = new System.Drawing.Size(1076, 640); this.zedGraphWaveform.TabIndex = 4; this.zedGraphWaveform.UseExtendedPrintDialog = true; // @@ -529,7 +518,7 @@ private void InitializeComponent() this.panelProbe.Location = new System.Drawing.Point(1099, 2); this.panelProbe.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); this.panelProbe.Name = "panelProbe"; - this.panelProbe.Size = new System.Drawing.Size(445, 401); + this.panelProbe.Size = new System.Drawing.Size(445, 399); this.panelProbe.TabIndex = 0; // // menuStrip @@ -540,7 +529,7 @@ private void InitializeComponent() this.menuStrip.Location = new System.Drawing.Point(0, 0); this.menuStrip.Name = "menuStrip"; this.menuStrip.Padding = new System.Windows.Forms.Padding(5, 2, 0, 2); - this.menuStrip.Size = new System.Drawing.Size(1547, 28); + this.menuStrip.Size = new System.Drawing.Size(1547, 30); this.menuStrip.TabIndex = 7; this.menuStrip.Text = "menuStrip1"; // @@ -549,7 +538,7 @@ private void InitializeComponent() this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.stimulusWaveformToolStripMenuItem}); this.fileToolStripMenuItem.Name = "fileToolStripMenuItem"; - this.fileToolStripMenuItem.Size = new System.Drawing.Size(46, 24); + this.fileToolStripMenuItem.Size = new System.Drawing.Size(46, 26); this.fileToolStripMenuItem.Text = "File"; // // stimulusWaveformToolStripMenuItem @@ -585,21 +574,21 @@ private void InitializeComponent() this.tableLayoutPanel1.Controls.Add(this.tabControlVisualization, 0, 0); this.tableLayoutPanel1.Controls.Add(this.flowLayoutPanel1, 0, 2); this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; - this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 28); + this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 30); this.tableLayoutPanel1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); this.tableLayoutPanel1.Name = "tableLayoutPanel1"; this.tableLayoutPanel1.RowCount = 3; this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 274F)); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 42F)); - this.tableLayoutPanel1.Size = new System.Drawing.Size(1547, 721); + this.tableLayoutPanel1.Size = new System.Drawing.Size(1547, 719); this.tableLayoutPanel1.TabIndex = 8; // // groupBox1 // this.groupBox1.Controls.Add(this.panelParameters); this.groupBox1.Dock = System.Windows.Forms.DockStyle.Fill; - this.groupBox1.Location = new System.Drawing.Point(1099, 407); + this.groupBox1.Location = new System.Drawing.Point(1099, 405); this.groupBox1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); this.groupBox1.Name = "groupBox1"; this.groupBox1.Padding = new System.Windows.Forms.Padding(3, 2, 3, 2); @@ -615,12 +604,23 @@ private void InitializeComponent() this.flowLayoutPanel1.Controls.Add(this.buttonOk); this.flowLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; this.flowLayoutPanel1.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; - this.flowLayoutPanel1.Location = new System.Drawing.Point(3, 681); + this.flowLayoutPanel1.Location = new System.Drawing.Point(3, 679); this.flowLayoutPanel1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); this.flowLayoutPanel1.Name = "flowLayoutPanel1"; this.flowLayoutPanel1.Size = new System.Drawing.Size(1541, 38); this.flowLayoutPanel1.TabIndex = 7; // + // toolStripStatusIsValid + // + this.toolStripStatusIsValid.BorderSides = System.Windows.Forms.ToolStripStatusLabelBorderSides.Right; + this.toolStripStatusIsValid.BorderStyle = System.Windows.Forms.Border3DStyle.Raised; + this.toolStripStatusIsValid.Image = global::OpenEphys.Onix1.Design.Properties.Resources.StatusReadyImage; + this.toolStripStatusIsValid.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; + this.toolStripStatusIsValid.Name = "toolStripStatusIsValid"; + this.toolStripStatusIsValid.Size = new System.Drawing.Size(186, 20); + this.toolStripStatusIsValid.Text = "Valid stimulus sequence"; + this.toolStripStatusIsValid.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // // Rhs2116StimulusSequenceDialog // this.AccessibleDescription = ""; diff --git a/OpenEphys.Onix1.Design/Rhs2116StimulusSequenceDialog.cs b/OpenEphys.Onix1.Design/Rhs2116StimulusSequenceDialog.cs index 178a5fd..5709415 100644 --- a/OpenEphys.Onix1.Design/Rhs2116StimulusSequenceDialog.cs +++ b/OpenEphys.Onix1.Design/Rhs2116StimulusSequenceDialog.cs @@ -619,18 +619,6 @@ private void SetPercentOfSlotsUsed() private void ButtonAddPulses_Click(object sender, EventArgs e) { - if (ChannelDialog.SelectedContacts.All(x => x)) - { - DialogResult result = MessageBox.Show("Caution: All channels are currently selected, and all " + - "settings will be applied to all channels if you continue. Press Okay to add pulse settings to all channels, or Cancel to keep them as is", - "Set all channel settings?", MessageBoxButtons.OKCancel); - - if (result == DialogResult.Cancel) - { - return; - } - } - if (ChannelDialog.SelectedContacts.All(x => x == false)) { MessageBox.Show("No contacts selected. Please select contact(s) before trying to add pulses."); @@ -650,14 +638,44 @@ private void ButtonAddPulses_Click(object sender, EventArgs e) var validAnodicAmplitude = GetSampleFromAmplitude(currentAnodicAmplitude, out var newAnodicSteps); var validCathodicAmplitude = GetSampleFromAmplitude(currentAnodicAmplitude, out var newCathodicSteps); - return (ValidAmplitudes: validAnodicAmplitude && newAnodicSteps != 0 && validCathodicAmplitude && newCathodicSteps != 0, + var anodicError = CalculateAmplitudePercentError(currentAnodicAmplitude, StepSize); + var cathodicError = CalculateAmplitudePercentError(currentCathodicAmplitude, StepSize); + + return (ValidAmplitude: validAnodicAmplitude && newAnodicSteps != 0 && validCathodicAmplitude && newCathodicSteps != 0, s.Index, - s.Stimulus, + ErrorAnodic: anodicError, + ErrorCathodic: cathodicError, NewAnodicSteps: newAnodicSteps, NewCathodicSteps: newCathodicSteps); }); - foreach (var (ValidAmplitudes, Index, Stimulus, NewAnodicSteps, NewCathodicSteps) in stimuli) + //if (stimuli.All(s => s.ValidAmplitude)) + //{ + var amplitudeError = stimuli.Select(s => (s.ErrorAnodic, s.ErrorCathodic)); + + if (amplitudeError.Any(e => e.ErrorCathodic != 0 || e.ErrorAnodic != 0)) + { + var message = $"The step size is changing from {GetStepSizeStringuA(Sequence.CurrentStepSize)} to {GetStepSizeStringuA(StepSize)}, " + + $"which will adjust some amplitudes. If applied, the following values will be modified:\n"; + + foreach (var (ValidAmplitudes, Index, ErrorAnodic, ErrorCathodic, NewAnodicSteps, NewCathodicSteps) in stimuli) + { + if (ErrorAnodic != 0 || ErrorCathodic != 0) + { + message += $"\nChannel {Index}: Anode = {GetAmplitudeFromSample(Sequence.Stimuli[Index].AnodicAmplitudeSteps, Sequence.CurrentStepSize)} µA → {GetAmplitudeFromSample(NewAnodicSteps, StepSize)} µA," + + $" Cathode = {GetAmplitudeFromSample(Sequence.Stimuli[Index].CathodicAmplitudeSteps, Sequence.CurrentStepSize)} µA → {GetAmplitudeFromSample(NewCathodicSteps, StepSize)} µA"; + } + } + + message += "\n\nClick Update to update these channels, or Cancel to stop."; + + CustomMessageBox messageBox = new(message, "New Amplitude Values", "Update"); + var result = messageBox.ShowDialog(); + + if (result == DialogResult.Cancel) return; + } + + foreach (var (ValidAmplitudes, Index, ErrorAnodic, ErrorCathodic, NewAnodicSteps, NewCathodicSteps) in stimuli) { if (ValidAmplitudes) { @@ -666,17 +684,15 @@ private void ButtonAddPulses_Click(object sender, EventArgs e) } else { - var result = MessageBox.Show($"The new amplitude ({GetAmplitudeString((byte)textboxAmplitudeAnodic.Tag) + " µA"}) is using a step size of {GetStepSizeStringuA(StepSize)}," + - $" but channel {Index} ({GetAmplitudeString(Stimulus.AnodicAmplitudeSteps, Sequence.CurrentStepSize) + " µA"}) cannot be defined with this step size. " + - $"Press Ok to clear channel {Index}, or Cancel to stop adding this sequence.", - "Amplitude Out of Range", MessageBoxButtons.OKCancel); - - if (result == DialogResult.Cancel) + if (NewAnodicSteps == 0 && NewCathodicSteps == 0) { - return; + Sequence.Stimuli[Index].Clear(); + } + else + { + Sequence.Stimuli[Index].AnodicAmplitudeSteps = NewAnodicSteps; + Sequence.Stimuli[Index].CathodicAmplitudeSteps = NewCathodicSteps; } - - Sequence.Stimuli[Index].Clear(); } } } @@ -882,16 +898,31 @@ private bool GetSampleFromTime(double value, out uint samples) /// Get the number of samples needed at the current step size to represent a given amplitude. /// /// Double value defining the amplitude in microamps. + /// /// Output returning the number of samples as a byte. /// Returns true if the number of samples is a valid byte value (between 0 and 255). Returns false if the number of samples cannot be represented in byte format. - private bool GetSampleFromAmplitude(double value, out byte samples) + private bool GetSampleFromAmplitude(double value, Rhs2116StepSize stepSize, out byte samples) { - var ratio = value / Rhs2116StimulusSequence.GetStepSizeuA(StepSize); - samples = (byte)Math.Round(ratio); + var ratio = GetRatio(value, Rhs2116StimulusSequence.GetStepSizeuA(stepSize)); + + if (ratio >= 255) samples = 255; + else if (ratio <= 0) samples = 0; + else samples = (byte)Math.Round(ratio); return !(ratio > byte.MaxValue || ratio < 0); } + private double GetRatio(double value1, double value2) + { + return value1 / value2; + } + + /// + private bool GetSampleFromAmplitude(double value, out byte samples) + { + return GetSampleFromAmplitude(value, StepSize, out samples); + } + private double GetTimeFromSample(uint value) { return value * SamplePeriodMilliSeconds; @@ -932,7 +963,7 @@ private void Amplitude_TextChanged(object sender, EventArgs e) if (!UpdateStepSizeFromAmplitude(result)) { textBox.Text = result > MaxAmplitudeuA ? MaxAmplitudeuA.ToString() : "0"; - textBox.Tag = result > MaxAmplitudeuA ? 255 : 0; + textBox.Tag = result > MaxAmplitudeuA ? (byte)255 : (byte)0; return; } @@ -991,58 +1022,84 @@ private bool UpdateStepSizeFromAmplitude(double amplitude) // NB: Update step size to a value that supports the requested amplitude. var possibleStepSizes = Enum.GetValues(typeof(Rhs2116StepSize)) .Cast() - .Where(s => + .Where(stepSize => { - return IsValidNumberOfSteps(GetNumberOfSteps(amplitude, s)); + var isValidSequence = Sequence.Stimuli + .Select(stimulus => + { + var amplitude = GetAmplitudeFromSample(stimulus.AnodicAmplitudeSteps, Sequence.CurrentStepSize); + + if (amplitude == 0) return true; + + var steps = GetNumberOfSteps(amplitude, stepSize); + + return IsValidNumberOfSteps(steps); + }) + .All(s => s); + + return IsValidNumberOfSteps(GetNumberOfSteps(amplitude, stepSize)) && isValidSequence; }); if (possibleStepSizes.Count() == 1) { StepSize = possibleStepSizes.First(); + + textBoxStepSize.Text = GetStepSizeStringuA(StepSize); + return true; } - else - { - if (possibleStepSizes.Contains(Sequence.CurrentStepSize)) - { - StepSize = Sequence.CurrentStepSize; - } - else - { - // NB: Search through the possible step sizes and try to find one that matches all current amplitudes - var validStepSizes = possibleStepSizes.Where(s => - { - var numberOfStimuli = Sequence.Stimuli.Length; - bool[] isValid = new bool[numberOfStimuli]; + // NB: Calculate the max error at each step size, and choose the step size with the smallest maximum error + var result = Enum.GetValues(typeof(Rhs2116StepSize)) + .Cast() + .Select(stepSize => + { + var anodicError = Sequence.Stimuli + .Select(stimulus => + { + var amplitude = GetAmplitudeFromSample(stimulus.AnodicAmplitudeSteps, Sequence.CurrentStepSize); + var steps = GetNumberOfSteps(amplitude, stepSize); - for (int i = 0; i < numberOfStimuli; i++) - { - isValid[i] = IsValidNumberOfSteps(GetNumberOfSteps(GetAmplitudeFromSample(Sequence.Stimuli[i].AnodicAmplitudeSteps, Sequence.CurrentStepSize), s)); - } + // NB: To prevent deadlocks where the step size can never increase or decrease, return a 0 error if the amplitude is out of bounds + if (!IsValidNumberOfSteps(steps)) return 0; - return isValid.All(i => i); - }); + return CalculateAmplitudePercentError(amplitude, stepSize); + }) + .Max(); - if (!validStepSizes.Any()) - { - MessageBox.Show("No step size found that fits all existing and new amplitudes. " + - "Either clear existing stimuli that fall outside the range of the new step size, or modify " + - "the new amplitude.", "Invalid Amplitude"); + var cathodicError = Sequence.Stimuli + .Select(stimulus => + { + var amplitude = GetAmplitudeFromSample(stimulus.AnodicAmplitudeSteps, Sequence.CurrentStepSize); + var steps = GetNumberOfSteps(amplitude, stepSize); - StepSize = possibleStepSizes.First(); - } - else - { - StepSize = validStepSizes.First(); - } - } + // NB: To prevent deadlocks where the step size can never increase or decrease, return a 0 error if the amplitude is out of bounds + if (!IsValidNumberOfSteps(steps)) return 0; + + return CalculateAmplitudePercentError(amplitude, stepSize); + }) + .Max(); + + var maxError = Math.Max(anodicError, cathodicError); + + var newAmplitudeError = CalculateAmplitudePercentError(amplitude, stepSize); + + return (stepSize, MaxError: Math.Max(maxError, newAmplitudeError)); + }) + .OrderBy(val => val.MaxError); + + if (result.Select(val => val.MaxError).Distinct().Count() == result.Count()) + { + StepSize = result.Select(val => val.stepSize).First(); + } + else + { + var error = result.Select(val => val.MaxError).Distinct().First(); + StepSize = result.TakeWhile(val => val.MaxError == error).Select(val => val.stepSize).Last(); } textBoxStepSize.Text = GetStepSizeStringuA(StepSize); - return true; } - private bool IsValidNumberOfSteps(int numberOfSteps) { return numberOfSteps > 0 && numberOfSteps <= 255; @@ -1053,6 +1110,17 @@ private int GetNumberOfSteps(double amplitude, Rhs2116StepSize stepSize) return (int)(amplitude / Rhs2116StimulusSequence.GetStepSizeuA(stepSize)); } + private double CalculateAmplitudePercentError(double amplitude, Rhs2116StepSize stepSize) + { + if (amplitude == 0) return 0; + + var stepSizeuA = Rhs2116StimulusSequence.GetStepSizeuA(stepSize); + + GetSampleFromAmplitude(amplitude, stepSize, out var steps); + + return 100 * ((amplitude - steps * stepSizeuA) / amplitude); + } + private void Checkbox_CheckedChanged(object sender, EventArgs e) { if (checkboxBiphasicSymmetrical.Checked) @@ -1089,23 +1157,9 @@ private void Checkbox_CheckedChanged(object sender, EventArgs e) private void ButtonClearPulses_Click(object sender, EventArgs e) { - if (ChannelDialog.SelectedContacts.All(x => x == false) || ChannelDialog.SelectedContacts.All(x => x == true)) - { - DialogResult result = MessageBox.Show("Caution: All channels are currently selected, and all " + - "settings will be cleared if you continue. Press Okay to clear all pulse settings, or Cancel to keep them", - "Remove all channel settings?", MessageBoxButtons.OKCancel); - - if (result == DialogResult.Cancel) - { - return; - } - } - - var clearAllContacts = ChannelDialog.SelectedContacts.All(x => x == false); - for (int i = 0; i < ChannelDialog.SelectedContacts.Length; i++) { - if (ChannelDialog.SelectedContacts[i] || clearAllContacts) + if (ChannelDialog.SelectedContacts[i]) { Sequence.Stimuli[i].Clear(); } @@ -1175,7 +1229,7 @@ private void MenuItemSaveFile_Click(object sender, EventArgs e) { if (!Sequence.Valid) { - var result = MessageBox.Show("Warning: Not all stimuli are valid; are you sure you want to save this file?", + var result = MessageBox.Show("Warning: Not all stimuli are valid; are you sure you want to save this file?", "Invalid Stimuli", MessageBoxButtons.YesNo, MessageBoxIcon.Error); if (result == DialogResult.No) return;