diff --git a/src/App.tsx b/src/App.tsx index cfc5eb7..e44214d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -62,7 +62,7 @@ function App() { const [time, setTime] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [trajectoryStatus, setTrajectoryStatus] = useState( - TrajectoryStatus.INITIAL + TrajectoryStatus.INITIAL, ); /** @@ -96,11 +96,11 @@ function App() { ], }); const [timeFactor, setTimeFactor] = useState( - LiveSimulationData.INITIAL_TIME_FACTOR + LiveSimulationData.INITIAL_TIME_FACTOR, ); const [completedModules, setCompletedModules] = useState>( - new Set() + new Set(), ); const [viewportSize, setViewportSize] = useState(DEFAULT_VIEWPORT_SIZE); const adjustableAgentName = @@ -161,6 +161,7 @@ function App() { const clearAllAnalysisState = useCallback(() => { resetCurrentRunAnalysisState(); setRecordedInputConcentration([]); + setProductEquilibriumConcentrations([]); setProductOverTimeTraces([]); setRecordedReactantConcentration([]); setTimeToReachEquilibrium([]); @@ -176,7 +177,7 @@ function App() { ) { isPassedEquilibrium.current = isSlopeZero( currentProductConcentrationArray, - timeFactor + timeFactor, ); } else if (arrayLength === 0 && isPassedEquilibrium.current) { isPassedEquilibrium.current = false; @@ -195,29 +196,32 @@ function App() { simulationData.getInitialConcentrations( activeAgents, currentModule, - sectionType === Section.Experiment - ) + sectionType === Section.Experiment, + ), ); - resetCurrentRunAnalysisState(); + clearAllAnalysisState(); const trajectory = simulationData.createAgentsFromConcentrations( activeAgents, currentModule, - sectionType === Section.Experiment + sectionType === Section.Experiment, ); if (!trajectory) { return null; } const longestAxis = Math.max(viewportSize.width, viewportSize.height); const startMixed = sectionType !== Section.Introduction; + if (process.env.NODE_ENV !== "production") { + console.log("NEW BINDING SIMULATOR"); + } return new BindingSimulator( trajectory, longestAxis / 3, - startMixed ? InitialCondition.RANDOM : InitialCondition.SORTED + startMixed ? InitialCondition.RANDOM : InitialCondition.SORTED, ); }, [ simulationData, currentModule, - resetCurrentRunAnalysisState, + clearAllAnalysisState, viewportSize.width, viewportSize.height, sectionType, @@ -238,7 +242,7 @@ function App() { { clientSimulator: clientSimulator, }, - LIVE_SIMULATION_NAME + LIVE_SIMULATION_NAME, ); }, [simulariumController, clientSimulator]); @@ -287,13 +291,13 @@ function App() { () => uniqMeasuredConcentrations.filter((c) => c > halfFilled).length >= 1, - [halfFilled, uniqMeasuredConcentrations] + [halfFilled, uniqMeasuredConcentrations], ); const hasAValueBelowKd = useMemo( () => uniqMeasuredConcentrations.filter((c) => c < halfFilled).length >= 1, - [halfFilled, uniqMeasuredConcentrations] + [halfFilled, uniqMeasuredConcentrations], ); const canDetermineKd = useMemo(() => { return ( @@ -315,17 +319,17 @@ function App() { setCurrentProductConcentrationArray([]); } }, - [currentProductConcentrationArray, productOverTimeTraces] + [currentProductConcentrationArray, productOverTimeTraces], ); const setExperiment = () => { setIsPlaying(false); - + setCurrentView(ViewType.Simulation); const activeAgents = simulationData.getActiveAgents(currentModule); const concentrations = simulationData.getInitialConcentrations( activeAgents, currentModule, - true + true, ); clientSimulator?.mixAgents(); setTimeFactor(LiveSimulationData.INITIAL_TIME_FACTOR); @@ -362,7 +366,7 @@ function App() { value, sectionType === Section.Experiment ? InitialCondition.RANDOM - : InitialCondition.SORTED + : InitialCondition.SORTED, ); simulariumController.gotoTime(1); // the number isn't used, but it triggers the update const previousConcentration = inputConcentration[agentName] || 0; @@ -376,15 +380,17 @@ function App() { addProductionTrace, resetCurrentRunAnalysisState, sectionType, - ] + ], ); + + // takes you to the home state const totalReset = useCallback(() => { setCurrentView(ViewType.Lab); const activeAgents = [AgentName.A, AgentName.B]; setCurrentModule(Module.A_B_AB); const concentrations = simulationData.getInitialConcentrations( activeAgents, - Module.A_B_AB + Module.A_B_AB, ); setLiveConcentration({ [AgentName.A]: concentrations[AgentName.A], @@ -400,7 +406,7 @@ function App() { concentrations[AgentName.B] ?? LiveSimulationData.INITIAL_CONCENTRATIONS[Module.A_B_AB][ AgentName.B - ] + ], ); setIsPlaying(false); clearAllAnalysisState(); @@ -428,7 +434,7 @@ function App() { currentProductConcentrationArray.length > 1, () => { totalReset(); - } + }, ); const hasRecordedFirstValue = useRef(false); // they have recorded a single value, changed the slider and pressed play @@ -444,7 +450,7 @@ function App() { () => { hasRecordedFirstValue.current = true; setPage(page + 1); - } + }, ); const switchToLiveSimulation = useCallback( @@ -462,7 +468,7 @@ function App() { setTrajectoryName(LIVE_SIMULATION_NAME); } }, - [simulariumController, trajectoryStatus] + [simulariumController, trajectoryStatus], ); // handle trajectory changes based on content changes @@ -488,7 +494,7 @@ function App() { await fetch3DTrajectory( url, simulariumController, - setPreComputedTrajectoryPlotData + setPreComputedTrajectoryPlotData, ); setTrajectoryStatus(TrajectoryStatus.LOADED); }; @@ -555,7 +561,7 @@ function App() { simulariumController.setCameraType(false); setTimeFactor(trajectoryInfo.timeStepSize); setFinalTime( - trajectoryInfo.totalSteps * trajectoryInfo.timeStepSize + trajectoryInfo.totalSteps * trajectoryInfo.timeStepSize, ); } }; @@ -582,7 +588,7 @@ function App() { preComputedPlotDataManager.getCurrentConcentrations(); } else if (clientSimulator) { concentrations = clientSimulator.getCurrentConcentrations( - productName + productName, ) as CurrentConcentration; } const productConcentration = concentrations[productName]; @@ -607,7 +613,7 @@ function App() { const handleFinishInputConcentrationChange = ( name: string, - value: number + value: number, ) => { // this is called when the user finishes dragging the slider // it stores the previous collected data and resets the live data @@ -631,7 +637,7 @@ function App() { const handleSwitchView = () => { setCurrentView((prevView) => - prevView === ViewType.Lab ? ViewType.Simulation : ViewType.Lab + prevView === ViewType.Lab ? ViewType.Simulation : ViewType.Lab, ); }; @@ -659,43 +665,43 @@ function App() { const currentTime = indexToTime( currentProductConcentrationArray.length, timeFactor, - simulationData.timeUnit + simulationData.timeUnit, ); const { newArray, index } = insertValueSorted( recordedReactantConcentrations, - reactantConcentration + reactantConcentration, ); setRecordedReactantConcentration(newArray); updateArrayInState( productEquilibriumConcentrations, index, productConcentration, - setProductEquilibriumConcentrations + setProductEquilibriumConcentrations, ); updateArrayInState( recordedInputConcentration, index, currentInputConcentration, - setRecordedInputConcentration + setRecordedInputConcentration, ); updateArrayInState( timeToReachEquilibrium, index, currentTime, - setTimeToReachEquilibrium + setTimeToReachEquilibrium, ); const color = PLOT_COLORS[ getColorIndex( currentInputConcentration, - simulationData.getMaxConcentration(currentModule) + simulationData.getMaxConcentration(currentModule), ) ]; updateArrayInState(dataColors, index, color, setDataColors); setEquilibriumFeedbackTimeout( <> Great! - + , ); }; diff --git a/src/components/plots/EquilibriumPlot.tsx b/src/components/plots/EquilibriumPlot.tsx index 530821d..861d0b4 100644 --- a/src/components/plots/EquilibriumPlot.tsx +++ b/src/components/plots/EquilibriumPlot.tsx @@ -9,11 +9,12 @@ import { GRAY_COLOR, } from "./constants"; import { SimulariumContext } from "../../simulation/context"; -import { AGENT_A_COLOR } from "../../constants/colors"; +import { AGENT_A_COLOR, AGENT_AB_COLOR } from "../../constants/colors"; import { MICRO } from "../../constants"; import plotStyles from "./plots.module.css"; import { Dash } from "plotly.js"; +import { Module } from "../../types"; interface PlotProps { x: number[]; @@ -37,6 +38,7 @@ const EquilibriumPlot: React.FC = ({ productName, getAgentColor, adjustableAgentName, + module, } = useContext(SimulariumContext); const xMax = Math.max(...x); const xAxisMax = Math.max(kd * 2, xMax * 1.1); @@ -47,17 +49,35 @@ const EquilibriumPlot: React.FC = ({ xVal, y[index], ]); + let fitResult; + let value; + if (module === Module.A_B_D_AB) { + fitResult = regression.exponential(regressionData); + const max = Math.max(...y); + const min = Math.min(...y); + const halfMax = (max - min) / 2 + min; + // for exponential, the equation is in the form y = a * e^(b*x) + // fitResult.equation[0] is a and fitResult.equation[1] is b, so to solve for x when y is halfMax: + // halfMax = a * e^(b*x) + // halfMax / a = e^(b*x) + // ln(halfMax / a) = b*x + // x = ln(halfMax / a) / b + value = + Math.log(halfMax / fitResult.equation[0]) / fitResult.equation[1]; + } else { + fitResult = regression.logarithmic(regressionData); + + const halfFilled = fixedAgentStartingConcentration / 2; + value = + Math.E ** + ((halfFilled - fitResult.equation[0]) / fitResult.equation[1]); + } + const bestFitPoints = fitResult.points; - const bestFit = regression.logarithmic(regressionData); - const bestFitPoints = bestFit.points; const bestFitX = bestFitPoints.map((point) => point[0]); const bestFitY = bestFitPoints.map((point) => point[1]); - const halfFilled = fixedAgentStartingConcentration / 2; - const kdValue = - Math.E ** - ((halfFilled - bestFit.equation[0]) / bestFit.equation[1]); - return { x: bestFitX, y: bestFitY, kd: kdValue }; - }, [x, y, fixedAgentStartingConcentration]); + return { x: bestFitX, y: bestFitY, value: value }; + }, [x, y, fixedAgentStartingConcentration, module]); const hintOverlay = (
= ({ dash: "dot" as Dash, }; - const horizontalLine = { + const kdHorizontalLine = { x: [0, xAxisMax], - y: [5, 5], + y: [ + fixedAgentStartingConcentration / 2, + fixedAgentStartingConcentration / 2, + ], mode: "lines", name: "50% bound", hovertemplate: "50% bound", @@ -94,22 +117,44 @@ const EquilibriumPlot: React.FC = ({ }, line: lineOptions, }; - const horizontalLineMax = { + const kdHorizontalLineMax = { x: [0, xAxisMax], - y: [10, 10], + y: [fixedAgentStartingConcentration, fixedAgentStartingConcentration], mode: "lines", name: "Initial [A]", hoverlabel: { bgcolor: AGENT_A_COLOR }, hovertemplate: "Initial [A]", line: lineOptions, }; + const kiHorizontalLine = { + x: [0, xAxisMax], + y: [Math.max(...y) / 2, Math.max(...y) / 2], + mode: "lines", + name: "Half max inhibition", + hovertemplate: "50% inhibition", + hoverlabel: { + bgcolor: AGENT_A_COLOR, + }, + line: lineOptions, + }; + const kiHorizontalLineMax = { + x: [0, xAxisMax], + y: [Math.max(...y), Math.max(...y)], + mode: "lines", + name: "[AB] without inhibitor", + hoverlabel: { bgcolor: AGENT_AB_COLOR }, + line: lineOptions, + }; const kdIndicator = { - x: [bestFit.kd, bestFit.kd], + x: [bestFit.value, bestFit.value], y: [0, fixedAgentStartingConcentration / 2], mode: "lines", name: "", - hovertemplate: `Kd: ${bestFit.kd.toFixed(2)} ${MICRO}M`, + hovertemplate: + module === Module.A_B_D_AB + ? `Ki: ${bestFit.value.toFixed(2)} ${MICRO}M` + : `Kd: ${bestFit.value.toFixed(2)} ${MICRO}M`, hoverlabel: { bgcolor: getAgentColor(adjustableAgentName), }, @@ -119,6 +164,11 @@ const EquilibriumPlot: React.FC = ({ dash: "dot" as Dash, }, }; + + const horizontalLine = + module === Module.A_B_D_AB ? kiHorizontalLine : kdHorizontalLine; + const horizontalLineMax = + module === Module.A_B_D_AB ? kiHorizontalLineMax : kdHorizontalLineMax; const traces = [ horizontalLine, horizontalLineMax, @@ -158,7 +208,7 @@ const EquilibriumPlot: React.FC = ({ traces.push(kdIndicator); // filter out axis values that are so close to the kd value that they would overlap on the axis xAxisTicks = xAxisTicks.filter( - (tick) => Math.abs(tick - bestFit.kd) >= interval / 2 + (tick) => Math.abs(tick - bestFit.value) >= interval / 2, ); } @@ -176,7 +226,7 @@ const EquilibriumPlot: React.FC = ({ color: getAgentColor(adjustableAgentName), }, tickmode: bestFitVisible ? ("array" as const) : ("auto" as const), - tickvals: [...xAxisTicks, bestFit.kd.toFixed(1)], + tickvals: [...xAxisTicks, bestFit.value], }, yaxis: { ...AXIS_SETTINGS, diff --git a/src/components/quiz-questions/KdQuestion.tsx b/src/components/quiz-questions/KdQuestion.tsx index 7be9859..9a02d63 100644 --- a/src/components/quiz-questions/KdQuestion.tsx +++ b/src/components/quiz-questions/KdQuestion.tsx @@ -94,6 +94,10 @@ const KdQuestion: React.FC = ({ kd, canAnswer }) => { You have now measured enough points to estimate the value of B where half of the binding sites of A are occupied.

+

+ If you're not sure, look at where the line crosses the 50% + mark on the Equilibrium concentration plot. +

Kd = ? diff --git a/src/content/LowAffinity.tsx b/src/content/LowAffinity.tsx index 665e9ac..9a909e3 100644 --- a/src/content/LowAffinity.tsx +++ b/src/content/LowAffinity.tsx @@ -61,7 +61,7 @@ export const lowAffinityContentArray: PageContent[] = [ content: "Congratulations, you’ve completed the Low Affinity experiment!", backButton: true, - // nextButton: true, + nextButton: true, nextButtonText: "View examples", section: Section.BonusContent, layout: LayoutType.FullScreenOverlay, diff --git a/src/index.css b/src/index.css index 42511f9..fc2e684 100644 --- a/src/index.css +++ b/src/index.css @@ -36,7 +36,7 @@ body { height: 100dvh; background-color: var(--background-color); color: var(--text-color); - /* overflow: hidden; */ + overflow: hidden; } h1, diff --git a/src/simulation/BindingSimulator2D.ts b/src/simulation/BindingSimulator2D.ts index 6e488fc..49bed5e 100644 --- a/src/simulation/BindingSimulator2D.ts +++ b/src/simulation/BindingSimulator2D.ts @@ -45,7 +45,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { agents: InputAgent[], size: number, initPositions: InitialCondition = InitialCondition.SORTED, - timeFactor: number = LiveSimulationData.DEFAULT_TIME_FACTOR + timeFactor: number = LiveSimulationData.DEFAULT_TIME_FACTOR, ) { this.size = size; this.productColor = new Map(); @@ -92,7 +92,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { private getProductIdByAgents( agent1: BindingInstance | InputAgent, - agent2: BindingInstance | InputAgent + agent2: BindingInstance | InputAgent, ) { if (agent1.id > agent2.id) { return `${agent1.id}#${agent2.id}`; @@ -111,7 +111,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { const color2 = this.productColor.get(partnerId); if (color1 && color2) { throw new Error( - `Both agents (${id} and ${partnerId}) have a product color defined. Only one should have a product color.` + `Both agents (${id} and ${partnerId}) have a product color defined. Only one should have a product color.`, ); } return color2 || color1 || ""; @@ -139,7 +139,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { private initializeAgents( agents: InputAgent[], - initPositions: InitialCondition = InitialCondition.SORTED + initPositions: InitialCondition = InitialCondition.SORTED, ): StoredAgent[] { for (let i = 0; i < agents.length; ++i) { const agent = agents[i] as StoredAgent; // count is no longer optional @@ -148,7 +148,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { // the count will already be set if (agent.count === undefined) { agent.count = this.convertConcentrationToCount( - agent.initialConcentration + agent.initialConcentration, ); } if (agent.complexColor) { @@ -167,14 +167,14 @@ export default class BindingSimulator implements IClientSimulatorImpl { } const circle = new Circle( new Vector(...position), - agent.radius + agent.radius, ); const instance = new BindingInstance( circle, agent.id, agent.partners, agent.kOn, - agent.kOff + agent.kOff, ); this.system.insert(instance); this.instances.push(instance); @@ -196,7 +196,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { this.system.createLine( new Vector(point[0], point[1]), new Vector(nextPoint[0], nextPoint[1]), - { isStatic: true } + { isStatic: true }, ); }); } @@ -274,7 +274,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { public changeConcentration( agentId: number, newConcentration: number, - initPositions: InitialCondition + initPositions: InitialCondition, ) { const agent = find(this.agents, (agent) => agent.id === agentId); if (!agent) { @@ -305,21 +305,21 @@ export default class BindingSimulator implements IClientSimulatorImpl { const circle = new Circle( new Vector(...position), - agent.radius + agent.radius, ); const instance = new BindingInstance( circle, agent.id, agent.partners, agent.kOn, - agent.kOff + agent.kOff, ); this.system.insert(instance); this.instances.push(instance); } } else if (diff < 0) { const toRemove = this.instances.filter( - (instance) => instance.id === agentId + (instance) => instance.id === agentId, ); for (let i = 0; i < Math.abs(diff); ++i) { const instance = toRemove[i]; @@ -349,14 +349,14 @@ export default class BindingSimulator implements IClientSimulatorImpl { const init = <{ [key: string]: number }>{}; const concentrations = this.agents.reduce((acc, agent) => { acc[agent.name] = this.convertCountToConcentration( - agent.count - this.currentComplexMap.get(agent.id.toString())! + agent.count - this.currentComplexMap.get(agent.id.toString())!, ); return acc; }, init); const productId = this.getProductIdByProductName(product); if (productId) { concentrations[product] = this.convertCountToConcentration( - this.currentComplexMap.get(productId) || 0 + this.currentComplexMap.get(productId) || 0, ); } return concentrations; @@ -391,7 +391,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { for (let i = 0; i < this.instances.length; ++i) { const releasedChild = this.instances[i].oneStep( this.size, - this.timeFactor + this.timeFactor, ); if (releasedChild) { this.currentNumberOfUnbindingEvents++; @@ -412,10 +412,10 @@ export default class BindingSimulator implements IClientSimulatorImpl { const childPosition = instance.pos; const distanceVector = new Vector( childPosition.x - parentPosition.x, - childPosition.y - parentPosition.y + childPosition.y - parentPosition.y, ); const distance = Math.sqrt( - distanceVector.x ** 2 + distanceVector.y ** 2 + distanceVector.x ** 2 + distanceVector.y ** 2, ); const perfectBoundDistance = instance.parent.r + instance.r - bindingOverlap; @@ -433,12 +433,12 @@ export default class BindingSimulator implements IClientSimulatorImpl { private incrementBoundCounts( a: BindingInstance, b: BindingInstance, - amount: number + amount: number, ) { const complexName = this.getProductIdByAgents(a, b); this.currentComplexMap.set( complexName, - (this.currentComplexMap.get(complexName) || 0) + amount + (this.currentComplexMap.get(complexName) || 0) + amount, ); const previousValueA = this.currentComplexMap.get(a.id.toString()) || 0; diff --git a/src/simulation/LiveSimulationData.ts b/src/simulation/LiveSimulationData.ts index 2bddcb1..133edf2 100644 --- a/src/simulation/LiveSimulationData.ts +++ b/src/simulation/LiveSimulationData.ts @@ -36,7 +36,7 @@ const agentB: InputAgent = { initialConcentration: 0, radius: 1, partners: [0], - kOn: 0.9, + kOn: 0.95, kOff: 0.01, color: AGENT_B_COLOR, complexColor: AGENT_AB_COLOR, @@ -163,7 +163,7 @@ export default class LiveSimulation implements ISimulationData { createAgentsFromConcentrations = ( activeAgents?: AgentName[], module?: Module, - isExperiment: boolean = false + isExperiment: boolean = false, ): InputAgent[] => { if (!module) { throw new Error("Module must be specified to create agents."); @@ -174,7 +174,7 @@ export default class LiveSimulation implements ISimulationData { const concentrations = this.getInitialConcentrations( activeAgents, module, - isExperiment + isExperiment, ); return (activeAgents ?? []).map((agentName: AgentName) => { const agent = { @@ -206,7 +206,7 @@ export default class LiveSimulation implements ISimulationData { getInitialConcentrations = ( activeAgents: AgentName[], module: Module, - isExperiment: boolean = false + isExperiment: boolean = false, ): CurrentConcentration => { const concentrations = isExperiment ? { ...LiveSimulation.EXPERIMENT_CONCENTRATIONS[module] } diff --git a/src/simulation/PreComputedSimulationData.ts b/src/simulation/PreComputedSimulationData.ts index 3b5e6e7..664d8d9 100644 --- a/src/simulation/PreComputedSimulationData.ts +++ b/src/simulation/PreComputedSimulationData.ts @@ -13,7 +13,7 @@ import ISimulationData, { import { MICRO } from "../constants"; export default class PreComputedSimulationData implements ISimulationData { - static NAME_TO_FUNCTION_MAP = { + static NAME_TO_TYPE_MAP = { [AgentName.Antibody]: AgentType.Fixed, [AgentName.Antigen]: AgentType.Adjustable_1, [ProductName.AntibodyAntigen]: AgentType.Complex_1, @@ -58,7 +58,7 @@ export default class PreComputedSimulationData implements ISimulationData { getAgentType = (name: AgentName | ProductName): AgentType => { return ( - PreComputedSimulationData.NAME_TO_FUNCTION_MAP as Record< + PreComputedSimulationData.NAME_TO_TYPE_MAP as Record< AgentName | ProductName, AgentType >