unit uDendriteFrame;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, math,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
  Vcl.StdCtrls, AbNumEdit, {uBodyPlan,} uNeuronTypes,
  Vcl.ExtCtrls,
  JvExControls, System.Generics.Collections,
  SDL_NumLab, PropSaveMain, rImprovedComps, VrControls, VrSlider;

Const
  Vdelta = 1 * 1E-12; // One pA

type
  TDendriteFrame = class(TFrame)
    gbDimensions: TGroupBox;
    lblLength_uM: TLabel;
    lblDiameter_uM: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    btnResetDimensions: TButton;
    nsLength_uM: TAbNumSpin;
    neMembrane_kOhm: TAbNumEdit;

    nsDiameter_uM: TAbNumSpin;
    gbInOut: TGroupBox;
    nlInput_pA: TNumLab;
    nlMaxInput_pA: TNumLab;
    nlOutput_mV: TNumLab;
    gbSTDP: TGroupBox;
    nlWeight: TNumLab;
    slideWeight: TVrSlider;
    rbEPSP: TRadioButton;
    rbIPSP: TRadioButton;
    neMembraneCap_pF: TAbNumEdit;
    neLamda_cM: TAbNumEdit;
    lblLamda_cM: TLabel;
    Label1: TLabel;
    neTau_mS: TAbNumEdit;
    lblTau_mS: TLabel;
    lbl_mS: TLabel;
    btnTest: TButton;
    Button1: TButton;
    neMembraneAxial_kOhm: TAbNumEdit;
    procedure btnResetDimensionsClick(Sender: TObject);
    procedure nsLength_uMClick(Sender: TObject);
    procedure nsDiameter_uMClick(Sender: TObject);
    procedure nsDiameter_uMValueChanged(Sender: TObject);
    procedure nsLength_uMValueChanged(Sender: TObject);
    procedure slideWeightChange(Sender: TObject);
    procedure slideWeightDblClick(Sender: TObject);
    procedure btnTestClick(Sender: TObject);
    procedure neMembraneAxial_kOhmValueChanged(Sender: TObject);

  private
    { Private declarations }
    FStack: TStack<TObject>;
    MaxInput_pA: Single;
    CalcTick: integer;
    ResetX: boolean;
    dt_mS, Term, diffMembrane_mV: Double;
    NumSteps: integer;
    Membrane_mV: Single;
    X: boolean;
    Dendrite, Synapse: boolean;
    Procedure ResetDimensions;

  public
    { Public declarations }
    DebugValue: string;
    Charging, Discharging: boolean;
    StartChargingTick, StopChargingTick, StartDischargingTick, StopDischargingTick: integer;
    StartChargingIin, StopCharginIin: Single;
    StartDischargingIin, StopDischargingIin: Single;
    ChargingTick, DischargingTick: integer;

    VStartCharge, VStartDischarge, DeltaIin, LastIin, target: Single;
    ChargeDischargeTick: integer;
    stackSources: TStack<String>;
    stackInput_mV: TStack<Single>;
    LengthConstant: Single; // Lamda = SQRT(Rm/Ri) also call the space constant
    Membrane_pA: Single; // membrain current [pA]
    Input_pA: Single; // input current [pA]
    VRa: Variant;
    ID: String;
    Length_cM: Single; // [m]
    Diameter_cM: Single; // [m]
    area: Single;
    xarea: Single;
    FOn: boolean;
    bias: integer;
    intTest: integer;

    SetMessage: TSetMessage;
    resetMessage: TresetMessage;
    bAPMessageIn, bAPMessageOut: TbAPMessage;

    TagRec: TTagRec;

    Buf: array [0 .. 50] of Single;
    BufIndex: integer;
    Trys: integer;

    InputRec: TInputRec;
    arrInput_mV: TArray<Single>;
    ThisBodyPart: String;
    Tstr: String; // Test string used in SI converstion

    procedure ProcessSetFunctionMessage(var Msg: TMessage);
    procedure ProcesssetResetFunctionMessage(var Msg: TMessage);
    Procedure Manage_bAP_Message(var Msg: TMessage);
    procedure Init;

    property On: boolean read FOn write FOn;
  end;

implementation

uses uIPSP, uBodyPlan, uBiophysics, StrUtils, uRegister;
{$R *.dfm}

function SI(Value: Single): string;
var
  Exponent: integer;
  AdjustedExponent: integer;
  Mantissa: Single;
begin
  if Value = 0 then
    Exit('0.0E+000'); // Handle zero case

  // Calculate the base-10 logarithm and determine the original exponent
  Exponent := Trunc(Log10(Abs(Value)));
  AdjustedExponent := (Exponent div 3) * 3; // Adjust exponent to the nearest multiple of three
  Mantissa := Value / Power(10, AdjustedExponent); // Calculate the mantissa
  Result := Format('%g*1E%d ', [Mantissa, AdjustedExponent]); // Good
end;

procedure TSynapseFrame.Init;
const
  TimeStep_mS = 1; // ms
  MaxTime = 25; // ms

begin

  Synapse := ContainsStr(ThisBodyPart, 'Synapse');
  Dendrite := ContainsStr(ThisBodyPart, 'Dendrite');
  stackInput_mV := TStack<Single>.Create;
  if Dendrite Then
    stackSources := TStack<string>.Create;

  FStack := TStack<TObject>.Create; // Create the stack
  DeltaIin := 0;
  ChargeDischargeTick := 0;

  dt_mS := TimeStep_mS; //
  NumSteps := Trunc(MaxTime / TimeStep_mS) + 1;
  Membrane_mV := 0;

  TagRec := PTagRec(owner.Tag)^; // Pick up the tag
  TagRec.Membrane_Ohm := neMembrane_kOhm.Value * 1E3;
  frmBiophysics.RefreshBiophysics(nsLength_uM.Value, nsDiameter_uM.Value, neMembrane_kOhm, neMembraneCap_pF, neLamda_cM, neMembraneAxial_kOhm, neTau_mS);

end;

procedure TSynapseFrame.neMembraneAxial_kOhmValueChanged(Sender: TObject);
begin
  frmBiophysics.RefreshBiophysics(nsLength_uM.Value, nsDiameter_uM.Value, neMembrane_kOhm, neMembraneCap_pF, neLamda_cM, neMembraneAxial_kOhm, neTau_mS);

end;

procedure TSynapseFrame.nsDiameter_uMClick(Sender: TObject);
begin
  nsDiameter_uM.SetFocus;
  nsDiameter_uM.SelectAll;
end;

procedure TSynapseFrame.nsDiameter_uMValueChanged(Sender: TObject);

begin
  frmBiophysics.RefreshBiophysics(nsLength_uM.Value, nsDiameter_uM.Value, neMembrane_kOhm, neMembraneCap_pF, neLamda_cM, neMembraneAxial_kOhm, neTau_mS);

end;

procedure TSynapseFrame.nsLength_uMClick(Sender: TObject);
begin
  nsLength_uM.SetFocus;
  nsLength_uM.SelectAll;
end;

procedure TSynapseFrame.nsLength_uMValueChanged(Sender: TObject);

begin
  frmBiophysics.RefreshBiophysics(nsLength_uM.Value, nsDiameter_uM.Value, neMembrane_kOhm, neMembraneCap_pF, neLamda_cM, neMembraneAxial_kOhm, neTau_mS);

end;

Procedure TSynapseFrame.ResetDimensions;

begin
  Length_cM := 0.010; // [cm]
  Diameter_cM := 0.00010; // [cm]

  nsLength_uM.Value := Length_cM * 1E4;
  nsDiameter_uM.Value := Diameter_cM * 1E4;
  frmBiophysics.RefreshBiophysics(nsLength_uM.Value, nsDiameter_uM.Value, neMembrane_kOhm, neMembraneCap_pF, neLamda_cM, neMembraneAxial_kOhm, neTau_mS);
end;

procedure TSynapseFrame.btnResetDimensionsClick(Sender: TObject);
begin
  ResetDimensions;
  ResetDimensions; // Needed to get both spinners to reset.
end;

procedure TSynapseFrame.btnTestClick(Sender: TObject);
begin
  frmBiophysics.RefreshBiophysics(nsLength_uM.Value, nsDiameter_uM.Value, neMembrane_kOhm, neMembraneCap_pF, neLamda_cM, neMembraneAxial_kOhm, neTau_mS);
end;

procedure TSynapseFrame.ProcesssetResetFunctionMessage(var Msg: TMessage);

begin
  resetMessage := PresetMessage(NativeInt(Msg.LParam))^; // Pick up the Reset message
  owner.Tag := NativeInt(@TagRec); // Save the Function output back into the tag
  stackInput_mV.Clear;

  if resetMessage.ResetY then
    begin
      nlInput_pA.Value := 0;
    end;
  if resetMessage.ResetX then
    begin
      ResetX := True;
      // Membrane_mV := 0;
      VStartCharge := 0;
      VStartDischarge := 0;
      DeltaIin := 0;
      CalcTick := 0;
      ChargeDischargeTick := 0;
      MaxInput_pA := 0;
      FStack.Clear; // Clear the collection of PostSynaps APs
      // stackInput_mV.Clear;
      LastIin := 0; // Last current input
      Membrane_mV := 0; // Fix for Freeking Hard Problem 2025-03-27
      X := True;
    end
  else
    begin
      ResetX := False;
      X := False;
    end;
end;

procedure TSynapseFrame.slideWeightChange(Sender: TObject);
begin
  if Synapse then
    begin
      if slideWeight.Position >= 0 then
        begin
          slideWeight.Palette.High := ClLime;
          slideWeight.Palette.Low := ClLime;
        end;

      if slideWeight.Position < 0 then
        begin
          slideWeight.Palette.High := clRed;
          slideWeight.Palette.Low := clRed;
        end;
      Try
        nlWeight.Value := slideWeight.Position / 100;
      except
        on Exception do
          ResetDimensions;
      End;
    end;
end;

procedure TSynapseFrame.slideWeightDblClick(Sender: TObject);
begin
  If Synapse then
    slideWeight.Position := 0;
end;

Procedure TSynapseFrame.Manage_bAP_Message(var Msg: TMessage);
var
  TotalLength: Single;
  numSources: integer;
  CreatedSrcForm: TForm;
  i: integer;
  str: String;
  bAPIn_mV, bAPOut_mV: Single;
begin
  bAPMessageIn := PbAPMessage(NativeInt(Msg.LParam))^; // Pick up the bAP message
  TotalLength := bAPMessageIn.LengthCumulative + nsLength_uM.Value;
  if (Synapse AND frmBodyPlan.STDP.Checked) then // ==========================      CalculateSTDP    ==================
    // if (Synapse) then
    begin
      slideWeight.Position := slideWeight.Position - 1;
    end;
  if Dendrite then
    begin
      numSources := stackSources.Count;
      for i := 0 to numSources - 1 do // Send bAP message to each Immidiate source
        begin
          str := stackSources.Pop; //
          CreatedSrcForm := CreateForm(str);
          if CreatedSrcForm = Nil then
            ShowMessage(' Error in STDP Soma Frame');
          bAPIn_mV := bAPMessageIn.bAP_Amplitude_mV;

          bAPOut_mV := bAPIn_mV * Exp(-TotalLength / neLamda_cM.Value * 1E-4);

          bAPMessageOut.Source := ThisBodyPart;
          bAPMessageOut.SomaName := bAPMessageIn.SomaName;
          bAPMessageOut.LengthCumulative := TotalLength;
          bAPMessageOut.MembraneResetTick := bAPMessageIn.MembraneResetTick;
          // Send the Backpropagation message to the Source bodyparts.
          (CreatedSrcForm as TControl).Perform(WM_bAPFunction, 0, NativeInt(@bAPMessageOut));
        end;
    end;
end;

procedure TSynapseFrame.ProcessSetFunctionMessage(var Msg: TMessage);

  Function Unique(PP: TStack<String>; CkString: String): boolean;
  var
    PPIndex: integer;
    tempArrySources: TArray<String>;
  begin
    Result := True; // Unique
    tempArrySources := PP.ToArray; // Convert the stack to an array
    for PPIndex := Low(tempArrySources) to High(tempArrySources) do
      if tempArrySources[PPIndex] = CkString then
        Result := False // Duplicate
  end;

  function RCChargeResponse(V_initial, V_steady, t, tau: Double): Double;
  var
    ExpDecayTerm: Double;
    tauCorrected: Double;
    V_steadyCorrected: Double;
    V_initialCorrected: Double;
    res: Double;
  begin

    V_steadyCorrected := V_steady * 1E3;
    V_initialCorrected := V_initial * 1E3;

    tauCorrected := tau * 1E3;
    tauCorrected := Round(tauCorrected);
    ExpDecayTerm := Exp(-t / tauCorrected);
    // Result := V_steady * (1 - (ExpDecayTerm)) + V_initial * (ExpDecayTerm);
    res := V_steadyCorrected * (1 - ExpDecayTerm) + V_initialCorrected * ExpDecayTerm;
    Result := res * 1E-3;
  end;

  function RCDischargeResponse(V_initial, V_steady, tau, t: Double): Double;
  begin
    Result := V_steady * (Exp(-t / (tau * 1E3))) + V_initial * Exp(-t / (tau * 1E3));
  end;

const
  Limit = 0.001;
var
  i: integer;
  Input_mV: Variant; // Incomming millivolt from the source
  Steady_mV: Variant;
begin
  SetMessage := PSetMessage(NativeInt(Msg.LParam))^; // Pick up the message
  if On then
    begin
      TagRec := PTagRec(owner.Tag)^; // Pick up the tag
      if SetMessage.Command = Receive then // R E C E I V E
        begin
          // Jim Jim. The Synapse and dendrites must accept messages even in the off state to relay them onward
          Input_mV := (SetMessage.Input_mV); // Sink Membrain voltage [mV]

          if Input_mV <> Null then
            begin
              if SetMessage.IPSP then
                begin
                  Input_mV := frmIPSP.IPSP;
                  stackInput_mV.Push(Input_mV - frmBodyPlan.Vr);
                end
              else
                begin
                  Input_pA := (SetMessage.Input_mV / TagRec.Membrane_Ohm) * 1E9; // This Rm (membrane resistance)
                  nlInput_pA.Value := Input_pA;
                  stackInput_mV.Push(Input_mV); // Push Source Membrain Voltage [mV]
                end;

              if nlInput_pA.Value > MaxInput_pA then
                begin
                  MaxInput_pA := nlInput_pA.Value; // sets the IMax number lab.
                  nlMaxInput_pA.Value := MaxInput_pA;
                end;
              if Dendrite then
                if Unique(stackSources, SetMessage.SourceName) then
                  stackSources.Push(SetMessage.SourceName)
            end
          else
            begin // Vin = NULL
              Input_mV := 0;
              nlInput_pA.Value := 0;
              InputRec.Input_V := Input_mV;
              stackInput_mV.Push(0);
            end; // Null test
        end; // If Receive

      if SetMessage.Command = Send then // S E N D
        begin
          if NOT rbIPSP.Checked then
            begin
              arrInput_mV := stackInput_mV.ToArray; // Convert the stack to an array
              for i := Low(arrInput_mV) to High(arrInput_mV) do
                Steady_mV := Steady_mV + arrInput_mV[i]; // Sum all inputs at this Tick
              if neTau_mS.Value <> 0 then //
                Term := Exp(-dt_mS / neTau_mS.Value);
              diffMembrane_mV := Steady_mV * (1 - Term) + Membrane_mV * Term;
              Membrane_mV := diffMembrane_mV;
              Membrane_mV := Membrane_mV + (Membrane_mV * nlWeight.Value);
            end
          else
            Membrane_mV := frmIPSP.IPSP * 1E-3;

        end; // If SEND
    end // if ON
  else
    Membrane_mV := 0; // Shut down at Vr
   // Membrane_mV := NULL; // Shut down at Vr
  TagRec.Membrane_mV := Membrane_mV; //
  if Membrane_mV <> Null then
    nlOutput_mV.Value := TagRec.Membrane_mV;

  TagRec.Membrane_Ohm := neMembrane_kOhm.Value * 1E3;
  TagRec.MembraneAxial_Ohm := neMembraneAxial_kOhm.Value * 1E3;

  owner.Tag := NativeInt(@TagRec);
  // Save the Function output back into the tag
end;

end.
