function three_neuron_circuit_sim % 3-neuron conductance network UI (compact, robust playback) % - Three plots (A,B,C), fixed y [-80, 40] mV, linked x % - DC sliders, momentary pulses (buttons + A/B/C keys) % - Drug toggles (TTX / Picrotoxin / CNQX) with LED lamps + Reset % - Play/Stop + Playback speed (0.1x..20x), visible spike peaks, final-frame flush %% ----------------- Model params ----------------- P.T = 2.0; % s total P.dt = 1e-4; % s step (0.1 ms) P.t = 0:P.dt:P.T; P.nT = numel(P.t); % Intrinsic (per neuron) leakConductance = 1e-14 ; % P.Cm = 150e-12; % F P.Cm = 100e-12; % F P.gL = [12;12;12]*leakConductance; % S [A;B;C] P.EL = -65e-3; % V P.Vreset = -70e-3; % V P.Vthr0 = -48e-3; % V P.dVthr = 12e-3; % V P.tau_thr = 0.12; % s P.t_ref = 0.002; % s % Synapses (bi-exponential) P.E_AMPA = 0e-3; P.E_GABA = -70e-3; P.tau_e = 0.005; P.rise_e = 0.001; P.alpha_e = 1/(P.rise_e*P.tau_e); P.tau_i = 0.010; P.rise_i = 0.002; P.alpha_i = 1/(P.rise_i*P.tau_i); % Synaptic weights (S) excitSynConductance = 1e-11 ; inhibSynConductance = 5e-9 ; P.w_A_to_B_exc = excitSynConductance; % A->B (AMPA) P.w_C_to_A_exc = excitSynConductance; % C->A (AMPA) P.w_C_to_B_exc = excitSynConductance; % C->B (AMPA) P.w_B_to_A_inh = inhibSynConductance; % B->A (GABA) % Animation P.play_speed = 1.0; % playback speed factor (0.1x ... 20x) P.showSpikeMv = 30e-3; % V (plot-only spike marker height) %% ---- Figure with scrollable shell + fixed minimum content ---- %% ---- Figure + responsive main grid (no fixed content canvas) ---- f = uifigure('Name','3-Neuron Circuit','Color','w','Position',[100 100 900 600]); % Main grid: plots left, controls right. This grid resizes with the window. g = uigridlayout(f,[3 2], ... 'RowHeight',{'1x','1x','1x'}, ... 'ColumnWidth',{'3x','2x'}, ... 'Padding',6,'RowSpacing',6,'ColumnSpacing',8); %% ----------------- Axes (A/B/C) ----------------- axA = uiaxes(g); axA.Layout.Row=1; axA.Layout.Column=1; setupAxis(axA,'Neuron A',P); axB = uiaxes(g); axB.Layout.Row=2; axB.Layout.Column=1; setupAxis(axB,'Neuron B',P); axC = uiaxes(g); axC.Layout.Row=3; axC.Layout.Column=1; setupAxis(axC,'Neuron C',P); linkaxes([axA,axB,axC],'x'); % Pre-create lines lnA = plot(axA,nan,nan,'LineWidth',1.2); lnB = plot(axB,nan,nan,'LineWidth',1.2); lnC = plot(axC,nan,nan,'LineWidth',1.2); %% ----------------- Right side: 3 rows x 2 cols ----------------- % --- Right side: scrollable column with a fixed-minimum canvas --- rightScroll = uipanel(g, ... 'Scrollable','on', ... 'AutoResizeChildren','off'); % critical: don't auto-shrink children rightScroll.Layout.Row = [1 3]; rightScroll.Layout.Column = 2; % A canvas that can be taller than the viewport -> triggers scrollbars minRightHeight = 720; % tweak this to taste rightCanvas = uipanel(rightScroll, ... 'Units','pixels', ... 'Position',[0 0 1 minRightHeight], ... 'AutoResizeChildren','on'); % Keep the canvas at least minRightHeight tall; match width to viewport rightScroll.SizeChangedFcn = @(~,~) ... set(rightCanvas,'Position',[0, 0, rightScroll.InnerPosition(3), ... max(minRightHeight, rightScroll.InnerPosition(4))]); % Your existing right-hand grid now lives INSIDE the canvas, not directly in g rightStack = uigridlayout(rightCanvas,[3 2], ... 'RowHeight',{200, 260, 140}, ... % pixel heights ensure a real min size 'ColumnWidth',{'1x','1x'}, ... 'Padding',6,'RowSpacing',6,'ColumnSpacing',6); % (1) DC Currents (Row 1, span both columns) RH_DC_01 = 20 ; RH_DC_02 = 40 ; pDC = uipanel(rightStack,'Title','DC Current (pA)'); pDC.Layout.Row=1; pDC.Layout.Column=[1 2]; gc = uigridlayout(pDC,[6 2],'Padding',5,'RowSpacing',5,'ColumnWidth',{'1x',52},'RowHeight',{RH_DC_01,RH_DC_02,RH_DC_01,RH_DC_02,RH_DC_01,RH_DC_02}); uilabel(gc,'Text','A','FontWeight','bold'); rdA = uilabel(gc,'Text','0','HorizontalAlignment','right'); sA = uislider(gc,'Limits',[-300 300],'Value',0,'MajorTicks',-300:150:300); sA.Layout.Column=[1 2]; uilabel(gc,'Text','B','FontWeight','bold'); rdB = uilabel(gc,'Text','0','HorizontalAlignment','right'); sB = uislider(gc,'Limits',[-300 300],'Value',0,'MajorTicks',-300:150:300); sB.Layout.Column=[1 2]; uilabel(gc,'Text','C','FontWeight','bold'); rdC = uilabel(gc,'Text','0','HorizontalAlignment','right'); sC = uislider(gc,'Limits',[-300 300],'Value',0,'MajorTicks',-300:150:300); sC.Layout.Column=[1 2]; sA.ValueChangedFcn = @(src,~) set(rdA,'Text',sprintf('%+4.0f',src.Value)); sB.ValueChangedFcn = @(src,~) set(rdB,'Text',sprintf('%+4.0f',src.Value)); sC.ValueChangedFcn = @(src,~) set(rdC,'Text',sprintf('%+4.0f',src.Value)); % (2) Pulses (Row 2, Column 1) pPulse = uipanel(rightStack,'Title','Pulses'); pPulse.Layout.Row=2; pPulse.Layout.Column=1; RH_Pulses_01 = 20 ; RH_Pulses_02 = 20 ; gp = uigridlayout(pPulse,[6 3],'Padding',6,'RowSpacing',6,'ColumnWidth',{72,66,'1x'},'RowHeight',{RH_Pulses_01,RH_Pulses_02,RH_Pulses_01,RH_Pulses_02,RH_Pulses_01,RH_Pulses_02}); uilabel(gp,'Text','Pulse A','FontWeight','bold'); btnPulseA = uibutton(gp,'state','Text','OFF','Value',false,'ValueChangedFcn',@(bt,~) set(bt,'Text',tern(bt.Value,'ON','OFF'))); ampA = uieditfield(gp,'numeric','Limits',[-500 500],'Value',150,'ValueDisplayFormat','%+g'); ampA.Layout.Column=3; ampA.Layout.Row=1; uilabel(gp,'Text','Pulse B','FontWeight','bold'); btnPulseB = uibutton(gp,'state','Text','OFF','Value',false,'ValueChangedFcn',@(bt,~) set(bt,'Text',tern(bt.Value,'ON','OFF'))); ampB = uieditfield(gp,'numeric','Limits',[-500 500],'Value',150,'ValueDisplayFormat','%+g'); ampB.Layout.Column=3; ampB.Layout.Row=2; uilabel(gp,'Text','Pulse C','FontWeight','bold'); btnPulseC = uibutton(gp,'state','Text','OFF','Value',false,'ValueChangedFcn',@(bt,~) set(bt,'Text',tern(bt.Value,'ON','OFF'))); ampC = uieditfield(gp,'numeric','Limits',[-500 500],'Value',150,'ValueDisplayFormat','%+g'); ampC.Layout.Column=3; ampC.Layout.Row=3; % uilabel(pPulse,'Text','Hold keys A/B/C to inject while held.','FontAngle','italic','Position',[6 2 220 16]); % (2b) Pharmacology (Row 2, Column 2) with LEDs + Reset pDrug = uipanel(rightStack,'Title','Pharmacology'); pDrug.Layout.Row=2; pDrug.Layout.Column=2; gd = uigridlayout(pDrug,[4 3],'Padding',6,'RowSpacing',6,'ColumnWidth',{18,'1x',100},'RowHeight',{24,24,24,24}); LED_ON=[0 0.75 0]; LED_OFF=[0.6 0.6 0.6]; lampTTX = uilamp(gd); lampTTX.Color=LED_OFF; btnTTX = uibutton(gd,'state','Text','TTX: OFF','Value',false); btnTTX.ValueChangedFcn = @(bt,~) drugToggle(bt,lampTTX,'TTX',LED_ON,LED_OFF); lampPIC = uilamp(gd); lampPIC.Color=LED_OFF; btnPicro= uibutton(gd,'state','Text','Picrotoxin: OFF','Value',false); btnPicro.ValueChangedFcn= @(bt,~) drugToggle(bt,lampPIC,'Picrotoxin',LED_ON,LED_OFF); lampCNQX= uilamp(gd); lampCNQX.Color=LED_OFF; btnCNQX = uibutton(gd,'state','Text','CNQX: OFF','Value',false); btnCNQX.ValueChangedFcn = @(bt,~) drugToggle(bt,lampCNQX,'CNQX',LED_ON,LED_OFF); btnResetDrugs = uibutton(gd,'Text','Reset','ButtonPushedFcn',@(~,~) resetAllDrugs()); lampTTX.Layout.Row=1; lampTTX.Layout.Column=1; btnTTX.Layout.Row=1; btnTTX.Layout.Column=[2 3]; lampPIC.Layout.Row=2; lampPIC.Layout.Column=1; btnPicro.Layout.Row=2; btnPicro.Layout.Column=[2 3]; lampCNQX.Layout.Row=3; lampCNQX.Layout.Column=1; btnCNQX.Layout.Row=3; btnCNQX.Layout.Column=[2 3]; btnResetDrugs.Layout.Row=4; btnResetDrugs.Layout.Column=[2 3]; % (3) Run controls (Row 3, span both columns) pRun = uipanel(rightStack,'Title','Run'); pRun.Layout.Row=3; pRun.Layout.Column=[1 2]; RW_Play_01 = 30 ; RW_Play_02 = 40 ; gr = uigridlayout(pRun,[2 3],'Padding',6,'RowSpacing',6,'ColumnWidth',{86,86,'1x'},'RowHeight',{RW_Play_01,RW_Play_02}); btnPlay = uibutton(gr,'push','Text','Play','ButtonPushedFcn',@(~,~) runOnce()); btnStop = uibutton(gr,'push','Text','Stop','ButtonPushedFcn',@(~,~) setappdata(f,'stopFlag',true),'Enable','off'); lbl = uilabel(gr,'Text','Ready','HorizontalAlignment','right'); lbl.Layout.Column=3; % Playback speed (0.1x .. 20x), visibly different at low vs high lblSpeed = uilabel(gr,'Text','Speed (×): 1.00','HorizontalAlignment','left'); sSpeed = uislider(gr, ... 'Limits',[0.1 20], ... 'Value',P.play_speed, ... 'MajorTicks',[0.1 2 5 10 20], ... 'MinorTicks',[], ... 'ValueChangingFcn',@(s,evt) set(lblSpeed,'Text',sprintf('Speed (×): %.2f',evt.Value)) ); sSpeed.Layout.Column=[2 3]; %% ----------------- Keyboard hold-to-inject ----------------- set(f,'WindowKeyPressFcn', @(~,evt) keyHold(evt,true)); set(f,'WindowKeyReleaseFcn',@(~,evt) keyHold(evt,false)); setappdata(f,'keysHeld',struct('a',false,'b',false,'c',false)); %% ----------------- Store shared handles ----------------- setappdata(f,'P',P); setappdata(f,'axes',{axA,axB,axC}); setappdata(f,'lines',{lnA,lnB,lnC}); setappdata(f,'sDC',{sA,sB,sC}); setappdata(f,'pulseBtns',{btnPulseA,btnPulseB,btnPulseC}); setappdata(f,'pulseAmps',{ampA,ampB,ampC}); setappdata(f,'drugBtns',struct('TTX',btnTTX,'Picro',btnPicro,'CNQX',btnCNQX)); setappdata(f,'drugLamps',struct('TTX',lampTTX,'Picro',lampPIC,'CNQX',lampCNQX)); setappdata(f,'drugColors',struct('ON',LED_ON,'OFF',LED_OFF)); setappdata(f,'speedSlider',sSpeed); setappdata(f,'statusLabel',lbl); %% ----------------- Callbacks ----------------- function runOnce() P = getappdata(f,'P'); t = P.t; sDC = getappdata(f,'sDC'); IA = sDC{1}.Value*1e-12; IB = sDC{2}.Value*1e-12; IC = sDC{3}.Value*1e-12; Ibase = [IA; IB; IC]; btnPlay.Enable='off'; btnStop.Enable='on'; sDC{1}.Enable='off'; sDC{2}.Enable='off'; sDC{3}.Enable='off'; setappdata(f,'stopFlag',false); lbl = getappdata(f,'statusLabel'); lbl.Text = sprintf('Running: I_A=%+g pA, I_B=%+g pA, I_C=%+g pA', sDC{1}.Value, sDC{2}.Value, sDC{3}.Value); [VA_plot,VB_plot,VC_plot] = simulateAndAnimate(Ibase); % Final frame flush L = getappdata(f,'lines'); tms = t*1e3; set(L{1},'XData',tms,'YData',VA_plot*1e3); set(L{2},'XData',tms,'YData',VB_plot*1e3); set(L{3},'XData',tms,'YData',VC_plot*1e3); drawnow; if getappdata(f,'stopFlag'), lbl.Text='Stopped'; else, lbl.Text='Done'; end btnPlay.Enable='on'; btnStop.Enable='off'; sDC{1}.Enable='on'; sDC{2}.Enable='on'; sDC{3}.Enable='on'; end function [VA_plot,VB_plot,VC_plot] = simulateAndAnimate(Ibase) P = getappdata(f,'P'); t = P.t; L = getappdata(f,'lines'); lnA=L{1}; lnB=L{2}; lnC=L{3}; pulseBtns = getappdata(f,'pulseBtns'); pulseAmps = getappdata(f,'pulseAmps'); drugBtns = getappdata(f,'drugBtns'); axs = getappdata(f,'axes'); axA=axs{1}; % Playback speed factor (0.1x .. 20x) speed = max(0.1, getappdata(f,'speedSlider').Value); frameSkip = max(1, round(speed * 6)); % ~6 at 1x, ~120 at 20x pausePerFrame = 0; % default: no pause= 1:P if speed < 1 % gentle pause only when slowing down pausePerFrame = min(0.02, 0.008 / speed); % 8 ms @1x -> 80 ms @0.1x (capped at 20 ms) end % NEW: UI event poll cadence (process button changes even if not drawing) uiPollEvery = 10; % steps between event polls (~1 ms if dt=0.1 ms) % State V = repmat(P.EL,3,1); Vthr = repmat(P.Vthr0,3,1); tref = zeros(3,1); rE = zeros(3,1); sE = zeros(3,1); rI = zeros(3,1); sI = zeros(3,1); VA = zeros(1,P.nT); VB = zeros(1,P.nT); VC = zeros(1,P.nT); VA_plot = VA; VB_plot = VB; VC_plot = VC; % Clear traces; with linked x, set once on A set(lnA,'XData',[],'YData',[]); set(lnB,'XData',[],'YData',[]); set(lnC,'XData',[],'YData',[]); xlim(axA,[0 t(end)*1e3]); for k = 1:P.nT % Process queued UI events regularly for snappy buttons if mod(k, uiPollEvery) == 0 drawnow limitrate; % processes callbacks without heavy repaints end % Quick stop check (stop button sets stopFlag) if getappdata(f,'stopFlag') % draw whatever we have to the plots, then exit cleanly tms = t(1:max(k-1,1))*1e3; set(lnA,'XData',tms,'YData',VA_plot(1:max(k-1,1))*1e3); set(lnB,'XData',tms,'YData',VB_plot(1:max(k-1,1))*1e3); set(lnC,'XData',tms,'YData',VC_plot(1:max(k-1,1))*1e3); drawnow; return; end % Drugs TTX_on = drugBtns.TTX.Value; PICRO_on = drugBtns.Picro.Value; CNQX_on = drugBtns.CNQX.Value; % Currents (base + pulses + key holds) I = Ibase; if pulseBtns{1}.Value, I(1)=I(1)+pulseAmps{1}.Value*1e-12; end if pulseBtns{2}.Value, I(2)=I(2)+pulseAmps{2}.Value*1e-12; end if pulseBtns{3}.Value, I(3)=I(3)+pulseAmps{3}.Value*1e-12; end KH = getappdata(f,'keysHeld'); if KH.a, I(1)=I(1)+pulseAmps{1}.Value*1e-12; end if KH.b, I(2)=I(2)+pulseAmps{2}.Value*1e-12; end if KH.c, I(3)=I(3)+pulseAmps{3}.Value*1e-12; end % Record current Vm (and default plot values) VA(k)=V(1); VB(k)=V(2); VC(k)=V(3); VA_plot(k)=V(1); VB_plot(k)=V(2); VC_plot(k)=V(3); % Refractory countdown tref = max(0, tref - P.dt); % Receptor block (instant) if CNQX_on, rE(:)=0; sE(:)=0; end if PICRO_on, rI(:)=0; sI(:)=0; end % Syn conductances for this step gE = sE; if CNQX_on, gE(:)=0; end gI = sI; if PICRO_on, gI(:)=0; end % Membrane ODE dV = ( -P.gL.*(V-P.EL) - gE.*(V-P.E_AMPA) - gI.*(V-P.E_GABA) + I ) ./ P.Cm; % Refractory clamp mask_ref = (tref > 0); dV(mask_ref)=0; V(mask_ref)=P.Vreset; % Integrate membrane V = V + P.dt*dV; % Threshold relaxation Vthr = Vthr + P.dt*((P.Vthr0 - Vthr)/P.tau_thr); % Spike detection (TTX blocks spiking) if TTX_on, spk = [false;false;false]; else, spk = (V >= Vthr) & (tref == 0); end % Spike events (with receptor gating) if spk(1) VA_plot(k)=P.showSpikeMv; V(1)=P.Vreset; Vthr(1)=Vthr(1)+P.dVthr; tref(1)=P.t_ref; if ~CNQX_on, rE(2)=rE(2)+P.w_A_to_B_exc*P.alpha_e; sE(2)=sE(2)+P.w_A_to_B_exc; end end if spk(2) VB_plot(k)=P.showSpikeMv; V(2)=P.Vreset; Vthr(2)=Vthr(2)+P.dVthr; tref(2)=P.t_ref; if ~PICRO_on, rI(1)=rI(1)+P.w_B_to_A_inh*P.alpha_i; sI(1)=sI(1)+P.w_B_to_A_inh; end end if spk(3) VC_plot(k)=P.showSpikeMv; V(3)=P.Vreset; Vthr(3)=Vthr(3)+P.dVthr; tref(3)=P.t_ref; if ~CNQX_on rE(1)=rE(1)+P.w_C_to_A_exc*P.alpha_e; sE(1)=sE(1)+P.w_C_to_A_exc; rE(2)=rE(2)+P.w_C_to_B_exc*P.alpha_e; sE(2)=sE(2)+P.w_C_to_B_exc; end end % Synapse kinetics rE = rE + P.dt*(-rE/P.rise_e); sE = sE + P.dt*(-sE/P.tau_e + rE); rI = rI + P.dt*(-rI/P.rise_i); sI = sI + P.dt*(-sI/P.tau_i + rI); % --------- Animate with speed control (frameSkip + pause) --------- % --------- Animate with speed control (frameSkip + optional pause) --------- if mod(k, frameSkip) == 0 || k == P.nT tms = t(1:k)*1e3; set(lnA,'XData',tms,'YData',VA_plot(1:k)*1e3); set(lnB,'XData',tms,'YData',VB_plot(1:k)*1e3); set(lnC,'XData',tms,'YData',VC_plot(1:k)*1e3); if getappdata(f,'stopFlag') drawnow; % final flush with callbacks return; else drawnow limitrate; % service callbacks & throttle repaint rate if pausePerFrame > 0 pause(pausePerFrame); % only at slow speeds end end end end % Ensure last frame draws fully tms = t*1e3; set(lnA,'XData',tms,'YData',VA_plot*1e3); set(lnB,'XData',tms,'YData',VB_plot*1e3); set(lnC,'XData',tms,'YData',VC_plot*1e3); drawnow; end function keyHold(evt,isDown) KH = getappdata(f,'keysHeld'); if isfield(evt,'Key') switch lower(evt.Key) case 'a', KH.a = isDown; case 'b', KH.b = isDown; case 'c', KH.c = isDown; end setappdata(f,'keysHeld',KH); end end end %% ----------------- Helpers ----------------- function setupAxis(ax,label,P) title(ax,label); xlim(ax,[0 P.t(end)*1e3]); ylim(ax,[-80 40]); yline(ax,P.Vthr0*1e3,'k--','Baseline thr'); xlabel(ax,'Time (ms)'); ylabel(ax,'V_m (mV)'); ax.Box='on'; ax.FontSize=10; end function drugToggle(bt,lamp,labelTxt,ON,OFF) if bt.Value set(bt,'Text',[labelTxt ': ON']); set(lamp,'Color',ON); else set(bt,'Text',[labelTxt ': OFF']); set(lamp,'Color',OFF); end end function resetAllDrugs() db = getappdata(gcf,'drugBtns'); dl = getappdata(gcf,'drugLamps'); C = getappdata(gcf,'drugColors'); set(db.TTX,'Value',false); set(db.TTX,'Text','TTX: OFF'); set(dl.TTX,'Color',C.OFF); set(db.Picro,'Value',false); set(db.Picro,'Text','Picrotoxin: OFF'); set(dl.Picro,'Color',C.OFF); set(db.CNQX,'Value',false); set(db.CNQX,'Text','CNQX: OFF'); set(dl.CNQX,'Color',C.OFF); end function out = tern(cond,a,b) if cond, out=a; else, out=b; end end function localResizeContent(shell, content, baseW, baseH) ip = shell.InnerPosition; % [x y w h] of the scrollable viewport content.Position(3) = max(baseW, ip(3)); % grow with window, but never below baseW content.Position(4) = max(baseH, ip(4)); % grow with window, but never below baseH end