{"id":4668,"date":"2026-02-23T17:48:38","date_gmt":"2026-02-23T21:48:38","guid":{"rendered":"https:\/\/faculty.fiu.edu\/~theobald\/?page_id=4668"},"modified":"2026-09-17T12:17:10","modified_gmt":"2026-09-17T16:17:10","slug":"axon_conduction","status":"publish","type":"page","link":"https:\/\/faculty.fiu.edu\/~theobald\/fun\/neurobiology-fun\/axon_conduction\/","title":{"rendered":"axon_conduction"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"4668\" class=\"elementor elementor-4668\">\n\t\t\t\t\t\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-bdfe56a elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"bdfe56a\" data-element_type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-f99aee8\" data-id=\"f99aee8\" data-element_type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t\t\t<div class=\"elementor-element elementor-element-d7f5ab4 elementor-widget elementor-widget-html\" data-id=\"d7f5ab4\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \/>\n  <title>Axon conduction<\/title>\n  <style>\n    :root{\n      --bg:#0f1115;\n      --panel:#141823;\n      --panel2:#10141d;\n      --fg:#e6e8ee;\n      --muted:#aeb4c2;\n      --grid:#2a2f3a;\n      --accent:#7aa2ff;\n    }\n    html,body{\n      height:100%; margin:0; background:var(--bg); color:var(--fg);\n      font-family:system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;\n    }\n    .wrap{\n      display:grid; grid-template-columns:320px 1fr; gap:12px; padding:12px;\n      box-sizing:border-box; height:100%;\n    }\n    .main{\n      display:grid; grid-template-rows:1fr 260px; gap:12px;\n      min-width:0; min-height:0;\n    }\n    .panel{\n      background:var(--panel); border:1px solid #1f2530; border-radius:12px;\n      padding:12px; box-sizing:border-box;\n    }\n    .scene{\n      background:var(--panel2); border:1px solid #1f2530; border-radius:12px;\n      overflow:hidden; position:relative;\n    }\n    canvas{display:block; width:100%; height:100%;}\n    h2{font-size:16px; margin:0 0 10px 0; font-weight:650;}\n    .row{display:flex; align-items:center; gap:10px; margin:10px 0;}\n    .row label{flex:1 1 auto; font-size:13px; color:var(--muted);}\n    .row input[type=\"range\"]{flex:1 1 180px;}\n    .row .val{\n      width:78px; text-align:right; font-variant-numeric:tabular-nums;\n      color:var(--fg); font-size:13px;\n    }\n    .small{font-size:12px; color:var(--muted); line-height:1.3;}\n    .hint{margin-top:8px; padding-top:8px; border-top:1px solid #222836;}\n    .measure{\n      margin-top:10px; padding-top:9px; border-top:1px solid #222836;\n      font-size:12px; line-height:1.55; color:var(--fg);\n      font-variant-numeric:tabular-nums;\n    }\n    .measure .muted{color:var(--muted);}\n    .scopes{display:grid; grid-template-columns:1fr; gap:12px; min-width:0;}\n    .scopeBox{background:var(--panel2); border:1px solid #1f2530; border-radius:12px; overflow:hidden;}\n    .scopeTitle{position:absolute; left:10px; top:8px; font-size:12px; color:var(--muted); pointer-events:none;}\n    .scopeWrap{position:relative; height:124px;}\n    .btnrow{display:flex; gap:8px; margin-top:10px; flex-wrap:wrap;}\n    button{\n      background:#1b2230; color:var(--fg); border:1px solid #2a3242; border-radius:10px;\n      padding:8px 10px; cursor:pointer; font-weight:600;\n    }\n    button:hover{border-color:#3a4761;}\n    .kbd{\n      display:inline-block; padding:1px 6px; border:1px solid #3a4150;\n      border-bottom-width:2px; border-radius:6px; background:#111522;\n      color:#cfd6e6; font-size:12px;\n    }\n  <\/style>\n<\/head>\n<body>\n  <div class=\"wrap\">\n    <div class=\"panel\">\n      <h2>Axon conduction<\/h2>\n\n      <div class=\"row\">\n        <label style=\"justify-content:flex-start; gap:10px; color:var(--fg);\">\n          <input id=\"chkColor\" type=\"checkbox\" checked \/>\n          <span>Voltage coloration<\/span>\n        <\/label>\n      <\/div>\n\n      <div class=\"row\">\n        <label for=\"diam\">Axon diameter<\/label>\n        <input id=\"diam\" type=\"range\" min=\"0.4\" max=\"2.5\" step=\"0.01\" value=\"1.00\" \/>\n        <div class=\"val\" id=\"diamVal\">1.00\u00d7<\/div>\n      <\/div>\n\n      <div class=\"row\">\n        <label for=\"rax\">Axial resistance<\/label>\n        <input id=\"rax\" type=\"range\" min=\"0.4\" max=\"3.0\" step=\"0.01\" value=\"1.00\" \/>\n        <div class=\"val\" id=\"raxVal\">1.00\u00d7<\/div>\n      <\/div>\n\n      <div class=\"row\">\n        <label for=\"cm\">Membrane capacitance<\/label>\n        <input id=\"cm\" type=\"range\" min=\"0.25\" max=\"4.0\" step=\"0.01\" value=\"1.00\" \/>\n        <div class=\"val\" id=\"cmVal\">1.00\u00d7<\/div>\n      <\/div>\n\n      <div class=\"row\">\n        <label for=\"iinj\">Pulse amplitude<\/label>\n        <input id=\"iinj\" type=\"range\" min=\"0\" max=\"50\" step=\"0.5\" value=\"30.0\" \/>\n        <div class=\"val\" id=\"iinjVal\">30.0 \u00b5A\/cm\u00b2<\/div>\n      <\/div>\n\n      <div class=\"small\">\n        Click to place recording electrodes.<br\/>\n        <span class=\"kbd\">Shift<\/span> click to place stimulating electrodes.\n      <\/div>\n\n      <div class=\"hint small\">\n        <span class=\"kbd\">1<\/span> to stimulate electrode 1,<br\/> <span class=\"kbd\">2<\/span> for electrode 2.\n      <\/div>\n\n      <div class=\"measure\">\n        <div id=\"positionReadout\"><span class=\"muted\">Recordings:<\/span> A \u2014 &nbsp;&nbsp; B \u2014<\/div>\n        <div id=\"timingReadout\"><span class=\"muted\">\u0394t:<\/span> \u2014<\/div>\n      <\/div>\n\n      <div class=\"btnrow\">\n        <button id=\"btnReset\">Reset experiment<\/button>\n        <button id=\"btnPause\">Pause<\/button>\n        <button id=\"btnClear\">Clear electrodes<\/button>\n      <\/div>\n    <\/div>\n\n    <div class=\"main\">\n      <div class=\"scene\">\n        <canvas id=\"sceneCanvas\"><\/canvas>\n      <\/div>\n\n      <div class=\"scopes\">\n        <div class=\"scopeBox\">\n          <div class=\"scopeWrap\">\n            <div class=\"scopeTitle\" id=\"scopeTitleA\">Scope A<\/div>\n            <canvas id=\"scopeA\"><\/canvas>\n          <\/div>\n        <\/div>\n        <div class=\"scopeBox\">\n          <div class=\"scopeWrap\">\n            <div class=\"scopeTitle\" id=\"scopeTitleB\">Scope B<\/div>\n            <canvas id=\"scopeB\"><\/canvas>\n          <\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <script type=\"module\">\n    \/\/ WordPress plugin assets used by the existing simulator.\n    import { Neuron } from \"\/~theobald\/wp-content\/plugins\/neuro_demos\/assets\/neuron.js\";\n    import { NeuronRenderer, RingBuffer, drawScope } from \"\/~theobald\/wp-content\/plugins\/neuro_demos\/assets\/render.js\";\n\n    \/\/ Geometry controls drawing only. Physical distance for the conduction\n    \/\/ readout is calibrated from compartment spacing, because the HH model does\n    \/\/ not use the Bezier drawing coordinates in its cable dynamics.\n    const AXON_CPTS = [[0.9, 0.0], [4.5, 0.35], [2.0, -2.8], [8.0, -3.2]];\n    const AXON_NUM_PTS = 140;\n    const AXON_COMPARTMENT_LENGTH_MM = 0.5;\n    const AP_THRESHOLD_V = -0.020; \/\/ upward crossing used only for latency measurement\n\n    \/\/ ---------- Canvas sizing ----------\n    const sceneCanvas = document.getElementById(\"sceneCanvas\");\n    const sceneCtx = sceneCanvas.getContext(\"2d\", { alpha:false });\n\n    const scopeA = document.getElementById(\"scopeA\");\n    const scopeB = document.getElementById(\"scopeB\");\n    const scopeCtxA = scopeA.getContext(\"2d\", { alpha:false });\n    const scopeCtxB = scopeB.getContext(\"2d\", { alpha:false });\n\n    function resizeCanvasToCSS(canvas){\n      const dpr = window.devicePixelRatio || 1;\n      const rect = canvas.getBoundingClientRect();\n      const w = Math.max(2, Math.floor(rect.width * dpr));\n      const h = Math.max(2, Math.floor(rect.height * dpr));\n      if (canvas.width !== w || canvas.height !== h){\n        canvas.width = w;\n        canvas.height = h;\n      }\n    }\n\n    function resizeAll(){\n      resizeCanvasToCSS(sceneCanvas);\n      resizeCanvasToCSS(scopeA);\n      resizeCanvasToCSS(scopeB);\n    }\n    window.addEventListener(\"resize\", resizeAll);\n\n    \/\/ ---------- Electrode models ----------\n    class RecordingElectrode {\n      constructor({ compName, scopeIndex, label, color }){\n        this.type = \"record\";\n        this.compName = compName;\n        this.scopeIndex = scopeIndex; \/\/ 0=A, 1=B\n        this.label = label || `V(${compName})`;\n        this.color = color || \"rgba(140,180,255,0.95)\";\n        this.trace = new RingBuffer(2500);\n      }\n      primeWith(v){\n        this.trace.fill(v);\n      }\n      sample(neuron){\n        const c = neuron.getCompByName(this.compName);\n        if (c) this.trace.push(c.V);\n      }\n    }\n\n    class StimElectrode {\n      constructor({ compName, label, color, stimIndex }){\n        this.type = \"stim\";\n        this.compName = compName;\n        this.stimIndex = stimIndex;\n        this.label = label || `Stim ${stimIndex + 1}`;\n        this.color = color || \"rgba(255,190,120,0.95)\";\n      }\n    }\n\n    \/\/ Manages up to 2 recording + 2 stimulating electrodes.\n    class Electrodes {\n      constructor({ maxRecord=2, maxStim=2 }={}){\n        this.maxRecord = maxRecord;\n        this.maxStim = maxStim;\n        this.record = [];\n        this.stim = [];\n        this._nextScope = 0;\n        this._nextStim = 0;\n        this._recordColors = [\"rgba(120,170,255,0.95)\", \"rgba(110,220,180,0.95)\"];\n        this._stimColors = [\"rgba(255,190,120,0.95)\", \"rgba(255,130,190,0.95)\"];\n      }\n\n      clear(){\n        this.record.length = 0;\n        this.stim.length = 0;\n        this._nextScope = 0;\n        this._nextStim = 0;\n      }\n\n      addRecording(compName, neuron){\n        if (this.record.length >= this.maxRecord) this.record.shift();\n        const scopeIndex = this._nextScope % 2;\n        this._nextScope++;\n\n        const e = new RecordingElectrode({\n          compName,\n          scopeIndex,\n          label:`Record ${scopeIndex === 0 ? \"A\" : \"B\"}`,\n          color:this._recordColors[scopeIndex]\n        });\n\n        const c = neuron.getCompByName(compName);\n        e.primeWith(c ? c.V : -0.065);\n        this.record.push(e);\n        return e;\n      }\n\n      addStim(compName){\n        if (this.stim.length >= this.maxStim) this.stim.shift();\n        const stimIndex = this._nextStim % 2;\n        this._nextStim++;\n        const e = new StimElectrode({\n          compName,\n          stimIndex,\n          label:`Stim ${stimIndex + 1}`,\n          color:this._stimColors[stimIndex]\n        });\n        this.stim.push(e);\n        return e;\n      }\n\n      sampleAll(neuron){\n        for (const e of this.record) e.sample(neuron);\n      }\n    }\n\n    \/\/ ---------- Build neuron ----------\n    function buildNeuron(){\n      const n = new Neuron();\n      const soma = n.addSoma([0.5, 0.0], 0.5);\n      n.addAxon({\n        num_pts:AXON_NUM_PTS,\n        cpts:AXON_CPTS,\n        connect:soma,\n        draw_rad:0.05\n      });\n      rememberBaselineRadii(n);\n      return n;\n    }\n\n    function rememberBaselineRadii(n){\n      for (const section of n.sections){\n        for (const c of section){\n          c._base_draw_rad = c.draw_rad ?? null;\n        }\n      }\n    }\n\n    let neuron = buildNeuron();\n    const renderer = new NeuronRenderer();\n    const electrodes = new Electrodes();\n\n    \/\/ Voltage coloration toggle.\n    const chkColor = document.getElementById(\"chkColor\");\n    renderer.useVoltageColor = chkColor.checked;\n    chkColor.addEventListener(\"change\", () => {\n      renderer.useVoltageColor = chkColor.checked;\n    });\n\n    function findSoma(){\n      return neuron.sections.find(s => s?.length === 1)?.[0] || null;\n    }\n\n    function findAxon(){\n      return neuron.sections.find(s => s?.length > 1 && s?.[0]?.name?.startsWith(\"A\")) || null;\n    }\n\n    function addDefaultElectrodes(){\n      const soma = findSoma();\n      const ax = findAxon();\n      if (!soma || !ax) return;\n\n      \/\/ Both default soma electrodes use the shared renderer, but approach the\n      \/\/ soma from different directions so they remain visibly distinct.\n      const somaRec = electrodes.addRecording(soma.name, neuron);\n      somaRec.drawAngleRad = -2.35; \/\/ upper-left\n\n      electrodes.addRecording(ax[Math.floor(ax.length * 0.85)].name, neuron);\n\n      const somaStim = electrodes.addStim(soma.name);\n      somaStim.drawAngleRad = 2.35; \/\/ lower-left\n\n      electrodes.addStim(ax[Math.floor(ax.length * 0.35)].name);\n    }\n    addDefaultElectrodes();\n\n    \/\/ Fixed world window so diameter changes do not change the zoom.\n    function makeFixedXform(canvas, world, paddingPx=30){\n      const W = canvas.width;\n      const H = canvas.height;\n      const spanX = world.xmax - world.xmin;\n      const spanY = world.ymax - world.ymin;\n      const sx = (W - 2*paddingPx) \/ spanX;\n      const sy = (H - 2*paddingPx) \/ spanY;\n      const scale = Math.min(sx, sy);\n      const cx = 0.5*(world.xmin + world.xmax);\n      const cy = 0.5*(world.ymin + world.ymax);\n\n      const map = (x,y) => [\n        (x - cx)*scale + W\/2,\n        -(y - cy)*scale + H\/2\n      ];\n      return { map, scale };\n    }\n\n    let xform = null;\n    function updateXform(){\n      xform = makeFixedXform(\n        sceneCanvas,\n        { xmin:-2.5, xmax:9.5, ymin:-5.2, ymax:3.2 },\n        30\n      );\n    }\n\n    \/\/ ---------- Cable controls ----------\n    function setAllAxialG(n, gNew){\n      for (const section of n.sections){\n        for (const c of section){\n          if (!c.axial) continue;\n          for (const link of c.axial) link.g = gNew;\n        }\n      }\n    }\n\n    function estimateBaselineAxialG(n){\n      for (const section of n.sections){\n        for (const c of section){\n          if (c.axial?.length) return c.axial[0].g;\n        }\n      }\n      return 5e-8;\n    }\n    const gAxial0 = estimateBaselineAxialG(neuron);\n\n    function applyDiameterVisual(n, diamScale){\n      for (const section of n.sections){\n        if (!section || section.length < 2) continue;\n        for (const c of section){\n          const baseRad = c._base_draw_rad ?? c.draw_rad ?? 0.05;\n          c.draw_rad = baseRad * diamScale;\n        }\n      }\n    }\n\n    function applyCapacitance(n, cmScale, diamScale){\n      for (const section of n.sections){\n        if (!section || section.length < 2) continue;\n        for (const c of section){\n          if (!c?.p) continue;\n          if (c.p._Cm_base_uF_cm2 === undefined){\n            c.p._Cm_base_uF_cm2 = c.p.Cm_uF_cm2;\n          }\n          \/\/ Diameter increases effective membrane area per compartment in this demo.\n          c.p.Cm_uF_cm2 = c.p._Cm_base_uF_cm2 * cmScale * diamScale;\n        }\n      }\n    }\n\n    function applyCableFromSliders(){\n      const diam = Number(document.getElementById(\"diam\").value);\n      const rax = Number(document.getElementById(\"rax\").value);\n      const cm = Number(document.getElementById(\"cm\").value);\n\n      applyDiameterVisual(neuron, diam);\n      applyCapacitance(neuron, cm, diam);\n\n      \/\/ Simplified cable rule used by the original demo: g_axial \u221d d\u00b2 \/ R_axial.\n      setAllAxialG(neuron, gAxial0 * (diam*diam) \/ rax);\n\n      document.getElementById(\"diamVal\").textContent = diam.toFixed(2) + \"\u00d7\";\n      document.getElementById(\"raxVal\").textContent = rax.toFixed(2) + \"\u00d7\";\n      document.getElementById(\"cmVal\").textContent = cm.toFixed(2) + \"\u00d7\";\n    }\n\n    \/\/ ---------- Position readout ----------\n    \/\/ The curve on screen is only a drawing. Each axonal compartment is assigned\n    \/\/ a fixed physical spacing for the teaching readout. With the model's baseline\n    \/\/ membrane area and axial conductance, 0.5 mm per compartment gives a sensible\n    \/\/ calibration for a small unmyelinated axon.\n    function positionAlongAxonMm(compName){\n      const soma = findSoma();\n      if (soma?.name === compName) return 0.0;\n\n      const ax = findAxon();\n      if (!ax) return null;\n      const idx = ax.findIndex(c => c.name === compName);\n      if (idx < 0) return null;\n\n      return idx * AXON_COMPARTMENT_LENGTH_MM;\n    }\n\n    function recByScope(scopeIndex){\n      return electrodes.record.find(e => e.scopeIndex === scopeIndex) || null;\n    }\n\n    \/\/ ---------- Latency measurement ----------\n    let trial = null;\n    let lastMeasurement = null;\n\n    function invalidateMeasurement(){\n      trial = null;\n      lastMeasurement = null;\n      updateReadouts();\n    }\n\n    function beginTrial(stimIndex){\n      trial = {\n        stimIndex,\n        startTime:neuron.t,\n        crossings:[null, null],\n        prevV:[null, null]\n      };\n      lastMeasurement = null;\n\n      for (let scopeIndex=0; scopeIndex<2; scopeIndex++){\n        const rec = recByScope(scopeIndex);\n        const c = rec ? neuron.getCompByName(rec.compName) : null;\n        trial.prevV[scopeIndex] = c ? c.V : null;\n      }\n      updateReadouts();\n    }\n\n    function updateLatencyAfterStep(){\n      if (!trial) return;\n\n      for (let scopeIndex=0; scopeIndex<2; scopeIndex++){\n        if (trial.crossings[scopeIndex] !== null) continue;\n        const rec = recByScope(scopeIndex);\n        if (!rec) continue;\n        const c = neuron.getCompByName(rec.compName);\n        if (!c) continue;\n\n        const prev = trial.prevV[scopeIndex];\n        const now = c.V;\n        if (prev !== null && prev < AP_THRESHOLD_V && now >= AP_THRESHOLD_V){\n          trial.crossings[scopeIndex] = neuron.t;\n        }\n        trial.prevV[scopeIndex] = now;\n      }\n\n      if (trial.crossings[0] !== null && trial.crossings[1] !== null){\n        const dt = trial.crossings[1] - trial.crossings[0];\n        lastMeasurement = {\n          dt_s:Math.abs(dt),\n          first:dt >= 0 ? \"A\" : \"B\"\n        };\n        trial = null;\n        updateReadouts();\n      }\n    }\n\n    function updateReadouts(){\n      const recA = recByScope(0);\n      const recB = recByScope(1);\n      const pA = recA ? positionAlongAxonMm(recA.compName) : null;\n      const pB = recB ? positionAlongAxonMm(recB.compName) : null;\n\n      const pAText = pA === null ? \"\u2014\" : `${pA.toFixed(2)} mm`;\n      const pBText = pB === null ? \"\u2014\" : `${pB.toFixed(2)} mm`;\n      document.getElementById(\"positionReadout\").innerHTML =\n        `<span class=\"muted\">Recordings:<\/span> A ${pAText} &nbsp;&nbsp; B ${pBText}`;\n\n      let timingText = \"\u2014\";\n      if (trial){\n        const n = trial.crossings.filter(x => x !== null).length;\n        timingText = n === 0 ? \"waiting for spike\" : \"waiting for second site\";\n      } else if (lastMeasurement){\n        timingText = `${(lastMeasurement.dt_s*1000).toFixed(2)} ms (${lastMeasurement.first} first)`;\n      }\n\n      document.getElementById(\"timingReadout\").innerHTML =\n        `<span class=\"muted\">\u0394t:<\/span> ${timingText}`;\n    }\n\n    \/\/ ---------- Pulse injection ----------\n    const iinj = document.getElementById(\"iinj\");\n    const iinjVal = document.getElementById(\"iinjVal\");\n    const pulseDur_s = 0.5e-3;\n    const activePulses = new Map();\n\n    function updatePulseAmplitudeLabel(){\n      iinjVal.textContent = Number(iinj.value).toFixed(1) + \" \u00b5A\/cm\u00b2\";\n    }\n    updatePulseAmplitudeLabel();\n\n    function stimByIndex(index){\n      return electrodes.stim.find(e => e.stimIndex === index) || null;\n    }\n\n    function fireStim(index){\n      const e = stimByIndex(index);\n      if (!e) return;\n      beginTrial(index);\n      const amp = Number(iinj.value);\n\n      \/\/ Tie pulse duration to integration steps rather than animation frames.\n      \/\/ With dt = 0.01 ms, a 0.5 ms pulse is always exactly 50 steps.\n      const stepsRemaining = Math.max(1, Math.round(pulseDur_s \/ neuron.dt));\n      activePulses.set(e.compName, {\n        stepsRemaining,\n        amp_uAcm2:amp\n      });\n    }\n\n    \/\/ Prevent browser type-ahead find for 1 and 2 unless the user is typing text.\n    window.addEventListener(\"keydown\", (ev) => {\n      const el = ev.target;\n      const tag = el?.tagName?.toLowerCase() || \"\";\n      const isTextEntry =\n        tag === \"textarea\" ||\n        tag === \"select\" ||\n        (tag === \"input\" && ![\"range\", \"checkbox\", \"button\"].includes((el.type || \"\").toLowerCase())) ||\n        el?.isContentEditable;\n\n      if (isTextEntry) return;\n\n      if (ev.key === \"1\" || ev.key === \"2\"){\n        ev.preventDefault();\n        ev.stopPropagation();\n        if (ev.repeat) return;\n        fireStim(ev.key === \"1\" ? 0 : 1);\n      }\n    }, { capture:true });\n\n    \/\/ All electrodes, including coincident soma electrodes, are drawn by\n    \/\/ NeuronRenderer.drawElectrodes(). The updated renderer supports an optional\n    \/\/ electrode.drawAngleRad and automatically fans coincident electrodes.\n\n    \/\/ ---------- Click to place electrodes ----------\n    sceneCanvas.addEventListener(\"click\", (ev) => {\n      const rect = sceneCanvas.getBoundingClientRect();\n      const dpr = window.devicePixelRatio || 1;\n      const x = (ev.clientX - rect.left) * dpr;\n      const y = (ev.clientY - rect.top) * dpr;\n      const comp = renderer.hitTestScreen(neuron, xform, x, y, 18);\n      if (!comp) return;\n\n      if (ev.shiftKey){\n        electrodes.addStim(comp.name);\n      } else {\n        electrodes.addRecording(comp.name, neuron);\n      }\n      invalidateMeasurement();\n      updateReadouts();\n    });\n\n    \/\/ Changing an experimental parameter invalidates the previous latency result.\n    for (const id of [\"diam\", \"rax\", \"cm\"]){\n      document.getElementById(id).addEventListener(\"input\", () => {\n        applyCableFromSliders();\n        invalidateMeasurement();\n      });\n    }\n    iinj.addEventListener(\"input\", () => {\n      updatePulseAmplitudeLabel();\n      invalidateMeasurement();\n    });\n\n    \/\/ ---------- Buttons ----------\n    let paused = false;\n    const btnPause = document.getElementById(\"btnPause\");\n    btnPause.addEventListener(\"click\", () => {\n      paused = !paused;\n      btnPause.textContent = paused ? \"Resume\" : \"Pause\";\n    });\n\n    document.getElementById(\"btnClear\").addEventListener(\"click\", () => {\n      electrodes.clear();\n      activePulses.clear();\n      invalidateMeasurement();\n      updateReadouts();\n    });\n\n    const defaults = { diam:1.00, rax:1.00, cm:1.00, iinj:30.0 };\n    document.getElementById(\"btnReset\").addEventListener(\"click\", () => {\n      document.getElementById(\"diam\").value = defaults.diam;\n      document.getElementById(\"rax\").value = defaults.rax;\n      document.getElementById(\"cm\").value = defaults.cm;\n      document.getElementById(\"iinj\").value = defaults.iinj;\n      updatePulseAmplitudeLabel();\n\n      neuron = buildNeuron();\n      electrodes.clear();\n      activePulses.clear();\n      addDefaultElectrodes();\n      applyCableFromSliders();\n      invalidateMeasurement();\n\n      paused = false;\n      btnPause.textContent = \"Pause\";\n    });\n\n    \/\/ ---------- Main loop ----------\n    resizeAll();\n    updateXform();\n    applyCableFromSliders();\n    updateReadouts();\n\n    const stepsPerFrame = 10;\n\n    function frame(){\n      resizeAll();\n      updateXform();\n\n      if (!paused){\n        for (let k=0; k<stepsPerFrame; k++){\n          const currentsA_byName = {};\n\n          \/\/ Apply each pulse for an exact number of model timesteps.\n          \/\/ This makes pulse duration independent of requestAnimationFrame timing\n          \/\/ and avoids floating-point comparisons at the nominal end time.\n          for (const [name, p] of activePulses.entries()){\n            if (p.stepsRemaining > 0){\n              const c = neuron.getCompByName(name);\n              if (c){\n                currentsA_byName[name] =\n                  p.amp_uAcm2 * 1e-6 * c.p.area_cm2;\n              }\n              p.stepsRemaining -= 1;\n            }\n\n            if (p.stepsRemaining <= 0){\n              activePulses.delete(name);\n            }\n          }\n\n          neuron.step(neuron.dt, currentsA_byName);\n          updateLatencyAfterStep();\n        }\n\n        electrodes.sampleAll(neuron);\n      }\n\n      \/\/ Draw neuron and electrodes.\n      sceneCtx.clearRect(0, 0, sceneCanvas.width, sceneCanvas.height);\n      renderer.drawNeuron(sceneCtx, neuron, xform);\n      renderer.drawElectrodes(sceneCtx, neuron, electrodes, xform);\n\n      \/\/ Draw scopes.\n      const rec0 = recByScope(0);\n      const rec1 = recByScope(1);\n      const p0 = rec0 ? positionAlongAxonMm(rec0.compName) : null;\n      const p1 = rec1 ? positionAlongAxonMm(rec1.compName) : null;\n\n      document.getElementById(\"scopeTitleA\").textContent = rec0\n        ? `Record A${p0 === null ? \"\" : ` \u00b7 ${p0.toFixed(2)} mm`}`\n        : \"Scope A\";\n      document.getElementById(\"scopeTitleB\").textContent = rec1\n        ? `Record B${p1 === null ? \"\" : ` \u00b7 ${p1.toFixed(2)} mm`}`\n        : \"Scope B\";\n\n      drawScope(\n        scopeCtxA, scopeA, rec0 ? rec0.trace.toArray() : null, \"\",\n        rec0 ? rec0.color : \"rgba(230,232,238,0.35)\",\n        { autoscale:false, vmin:-0.100, vmax:0.070 }\n      );\n      drawScope(\n        scopeCtxB, scopeB, rec1 ? rec1.trace.toArray() : null, \"\",\n        rec1 ? rec1.color : \"rgba(230,232,238,0.35)\",\n        { autoscale:false, vmin:-0.100, vmax:0.070 }\n      );\n\n      requestAnimationFrame(frame);\n    }\n\n    requestAnimationFrame(frame);\n  <\/script>\n<\/body>\n<\/html>\n\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Axon conduction Axon conduction Voltage coloration Axon diameter 1.00\u00d7 Axial resistance 1.00\u00d7 Membrane capacitance 1.00\u00d7 Pulse amplitude 30.0 \u00b5A\/cm\u00b2 Click to place recording electrodes. Shift click to place stimulating electrodes. 1 to stimulate electrode 1, 2 for electrode 2. Recordings: A \u2014 &nbsp;&nbsp; B \u2014 \u0394t: \u2014 Reset experiment Pause Clear electrodes Scope A Scope [&hellip;]<\/p>\n","protected":false},"author":3,"featured_media":0,"parent":4438,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-4668","page","type-page","status-publish","hentry","entry"],"_links":{"self":[{"href":"https:\/\/faculty.fiu.edu\/~theobald\/wp-json\/wp\/v2\/pages\/4668","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/faculty.fiu.edu\/~theobald\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/faculty.fiu.edu\/~theobald\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/faculty.fiu.edu\/~theobald\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/faculty.fiu.edu\/~theobald\/wp-json\/wp\/v2\/comments?post=4668"}],"version-history":[{"count":32,"href":"https:\/\/faculty.fiu.edu\/~theobald\/wp-json\/wp\/v2\/pages\/4668\/revisions"}],"predecessor-version":[{"id":5008,"href":"https:\/\/faculty.fiu.edu\/~theobald\/wp-json\/wp\/v2\/pages\/4668\/revisions\/5008"}],"up":[{"embeddable":true,"href":"https:\/\/faculty.fiu.edu\/~theobald\/wp-json\/wp\/v2\/pages\/4438"}],"wp:attachment":[{"href":"https:\/\/faculty.fiu.edu\/~theobald\/wp-json\/wp\/v2\/media?parent=4668"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}