From 4753a11eea3336375d32da3eac56bb29288e4dba Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 28 Aug 2025 12:31:33 -0700 Subject: [PATCH 01/35] make indicator clickable --- src/components/PageIndicator.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/PageIndicator.tsx b/src/components/PageIndicator.tsx index ab11b8cb..52d9d0c5 100644 --- a/src/components/PageIndicator.tsx +++ b/src/components/PageIndicator.tsx @@ -5,6 +5,7 @@ import classNames from "classnames"; import { moduleNames } from "../content"; import styles from "./page-indicator.module.css"; +import { SimulariumContext } from "../simulation/context"; interface PageIndicatorProps { title: string; @@ -18,6 +19,7 @@ const PageIndicator: React.FC = ({ total, }) => { let indexOfActiveModule = -1; + const { setModule } = React.useContext(SimulariumContext); const getModulePercent = (isActiveModule: boolean, moduleIndex: number) => { if (isActiveModule) { @@ -44,6 +46,12 @@ const PageIndicator: React.FC = ({ moduleIndex <= indexOfActiveModule, [styles.current]: isActiveModule, })} + onClick={() => { + if (isActiveModule) { + return; + } + setModule(moduleIndex); + }} >
{name}
Date: Thu, 28 Aug 2025 14:03:19 -0700 Subject: [PATCH 02/35] switch module and save which have been completed --- src/App.tsx | 9 ++++++++ src/components/PageIndicator.tsx | 22 +++++++++++++------- src/components/page-indicator.module.css | 17 +++++++++++++-- src/components/quiz-questions/KdQuestion.tsx | 3 ++- src/simulation/LiveSimulationData.ts | 13 ++++++------ src/simulation/context.tsx | 4 ++++ 6 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8daaca08..c5b6ca66 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -95,6 +95,8 @@ function App() { const [timeFactor, setTimeFactor] = useState( LiveSimulationData.INITIAL_TIME_FACTOR ); + + const [completedModules, setCompletedModules] = useState([]); const [viewportSize, setViewportSize] = useState(DEFAULT_VIEWPORT_SIZE); const adjustableAgentName = LiveSimulationData.ADJUSTABLE_AGENT_MAP[currentModule]; @@ -455,8 +457,13 @@ function App() { // User input handlers + const addCompletedModule = (module: Module) => { + setCompletedModules((prev: Module[]) => [...prev, module] as Module[]); + }; + const setModule = (module: Module) => { setPage(FIRST_PAGE[module]); + clearAllAnalysisState(); setCurrentModule(module); setIsPlaying(false); }; @@ -653,7 +660,9 @@ function App() { handleTrajectoryChange, viewportSize, setViewportSize, + addCompletedModule, recordedConcentrations: recordedInputConcentration, + completedModules, }} > = ({ page, total, }) => { - let indexOfActiveModule = -1; - const { setModule } = React.useContext(SimulariumContext); + const { setModule, completedModules } = React.useContext(SimulariumContext); + const indexOfActiveModule: number = useMemo(() => { + let toReturn = -1; + map(moduleNames, (name, index) => { + if (name === title) { + toReturn = Number(index); + } + }); + return toReturn; + }, [title]); const getModulePercent = (isActiveModule: boolean, moduleIndex: number) => { + console.log(moduleIndex, indexOfActiveModule); if (isActiveModule) { return (page / total) * 100; - } else if (moduleIndex < indexOfActiveModule) { + } else if (completedModules.includes(moduleIndex)) { return 100; } else { return 0; @@ -34,10 +43,7 @@ const PageIndicator: React.FC = ({ {map(moduleNames, (name, index) => { const moduleIndex = Number(index); - const isActiveModule = name === title; - if (isActiveModule) { - indexOfActiveModule = moduleIndex; - } + const isActiveModule = moduleIndex === indexOfActiveModule; return (
= ({ kd, canAnswer }) => { const [selectedAnswer, setSelectedAnswer] = useState(null); const [formState, setFormState] = useState(FormState.Clear); - const { module } = useContext(SimulariumContext); + const { module, addCompletedModule } = useContext(SimulariumContext); useEffect(() => { setSelectedAnswer(null); @@ -82,6 +82,7 @@ const KdQuestion: React.FC = ({ kd, canAnswer }) => { Math.abs(selectedAnswer - correctAnswer) / correctAnswer; if (closeness <= tolerance) { setFormState(FormState.Correct); + addCompletedModule(module); } else { setFormState(FormState.Incorrect); } diff --git a/src/simulation/LiveSimulationData.ts b/src/simulation/LiveSimulationData.ts index ad780d62..881980e9 100644 --- a/src/simulation/LiveSimulationData.ts +++ b/src/simulation/LiveSimulationData.ts @@ -48,13 +48,12 @@ const agentC: InputAgent = { color: AGENT_C_COLOR, }; -const kds = { - [Module.A_B_AB]: 0.75, - [Module.A_C_AC]: 74, - [Module.A_B_C_AB_AC]: 5, -}; - export default class LiveSimulation implements ISimulationData { + static ESTIMATED_SOLUTIONS = { + [Module.A_B_AB]: 0.75, + [Module.A_C_AC]: 74, + [Module.A_B_C_AB_AC]: 5, + }; static NAME_TO_FUNCTION_MAP = { [AgentName.A]: AgentFunction.Fixed, [AgentName.B]: AgentFunction.Adjustable, @@ -168,6 +167,6 @@ export default class LiveSimulation implements ISimulationData { }, {}); }; getKd = (module: Module): number => { - return kds[module]; + return LiveSimulation.ESTIMATED_SOLUTIONS[module]; }; } diff --git a/src/simulation/context.tsx b/src/simulation/context.tsx index 0021589a..1747cb52 100644 --- a/src/simulation/context.tsx +++ b/src/simulation/context.tsx @@ -35,6 +35,8 @@ interface SimulariumContextType { viewportSize: { width: number; height: number }; setViewportSize: (value: { width: number; height: number }) => void; recordedConcentrations: number[]; + addCompletedModule: (value: Module) => void; + completedModules: Module[]; } export const SimulariumContext = createContext({ @@ -61,4 +63,6 @@ export const SimulariumContext = createContext({ viewportSize: DEFAULT_VIEWPORT_SIZE, setViewportSize: () => {}, recordedConcentrations: [], + addCompletedModule: () => {}, + completedModules: [], } as SimulariumContextType); From fe0c92604a23779c99667cd7de9cc2cdd9fe5b9f Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 28 Aug 2025 14:19:58 -0700 Subject: [PATCH 03/35] add pop confirm --- src/components/PageIndicator.tsx | 72 +++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/src/components/PageIndicator.tsx b/src/components/PageIndicator.tsx index 6333a6dc..de202bfe 100644 --- a/src/components/PageIndicator.tsx +++ b/src/components/PageIndicator.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from "react"; -import { Progress, Flex } from "antd"; +import { Progress, Flex, Popconfirm } from "antd"; import { map } from "lodash"; import classNames from "classnames"; @@ -18,7 +18,8 @@ const PageIndicator: React.FC = ({ page, total, }) => { - const { setModule, completedModules } = React.useContext(SimulariumContext); + const { module, setModule, completedModules } = + React.useContext(SimulariumContext); const indexOfActiveModule: number = useMemo(() => { let toReturn = -1; map(moduleNames, (name, index) => { @@ -30,7 +31,6 @@ const PageIndicator: React.FC = ({ }, [title]); const getModulePercent = (isActiveModule: boolean, moduleIndex: number) => { - console.log(moduleIndex, indexOfActiveModule); if (isActiveModule) { return (page / total) * 100; } else if (completedModules.includes(moduleIndex)) { @@ -39,37 +39,59 @@ const PageIndicator: React.FC = ({ return 0; } }; + + const getTitle = (moduleIndex: number) => { + if (module === moduleIndex) { + return "Restart module"; + } else { + return "Leave module"; + } + }; + + const getMessage = (moduleIndex: number) => { + if (module === moduleIndex) { + return "Are you sure to restart this module? You will lose all progress."; + } else { + return "Are you sure you want to leave this module? You will lose all progress."; + } + }; + return ( {map(moduleNames, (name, index) => { const moduleIndex = Number(index); const isActiveModule = moduleIndex === indexOfActiveModule; return ( -
{ - if (isActiveModule) { - return; - } + { setModule(moduleIndex); }} + onCancel={() => {}} + okText="Yes" + cancelText="No" > -
{name}
- -
+
+
{name}
+ +
+ ); })}
From e54b3e1d474be4f46c5da9c43122002f7406ef3b Mon Sep 17 00:00:00 2001 From: Megan Riel-Mehan Date: Tue, 2 Sep 2025 11:20:12 -0700 Subject: [PATCH 04/35] Update src/components/PageIndicator.tsx Co-authored-by: Peyton Lee --- src/components/PageIndicator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PageIndicator.tsx b/src/components/PageIndicator.tsx index de202bfe..3ff0d13c 100644 --- a/src/components/PageIndicator.tsx +++ b/src/components/PageIndicator.tsx @@ -50,7 +50,7 @@ const PageIndicator: React.FC = ({ const getMessage = (moduleIndex: number) => { if (module === moduleIndex) { - return "Are you sure to restart this module? You will lose all progress."; + return "Are you sure you want to restart this module? You will lose all progress."; } else { return "Are you sure you want to leave this module? You will lose all progress."; } From ad535ef9bbcc4f6e83454f33a12d4f909adf196c Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 2 Sep 2025 12:48:09 -0700 Subject: [PATCH 05/35] type index as number --- src/components/PageIndicator.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/PageIndicator.tsx b/src/components/PageIndicator.tsx index de202bfe..7f8a7e19 100644 --- a/src/components/PageIndicator.tsx +++ b/src/components/PageIndicator.tsx @@ -58,8 +58,8 @@ const PageIndicator: React.FC = ({ return ( - {map(moduleNames, (name, index) => { - const moduleIndex = Number(index); + {map(moduleNames, (name, index: number) => { + const moduleIndex = index; const isActiveModule = moduleIndex === indexOfActiveModule; return ( Date: Wed, 17 Sep 2025 10:43:25 -0700 Subject: [PATCH 06/35] add key --- src/components/PageIndicator.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/PageIndicator.tsx b/src/components/PageIndicator.tsx index b5090392..5546b356 100644 --- a/src/components/PageIndicator.tsx +++ b/src/components/PageIndicator.tsx @@ -63,6 +63,7 @@ const PageIndicator: React.FC = ({ const isActiveModule = moduleIndex === indexOfActiveModule; return ( { From 57a2de7f2e09a3519c9b2a3e5294f5660e874d23 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 18 Nov 2025 18:26:20 -0800 Subject: [PATCH 07/35] make sure index is a number --- src/components/PageIndicator.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/PageIndicator.tsx b/src/components/PageIndicator.tsx index 5546b356..9d082b7d 100644 --- a/src/components/PageIndicator.tsx +++ b/src/components/PageIndicator.tsx @@ -58,8 +58,8 @@ const PageIndicator: React.FC = ({ return ( - {map(moduleNames, (name, index: number) => { - const moduleIndex = index; + {map(moduleNames, (name, index: string) => { + const moduleIndex = Number(index); const isActiveModule = moduleIndex === indexOfActiveModule; return ( Date: Tue, 18 Nov 2025 18:34:15 -0800 Subject: [PATCH 08/35] use a set for completed modules --- src/App.tsx | 6 ++++-- src/components/PageIndicator.tsx | 2 +- src/simulation/context.tsx | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c5b6ca66..6d629cfb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -96,7 +96,9 @@ function App() { LiveSimulationData.INITIAL_TIME_FACTOR ); - const [completedModules, setCompletedModules] = useState([]); + const [completedModules, setCompletedModules] = useState>( + new Set() + ); const [viewportSize, setViewportSize] = useState(DEFAULT_VIEWPORT_SIZE); const adjustableAgentName = LiveSimulationData.ADJUSTABLE_AGENT_MAP[currentModule]; @@ -458,7 +460,7 @@ function App() { // User input handlers const addCompletedModule = (module: Module) => { - setCompletedModules((prev: Module[]) => [...prev, module] as Module[]); + setCompletedModules((prev: Set) => new Set(prev).add(module)); }; const setModule = (module: Module) => { diff --git a/src/components/PageIndicator.tsx b/src/components/PageIndicator.tsx index 9d082b7d..c3d8422d 100644 --- a/src/components/PageIndicator.tsx +++ b/src/components/PageIndicator.tsx @@ -33,7 +33,7 @@ const PageIndicator: React.FC = ({ const getModulePercent = (isActiveModule: boolean, moduleIndex: number) => { if (isActiveModule) { return (page / total) * 100; - } else if (completedModules.includes(moduleIndex)) { + } else if (completedModules.has(moduleIndex)) { return 100; } else { return 0; diff --git a/src/simulation/context.tsx b/src/simulation/context.tsx index 1747cb52..3b42231f 100644 --- a/src/simulation/context.tsx +++ b/src/simulation/context.tsx @@ -36,7 +36,7 @@ interface SimulariumContextType { setViewportSize: (value: { width: number; height: number }) => void; recordedConcentrations: number[]; addCompletedModule: (value: Module) => void; - completedModules: Module[]; + completedModules: Set; } export const SimulariumContext = createContext({ @@ -64,5 +64,5 @@ export const SimulariumContext = createContext({ setViewportSize: () => {}, recordedConcentrations: [], addCompletedModule: () => {}, - completedModules: [], + completedModules: new Set(), } as SimulariumContextType); From 21dab42c24748ac705a4ab6d2e0df9d9e9ffda94 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 18 Nov 2025 18:36:59 -0800 Subject: [PATCH 09/35] show cursor --- src/components/page-indicator.module.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/page-indicator.module.css b/src/components/page-indicator.module.css index 97831c29..d64c6ed6 100644 --- a/src/components/page-indicator.module.css +++ b/src/components/page-indicator.module.css @@ -10,6 +10,10 @@ position: relative; } +.progress-bar-wrapper:hover { + cursor: pointer; +} + .text-format { font-weight: 400; line-height: 14px; From 16e1b166c55115a31d3b0f978d34794d6fcaaf06 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 18 Nov 2025 18:40:12 -0800 Subject: [PATCH 10/35] fix merge conflict --- src/simulation/LiveSimulationData.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/simulation/LiveSimulationData.ts b/src/simulation/LiveSimulationData.ts index f456aade..a3dd36a2 100644 --- a/src/simulation/LiveSimulationData.ts +++ b/src/simulation/LiveSimulationData.ts @@ -48,17 +48,11 @@ const agentC: InputAgent = { color: AGENT_C_COLOR, }; -const kds = { - [Module.A_B_AB]: 0.75, - [Module.A_C_AC]: 74, - [Module.A_B_D_AB]: 5, -}; - export default class LiveSimulation implements ISimulationData { static ESTIMATED_SOLUTIONS = { [Module.A_B_AB]: 0.75, [Module.A_C_AC]: 74, - [Module.A_B_C_AB_AC]: 5, + [Module.A_B_D_AB]: 5, }; static NAME_TO_FUNCTION_MAP = { [AgentName.A]: AgentFunction.Fixed, From e425c5e8a583d5597e50520ba5613ea861126354 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 18 Nov 2025 18:41:29 -0800 Subject: [PATCH 11/35] fix merge conflict --- src/App.tsx | 2 -- src/simulation/context.tsx | 4 ---- 2 files changed, 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 96b8809a..72387c98 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -692,9 +692,7 @@ function App() { timeUnit: simulationData.timeUnit, trajectoryName, viewportSize, - setViewportSize, addCompletedModule, - recordedConcentrations: recordedInputConcentration, completedModules, }} > diff --git a/src/simulation/context.tsx b/src/simulation/context.tsx index 37dbd15c..38576392 100644 --- a/src/simulation/context.tsx +++ b/src/simulation/context.tsx @@ -39,8 +39,6 @@ interface SimulariumContextType { timeUnit: string; trajectoryName: string; viewportSize: { width: number; height: number }; - setViewportSize: (value: { width: number; height: number }) => void; - recordedConcentrations: number[]; addCompletedModule: (value: Module) => void; completedModules: Set; } @@ -72,8 +70,6 @@ export const SimulariumContext = createContext({ timeUnit: NANO, trajectoryName: LIVE_SIMULATION_NAME, viewportSize: DEFAULT_VIEWPORT_SIZE, - setViewportSize: () => {}, - recordedConcentrations: [], addCompletedModule: () => {}, completedModules: new Set(), } as SimulariumContextType); From a25115d5ab3fc0644c807d82c88196effd186432 Mon Sep 17 00:00:00 2001 From: Megan Riel-Mehan Date: Tue, 18 Nov 2025 18:43:46 -0800 Subject: [PATCH 12/35] Update src/components/PageIndicator.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/PageIndicator.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/PageIndicator.tsx b/src/components/PageIndicator.tsx index c3d8422d..aa27216a 100644 --- a/src/components/PageIndicator.tsx +++ b/src/components/PageIndicator.tsx @@ -74,7 +74,6 @@ const PageIndicator: React.FC = ({ cancelText="No" >
Date: Thu, 20 Nov 2025 13:32:14 -0800 Subject: [PATCH 13/35] clean up css --- src/components/page-indicator.module.css | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/page-indicator.module.css b/src/components/page-indicator.module.css index d64c6ed6..e9aacb06 100644 --- a/src/components/page-indicator.module.css +++ b/src/components/page-indicator.module.css @@ -8,9 +8,6 @@ display: flex; flex-direction: column; position: relative; -} - -.progress-bar-wrapper:hover { cursor: pointer; } @@ -35,7 +32,6 @@ left: 0px; border-radius: 50%; background-color: var(--dark-gray); - cursor: pointer; } /* click target */ @@ -47,7 +43,6 @@ top: 20px; left: -5px; border-radius: 50%; - cursor: pointer; } .progress-bar { From ee64742a50d8edeb090ee436d6b140e3408e79bc Mon Sep 17 00:00:00 2001 From: meganrm Date: Fri, 21 Nov 2025 13:57:15 -0800 Subject: [PATCH 14/35] display adjustments --- .../concentration-display/ConcentrationSlider.tsx | 6 +++++- .../concentration-display/LiveConcentrationDisplay.tsx | 2 +- src/components/main-layout/LeftPanel.tsx | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/concentration-display/ConcentrationSlider.tsx b/src/components/concentration-display/ConcentrationSlider.tsx index f978d234..2093aed3 100644 --- a/src/components/concentration-display/ConcentrationSlider.tsx +++ b/src/components/concentration-display/ConcentrationSlider.tsx @@ -69,7 +69,11 @@ const ConcentrationSlider: React.FC = ({ marks[index] = { label: ( 0 + ? (index.toFixed(1) as unknown as number) + : index + } disabledNumbers={disabledNumbers} onMouseUp={() => onChangeComplete?.(name, index)} /> diff --git a/src/components/concentration-display/LiveConcentrationDisplay.tsx b/src/components/concentration-display/LiveConcentrationDisplay.tsx index 235c5b0f..4d674d93 100644 --- a/src/components/concentration-display/LiveConcentrationDisplay.tsx +++ b/src/components/concentration-display/LiveConcentrationDisplay.tsx @@ -19,7 +19,7 @@ const LiveConcentrationDisplay: React.FC = ({ const { maxConcentration, getAgentColor } = useContext(SimulariumContext); // the steps have a 2px gap, so we are adjusting the // size of the step based on the total number we want - const steps = Math.min(maxConcentration, 10); + const steps = Math.max(maxConcentration, 10); const size = width / steps - 2; return (
diff --git a/src/components/main-layout/LeftPanel.tsx b/src/components/main-layout/LeftPanel.tsx index a2310afd..ba6ddf8a 100644 --- a/src/components/main-layout/LeftPanel.tsx +++ b/src/components/main-layout/LeftPanel.tsx @@ -38,7 +38,7 @@ const LeftPanel: React.FC = ({ const eventsOverTimeExcludedPages = { [Module.A_B_AB]: [0, 1, 2], [Module.A_C_AC]: [], - [Module.A_B_D_AB]: [], + [Module.A_B_D_AB]: [0, 1, 2, 3, 4], }; return ( <> From a2e517acf9c31751a293b9d32c002b4d7d759121 Mon Sep 17 00:00:00 2001 From: meganrm Date: Fri, 21 Nov 2025 13:59:58 -0800 Subject: [PATCH 15/35] add competitive content --- src/content/Competitive.tsx | 154 +++++++++++++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 2 deletions(-) diff --git a/src/content/Competitive.tsx b/src/content/Competitive.tsx index 6c3b52b9..49374751 100644 --- a/src/content/Competitive.tsx +++ b/src/content/Competitive.tsx @@ -1,3 +1,11 @@ +import { A, AB, AD, B, C, D } from "../components/agent-symbols"; +import Definition from "../components/shared/Definition"; +import StartExperiment from "../components/StartExperiment"; +import { + PLAY_BUTTON_ID, + RECORD_BUTTON_ID, + START_EXPERIMENT_ID, +} from "../constants"; import { LayoutType, PageContent, Section } from "../types"; export const competitiveArray: PageContent[] = [ @@ -41,11 +49,153 @@ export const competitiveArray: PageContent[] = [ layout: LayoutType.FullScreenOverlay, }, { - content: "Placeholder", - section: Section.Experiment, + title: "Competitive Binding", + content: ( + <> + We now know the binding affinity of and . Our + competitive inhibitor, , also binds to and we want to + figure out how effective it is at blocking the binding of . + We cannot directly measure the formation of (which is why + it's shown in grey). + + ), layout: LayoutType.LiveSimulation, + section: Section.Introduction, + progressionElement: PLAY_BUTTON_ID, + callToAction: + "Press play and watch the how the two different complexes form over time.", }, + { + title: "Start the experiment", + content: ( + <> + Now, let's use this simulation to make measurements. We'll start + with [] = 0 so we can get the formation of without + any competition. + + ), + actionButton: , + callToAction: ( + <> + Click the Start experiment button to reset the + simulation and begin by pressing play! + + ), + section: Section.Introduction, + layout: LayoutType.LiveSimulation, + progressionElement: START_EXPERIMENT_ID, + }, + { + title: "Start the experiment", + content: ( + <> + Now, let's use this simulation to make measurements. We'll start + with [] = 0 so we can get the formation of without + any competition. + + ), + actionButton: , + callToAction: ( + <> + Click the Start experiment button to reset the + simulation and begin by pressing play! + + ), + section: Section.Experiment, + layout: LayoutType.LiveSimulation, + progressionElement: PLAY_BUTTON_ID, + }, + { + title: "Find maximum complex formation", + content: ( + <> + With [] = 0, the complex is able to form without any + inhibition. This will be our baseline for the max amount of{" "} + that can form at these concentrations.{" "} + + ), + callToAction: ( + <> + Watch the Concentration over time plot until + you think the reaction has reached equilibrium. Then, press the{" "} + Record button to record the equilibrium + concentration. + + ), + section: Section.Experiment, + layout: LayoutType.LiveSimulation, + progressionElement: RECORD_BUTTON_ID, + }, + { + title: "Introduce the competitive inhibitor", + content: ( + <> + Now let's see how the addition of affects the formation of{" "} + . + + ), + callToAction: ( + <> + If you haven’t already done so, pause the + simulation and use the now-visible interactive slider under{" "} + Agent concentrations to adjust the + concentration of and play the simulation + again. + + ), + section: Section.Experiment, + layout: LayoutType.LiveSimulation, + progressionElement: PLAY_BUTTON_ID, + }, + { + title: "Repeating the experiment", + content: ( + <> + We want to understand the effect + has on the formation of . Let’s repeat the + experiment with a new concentration of . We will keep the + concentration of and constant. + + ), + callToAction: ( + <> + For each new concentration of , determine when equilibrium + has been reached and then press the Record{" "} + button to plot their equilibrium concentrations. + + ), + section: Section.Experiment, + layout: LayoutType.LiveSimulation, + progressionElement: RECORD_BUTTON_ID, + }, + { + title: "Deriving Ki", + content: ( + <> + The constant Ki is analogous to Kd for + inhibitors. Ki can be determined in this experiment + by finding the IC50, the concentration of inhibitor + where the amount of + complex is reduced by half. + + ), + moreInfo: ( + <> + K50 = [] (at equilibrium when [] is half + of max) + + ), + callToAction: ( + <> + Let’s find K50 - Repeat the experiment by pausing, + adjusting the concentration of and recording the + equilibrium point until you have enough data. + + ), + section: Section.Experiment, + layout: LayoutType.LiveSimulation, + }, { content: "Congratulations, you’ve completed the High Affinity experiment!", From 57dada6938f8b4f6b3ae349d1681641eb8a6cef9 Mon Sep 17 00:00:00 2001 From: meganrm Date: Fri, 21 Nov 2025 14:41:38 -0800 Subject: [PATCH 16/35] remove unused imports --- src/content/Competitive.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/content/Competitive.tsx b/src/content/Competitive.tsx index 49374751..1f48078b 100644 --- a/src/content/Competitive.tsx +++ b/src/content/Competitive.tsx @@ -1,5 +1,4 @@ -import { A, AB, AD, B, C, D } from "../components/agent-symbols"; -import Definition from "../components/shared/Definition"; +import { A, AB, AD, B, D } from "../components/agent-symbols"; import StartExperiment from "../components/StartExperiment"; import { PLAY_BUTTON_ID, From d6b35c87334fd0010ee6193c60340ee2d36f9a8d Mon Sep 17 00:00:00 2001 From: meganrm Date: Fri, 21 Nov 2025 14:43:05 -0800 Subject: [PATCH 17/35] change data structure --- src/App.tsx | 72 ++++++++---- src/simulation/BindingInstance.ts | 3 +- src/simulation/BindingSimulator2D.ts | 119 +++++++++++++++++--- src/simulation/ISimulationData.ts | 21 ++-- src/simulation/LiveSimulationData.ts | 112 +++++++++++++----- src/simulation/PreComputedSimulationData.ts | 14 +-- src/types/index.ts | 9 +- 7 files changed, 264 insertions(+), 86 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 72387c98..82781ba9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -85,9 +85,13 @@ function App() { const [inputConcentration, setInputConcentration] = useState({ [AgentName.A]: - LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.A], + LiveSimulationData.INITIAL_CONCENTRATIONS[Module.A_B_AB][ + AgentName.A + ], [AgentName.B]: - LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.B], + LiveSimulationData.INITIAL_CONCENTRATIONS[Module.A_B_AB][ + AgentName.B + ], }); const [timeFactor, setTimeFactor] = useState( LiveSimulationData.INITIAL_TIME_FACTOR @@ -108,9 +112,13 @@ function App() { const [liveConcentration, setLiveConcentration] = useState({ [AgentName.A]: - LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.A], + LiveSimulationData.INITIAL_CONCENTRATIONS[Module.A_B_AB][ + AgentName.A + ], [AgentName.B]: - LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.B], + LiveSimulationData.INITIAL_CONCENTRATIONS[Module.A_B_AB][ + AgentName.B + ], [productName]: 0, }); const [recordedInputConcentration, setRecordedInputConcentration] = @@ -182,21 +190,26 @@ function App() { const clientSimulator = useMemo(() => { const activeAgents = simulationData.getActiveAgents(currentModule); setInputConcentration( - simulationData.getInitialConcentrations(activeAgents) + simulationData.getInitialConcentrations( + activeAgents, + currentModule, + sectionType === Section.Experiment + ) ); resetCurrentRunAnalysisState(); - const trajectory = - simulationData.createAgentsFromConcentrations(activeAgents); + const trajectory = simulationData.createAgentsFromConcentrations( + activeAgents, + currentModule, + sectionType === Section.Experiment + ); if (!trajectory) { return null; } const longestAxis = Math.max(viewportSize.width, viewportSize.height); - const productColor = simulationData.getAgentColor(productName); const startMixed = sectionType !== Section.Introduction; return new BindingSimulator( trajectory, longestAxis / 3, - productColor, startMixed ? InitialCondition.RANDOM : InitialCondition.SORTED ); }, [ @@ -205,7 +218,6 @@ function App() { resetCurrentRunAnalysisState, viewportSize.width, viewportSize.height, - productName, sectionType, ]); @@ -304,6 +316,21 @@ function App() { [currentProductConcentrationArray, productOverTimeTraces] ); + const setExperiment = () => { + setIsPlaying(false); + + const activeAgents = simulationData.getActiveAgents(currentModule); + const concentrations = simulationData.getInitialConcentrations( + activeAgents, + currentModule, + true + ); + clientSimulator?.mixAgents(); + setTimeFactor(LiveSimulationData.INITIAL_TIME_FACTOR); + setInputConcentration(concentrations); + setLiveConcentration(concentrations); + }; + const handleMixAgents = useCallback(() => { if (clientSimulator) { setIsPlaying(false); @@ -350,23 +377,25 @@ function App() { ] ); const totalReset = useCallback(() => { + const activeAgents = [AgentName.A, AgentName.B]; + setCurrentModule(Module.A_B_AB); + const concentrations = simulationData.getInitialConcentrations( + activeAgents, + Module.A_B_AB + ); setLiveConcentration({ - [AgentName.A]: - LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.A], - [AgentName.B]: - LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.B], + [AgentName.A]: concentrations[AgentName.A], + [AgentName.B]: concentrations[AgentName.B], [productName]: 0, }); setCurrentModule(Module.A_B_AB); setInputConcentration({ - [AgentName.A]: - LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.A], - [AgentName.B]: - LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.B], + [AgentName.A]: concentrations[AgentName.A], + [AgentName.B]: concentrations[AgentName.B], }); handleNewInputConcentration( adjustableAgentName, - LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.B] + concentrations[AgentName.B] ?? 4 ); setIsPlaying(false); clearAllAnalysisState(); @@ -376,6 +405,7 @@ function App() { handleNewInputConcentration, productName, adjustableAgentName, + simulationData, ]); // Special events in page navigation // usePageNumber takes a page number, a conditional and a callback @@ -494,8 +524,8 @@ function App() { }; const handleStartExperiment = () => { - simulariumController.pause(); - totalReset(); + clearAllAnalysisState(); + setExperiment(); setPage(page + 1); }; diff --git a/src/simulation/BindingInstance.ts b/src/simulation/BindingInstance.ts index 8435f1dd..5b886d51 100644 --- a/src/simulation/BindingInstance.ts +++ b/src/simulation/BindingInstance.ts @@ -144,12 +144,13 @@ class BindingInstance extends Circle { } this.setPosition(this.pos.x + xStep, this.pos.y + yStep); if (this.child) { + const child = this.child; // first check if it will unbind, otherwise rotate const unbind = this.checkWillUnbind(this.child); if (!unbind) { this.rotateGroup(xStep, yStep); } else { - return true; + return child; } } } diff --git a/src/simulation/BindingSimulator2D.ts b/src/simulation/BindingSimulator2D.ts index 7f85d799..e6b6dfc2 100644 --- a/src/simulation/BindingSimulator2D.ts +++ b/src/simulation/BindingSimulator2D.ts @@ -13,6 +13,7 @@ import { } from "@aics/simularium-viewer"; import { + AgentName, InitialCondition, InputAgent, ProductName, @@ -31,23 +32,23 @@ export default class BindingSimulator implements IClientSimulatorImpl { timeFactor: number; static: boolean = false; initialState: boolean = true; + currentComplexMap: Map = new Map(); currentNumberBound: number = 0; currentNumberOfBindingEvents: number = 0; currentNumberOfUnbindingEvents: number = 0; onUpdate: (data: number) => void = () => {}; numberAgentOnLeft: number = 0; numberAgentOnRight: number = 0; - productColor: string = ""; + productColor: Map; size: number; constructor( agents: InputAgent[], size: number, - productColor: string, initPositions: InitialCondition = InitialCondition.SORTED, timeFactor: number = LiveSimulationData.DEFAULT_TIME_FACTOR ) { this.size = size; - this.productColor = productColor; + this.productColor = new Map(); this.system = new System(); this.createBoundingLines(); this.distanceFactor = 40; @@ -59,11 +60,46 @@ export default class BindingSimulator implements IClientSimulatorImpl { private clearAgents() { this.currentNumberBound = 0; + this.currentComplexMap.clear(); + this.productColor.clear(); this.currentNumberOfBindingEvents = 0; this.currentNumberOfUnbindingEvents = 0; this.system = new System(); this.instances = []; } + private getProductIdByProductName(productName: ProductName) { + let agent1: InputAgent | undefined; + let agent2: InputAgent | undefined; + switch (productName) { + case ProductName.AB: + agent1 = this.agents.find((a) => a.name === AgentName.A); + agent2 = this.agents.find((a) => a.name === AgentName.B); + break; + case ProductName.AC: + agent1 = this.agents.find((a) => a.name === AgentName.A); + agent2 = this.agents.find((a) => a.name === AgentName.C); + break; + case ProductName.AD: + agent1 = this.agents.find((a) => a.name === AgentName.A); + agent2 = this.agents.find((a) => a.name === AgentName.D); + break; + } + if (!agent1 || !agent2) { + throw new Error("Invalid product name"); + } + return this.getProductIdByAgents(agent1, agent2); + } + + private getProductIdByAgents( + agent1: BindingInstance | InputAgent, + agent2: BindingInstance | InputAgent + ) { + if (agent1.id > agent2.id) { + return `${agent1.id}#${agent2.id}`; + } else { + return `${agent2.id}#${agent1.id}`; + } + } private getRandomPoint() { return [ @@ -72,6 +108,10 @@ export default class BindingSimulator implements IClientSimulatorImpl { ]; } + private getBoundTypeId(id: number, partnerId: number) { + return 100 + id * 10 + partnerId; + } + private initializeAgents( agents: InputAgent[], initPositions: InitialCondition = InitialCondition.SORTED @@ -86,6 +126,11 @@ export default class BindingSimulator implements IClientSimulatorImpl { agent.initialConcentration ); } + if (agent.complexColor) { + this.productColor.set(agent.id, agent.complexColor); + } + this.currentComplexMap.set(agent.id.toString(), 0); + for (let j = 0; j < agent.count; ++j) { let position: number[] = []; if (initPositions === InitialCondition.RANDOM) { @@ -290,13 +335,16 @@ 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.currentNumberBound + agent.count - this.currentComplexMap.get(agent.id.toString())! ); return acc; }, init); - concentrations[product] = this.convertCountToConcentration( - this.currentNumberBound - ); + const productId = this.getProductIdByProductName(product); + if (productId) { + concentrations[product] = this.convertCountToConcentration( + this.currentComplexMap.get(productId) || 0 + ); + } return concentrations; } @@ -325,13 +373,14 @@ export default class BindingSimulator implements IClientSimulatorImpl { private updateAgentsPositions() { for (let i = 0; i < this.instances.length; ++i) { - const unbindingOccurred = this.instances[i].oneStep( + const releasedChild = this.instances[i].oneStep( this.size, this.timeFactor ); - if (unbindingOccurred) { + if (releasedChild) { this.currentNumberOfUnbindingEvents++; this.currentNumberBound--; + this.incrementBoundCounts(this.instances[i], releasedChild, -1); } } } @@ -365,6 +414,27 @@ export default class BindingSimulator implements IClientSimulatorImpl { } } + private incrementBoundCounts( + a: BindingInstance, + b: BindingInstance, + amount: number + ) { + const complexName = this.getProductIdByAgents(a, b); + this.currentComplexMap.set( + complexName, + (this.currentComplexMap.get(complexName) || 0) + amount + ); + + const previousValue = this.currentComplexMap.get(a.id.toString()) || 0; + const nextValue = previousValue + amount; + + this.currentComplexMap.set(a.id.toString(), nextValue); + this.currentComplexMap.set( + b.id.toString(), + (this.currentComplexMap.get(b.id.toString()) || 0) + amount + ); + } + private resolveBindingReactions() { this.system.checkAll((response: Response) => { const { a, b, overlapV } = response; @@ -454,6 +524,8 @@ export default class BindingSimulator implements IClientSimulatorImpl { const typeMapping: EncodedTypeMapping = {}; const size = this.size; for (let i = 0; i < this.agents.length; ++i) { + const id = this.agents[i].id; + typeMapping[this.agents[i].id] = { name: `${this.agents[i].name}`, geometry: { @@ -462,14 +534,27 @@ export default class BindingSimulator implements IClientSimulatorImpl { url: "", }, }; - typeMapping[this.agents[i].id + 100] = { - name: `${this.agents[i].name}#bound`, - geometry: { - color: this.productColor, - displayType: GeometryDisplayType.SPHERE, - url: "", - }, - }; + if (this.agents[i].partners.length > 0) { + for (let j = 0; j < this.agents[i].partners.length; ++j) { + const partnerId = this.agents[i].partners[j]; + const complexId = this.getBoundTypeId(id, partnerId); + const partner = this.agents.find((a) => a.id === partnerId); + if (!partner) { + continue; + } + typeMapping[complexId] = { + name: `${this.agents[i].name}#${partner.name}`, + geometry: { + color: + this.productColor.get(partnerId) || + this.productColor.get(id) || + "", + displayType: GeometryDisplayType.SPHERE, + url: "", + }, + }; + } + } } return { // TODO get msgType and connId out of here diff --git a/src/simulation/ISimulationData.ts b/src/simulation/ISimulationData.ts index b65296fe..dc6a269d 100644 --- a/src/simulation/ISimulationData.ts +++ b/src/simulation/ISimulationData.ts @@ -1,12 +1,14 @@ import { AGENT_AB_COLOR, AGENT_AC_COLOR, + AGENT_AD_COLOR, AGENT_A_COLOR, AGENT_B_COLOR, AGENT_C_COLOR, + AGENT_D_COLOR, } from "../constants/colors"; import { - AgentFunction, + AgentType, AgentName, CurrentConcentration, InputAgent, @@ -15,11 +17,13 @@ import { } from "../types"; export const AGENT_AND_PRODUCT_COLORS = { - [AgentFunction.Fixed]: AGENT_A_COLOR, - [AgentFunction.Adjustable]: AGENT_B_COLOR, - [AgentFunction.Competitor]: AGENT_C_COLOR, - [AgentFunction.Complex_1]: AGENT_AB_COLOR, - [AgentFunction.Complex_2]: AGENT_AC_COLOR, + [AgentType.Fixed]: AGENT_A_COLOR, + [AgentType.Adjustable_1]: AGENT_B_COLOR, + [AgentType.Adjustable_2]: AGENT_C_COLOR, + [AgentType.Competitor]: AGENT_D_COLOR, + [AgentType.Complex_1]: AGENT_AB_COLOR, + [AgentType.Complex_2]: AGENT_AC_COLOR, + [AgentType.Complex_3]: AGENT_AD_COLOR, }; export enum TrajectoryType { @@ -31,11 +35,12 @@ interface ISimulationData { type: TrajectoryType; getCurrentProduct: (module: Module) => ProductName; getMaxConcentration: (module: Module) => number; - getAgentFunction: (name: AgentName | ProductName) => AgentFunction; + getAgentType: (name: AgentName | ProductName) => AgentType; getAgentColor: (agentName: AgentName | ProductName) => string; getActiveAgents: (currentModule: Module) => AgentName[]; getInitialConcentrations: ( - activeAgents: AgentName[] + activeAgents: AgentName[], + module: Module ) => CurrentConcentration; createAgentsFromConcentrations: () => InputAgent[] | null; } diff --git a/src/simulation/LiveSimulationData.ts b/src/simulation/LiveSimulationData.ts index a3dd36a2..2bddcb1e 100644 --- a/src/simulation/LiveSimulationData.ts +++ b/src/simulation/LiveSimulationData.ts @@ -1,5 +1,5 @@ import { - AgentFunction, + AgentType, AgentName, CurrentConcentration, InputAgent, @@ -8,8 +8,12 @@ import { } from "../types"; import { AGENT_A_COLOR, + AGENT_AB_COLOR, + AGENT_AC_COLOR, + AGENT_AD_COLOR, AGENT_B_COLOR, AGENT_C_COLOR, + AGENT_D_COLOR, } from "../constants/colors"; import ISimulationData, { AGENT_AND_PRODUCT_COLORS, @@ -22,7 +26,7 @@ const agentA: InputAgent = { name: AgentName.A, initialConcentration: 0, radius: 3, - partners: [1, 2], + partners: [1, 2, 3], color: AGENT_A_COLOR, }; @@ -35,6 +39,7 @@ const agentB: InputAgent = { kOn: 0.9, kOff: 0.01, color: AGENT_B_COLOR, + complexColor: AGENT_AB_COLOR, }; const agentC: InputAgent = { @@ -46,25 +51,40 @@ const agentC: InputAgent = { kOn: 0.3, kOff: 0.9, color: AGENT_C_COLOR, + complexColor: AGENT_AC_COLOR, +}; + +const agentD: InputAgent = { + id: 3, + name: AgentName.D, + initialConcentration: 0, + radius: 1.2, + partners: [0], + kOn: 0.99, + kOff: 0.001, + color: AGENT_D_COLOR, + complexColor: AGENT_AD_COLOR, }; export default class LiveSimulation implements ISimulationData { static ESTIMATED_SOLUTIONS = { [Module.A_B_AB]: 0.75, [Module.A_C_AC]: 74, - [Module.A_B_D_AB]: 5, + [Module.A_B_D_AB]: 1.5, }; - static NAME_TO_FUNCTION_MAP = { - [AgentName.A]: AgentFunction.Fixed, - [AgentName.B]: AgentFunction.Adjustable, - [AgentName.C]: AgentFunction.Competitor, - [ProductName.AB]: AgentFunction.Complex_1, - [ProductName.AC]: AgentFunction.Complex_2, + static NAME_TO_TYPE_MAP = { + [AgentName.A]: AgentType.Fixed, + [AgentName.B]: AgentType.Adjustable_1, + [AgentName.C]: AgentType.Adjustable_2, + [AgentName.D]: AgentType.Competitor, + [ProductName.AB]: AgentType.Complex_1, + [ProductName.AC]: AgentType.Complex_2, + [ProductName.AD]: AgentType.Complex_3, }; static ADJUSTABLE_AGENT_MAP = { [Module.A_B_AB]: AgentName.B, [Module.A_C_AC]: AgentName.C, - [Module.A_B_D_AB]: AgentName.B, + [Module.A_B_D_AB]: AgentName.D, }; static INITIAL_TIME_FACTOR: number = 30; static DEFAULT_TIME_FACTOR: number = 90; @@ -72,11 +92,31 @@ export default class LiveSimulation implements ISimulationData { [AgentName.A]: agentA, [AgentName.B]: agentB, [AgentName.C]: agentC, + [AgentName.D]: agentD, }; static INITIAL_CONCENTRATIONS = { - [AgentName.A]: 10, - [AgentName.B]: 4, - [AgentName.C]: 30, + [Module.A_B_AB]: { + [AgentName.A]: 5, + [AgentName.B]: 4, + }, + [Module.A_C_AC]: { + [AgentName.A]: 5, + [AgentName.C]: 30, + }, + [Module.A_B_D_AB]: { + [AgentName.A]: 2, + [AgentName.B]: 2, + [AgentName.D]: 2, + }, + }; + // for competitive binding we want to start the experiment with zero D but + // still have it in the introduction + static EXPERIMENT_CONCENTRATIONS = { + ...this.INITIAL_CONCENTRATIONS, + [Module.A_B_D_AB]: { + ...this.INITIAL_CONCENTRATIONS[Module.A_B_D_AB], + [AgentName.D]: 0, + }, }; PRODUCT = { [Module.A_B_AB]: ProductName.AB, @@ -90,18 +130,18 @@ export default class LiveSimulation implements ISimulationData { return this.PRODUCT[module]; }; - getAgentFunction = (name: AgentName | ProductName): AgentFunction => { + getAgentType = (name: AgentName | ProductName): AgentType => { return ( - LiveSimulation.NAME_TO_FUNCTION_MAP as Record< + LiveSimulation.NAME_TO_TYPE_MAP as Record< AgentName | ProductName, - AgentFunction + AgentType > )[name]; }; getAgentColor = (name: AgentName | ProductName): string => { - const agentFunction = this.getAgentFunction(name); - return AGENT_AND_PRODUCT_COLORS[agentFunction]; + const type = this.getAgentType(name); + return AGENT_AND_PRODUCT_COLORS[type]; }; getMaxConcentration = (module: Module): number => { @@ -114,15 +154,28 @@ export default class LiveSimulation implements ISimulationData { maxConcentration = 75; break; case Module.A_B_D_AB: - maxConcentration = 20; //TODO: adjust these as needed + maxConcentration = 10; //TODO: adjust these as needed break; } return maxConcentration; }; createAgentsFromConcentrations = ( - activeAgents?: AgentName[] + activeAgents?: AgentName[], + module?: Module, + isExperiment: boolean = false ): InputAgent[] => { + if (!module) { + throw new Error("Module must be specified to create agents."); + } + if (!activeAgents) { + activeAgents = this.getActiveAgents(module); + } + const concentrations = this.getInitialConcentrations( + activeAgents, + module, + isExperiment + ); return (activeAgents ?? []).map((agentName: AgentName) => { const agent = { ...( @@ -132,10 +185,7 @@ export default class LiveSimulation implements ISimulationData { > )[agentName], }; - agent.initialConcentration = - LiveSimulation.INITIAL_CONCENTRATIONS[ - agentName as keyof typeof LiveSimulation.INITIAL_CONCENTRATIONS - ]; + agent.initialConcentration = concentrations[agentName] ?? 0; return agent; }); }; @@ -147,22 +197,24 @@ export default class LiveSimulation implements ISimulationData { case Module.A_C_AC: return [AgentName.A, AgentName.C]; case Module.A_B_D_AB: - return [AgentName.A, AgentName.B, AgentName.C]; + return [AgentName.A, AgentName.B, AgentName.D]; default: return []; } }; // filters down to the active agents getInitialConcentrations = ( - activeAgents: AgentName[] + activeAgents: AgentName[], + module: Module, + isExperiment: boolean = false ): CurrentConcentration => { + const concentrations = isExperiment + ? { ...LiveSimulation.EXPERIMENT_CONCENTRATIONS[module] } + : { ...LiveSimulation.INITIAL_CONCENTRATIONS[module] }; return activeAgents.reduce((acc, agent) => { return { ...acc, - [agent]: - LiveSimulation.INITIAL_CONCENTRATIONS[ - agent as keyof typeof LiveSimulation.INITIAL_CONCENTRATIONS - ], + [agent]: (concentrations as Record)[agent], }; }, {}); }; diff --git a/src/simulation/PreComputedSimulationData.ts b/src/simulation/PreComputedSimulationData.ts index 89742c3a..3b5e6e70 100644 --- a/src/simulation/PreComputedSimulationData.ts +++ b/src/simulation/PreComputedSimulationData.ts @@ -1,5 +1,5 @@ import { - AgentFunction, + AgentType, AgentName, CurrentConcentration, InputAgent, @@ -14,9 +14,9 @@ import { MICRO } from "../constants"; export default class PreComputedSimulationData implements ISimulationData { static NAME_TO_FUNCTION_MAP = { - [AgentName.Antibody]: AgentFunction.Fixed, - [AgentName.Antigen]: AgentFunction.Adjustable, - [ProductName.AntibodyAntigen]: AgentFunction.Complex_1, + [AgentName.Antibody]: AgentType.Fixed, + [AgentName.Antigen]: AgentType.Adjustable_1, + [ProductName.AntibodyAntigen]: AgentType.Complex_1, }; static EXAMPLE_TRAJECTORY_URLS = { [Module.A_B_AB]: @@ -56,17 +56,17 @@ export default class PreComputedSimulationData implements ISimulationData { return maxConcentration; }; - getAgentFunction = (name: AgentName | ProductName): AgentFunction => { + getAgentType = (name: AgentName | ProductName): AgentType => { return ( PreComputedSimulationData.NAME_TO_FUNCTION_MAP as Record< AgentName | ProductName, - AgentFunction + AgentType > )[name]; }; getAgentColor = (name: AgentName | ProductName): string => { - const agentFunction = this.getAgentFunction(name); + const agentFunction = this.getAgentType(name); return AGENT_AND_PRODUCT_COLORS[agentFunction]; }; diff --git a/src/types/index.ts b/src/types/index.ts index 26b7d37a..ba58126b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -28,18 +28,21 @@ export const enum LayoutType { PreComputedSimulation = "pre-computed-simulation", } -export enum AgentFunction { +export enum AgentType { Fixed = "Fixed", - Adjustable = "Adjustable", + Adjustable_1 = "Adjustable_1", + Adjustable_2 = "Adjustable_2", Competitor = "Competitor", Complex_1 = "Complex_1", Complex_2 = "Complex_2", + Complex_3 = "Complex_3", } export enum AgentName { A = "A", B = "B", C = "C", + D = "D", Antibody = "Antibody", Antigen = "Antigen", } @@ -47,6 +50,7 @@ export enum AgentName { export enum ProductName { AB = "AB", AC = "AC", + AD = "AD", AntibodyAntigen = "Antibody-Antigen", Hemoglobin = "Hemoglobin", } @@ -71,6 +75,7 @@ export interface InputAgent { kOff?: number; count?: number; color: string; + complexColor?: string; } export interface PageContent { From e4c3d4a5c0e9d6274074504d19e70d63e059d193 Mon Sep 17 00:00:00 2001 From: meganrm Date: Fri, 21 Nov 2025 15:34:00 -0800 Subject: [PATCH 18/35] use type id --- src/simulation/BindingSimulator2D.ts | 48 +++++++++++++--------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/simulation/BindingSimulator2D.ts b/src/simulation/BindingSimulator2D.ts index e6b6dfc2..0cc280dc 100644 --- a/src/simulation/BindingSimulator2D.ts +++ b/src/simulation/BindingSimulator2D.ts @@ -101,6 +101,12 @@ export default class BindingSimulator implements IClientSimulatorImpl { } } + private getProductColor(id: number, partnerId: number) { + return ( + this.productColor.get(partnerId) || this.productColor.get(id) || "" + ); + } + private getRandomPoint() { return [ random(-this.size / 2, this.size / 2, true), @@ -138,10 +144,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { // of the agents on the sides of the bounding box position = this.getRandomPoint(); } else { - position = this.getRandomPointOnSide( - agent.id, - agents.length - ); + position = this.getRandomPointOnSide(agent.id); } const circle = new Circle( new Vector(...position), @@ -207,17 +210,12 @@ export default class BindingSimulator implements IClientSimulatorImpl { return concentration; } - private getRandomPointOnSide(side: number, total: number) { + private getRandomPointOnSide(side: number) { const size = this.size; const buffer = size / 20; const dFromSide = random(0 + buffer, size / 2, true); - let dAlongSide = random(-size / 2, size / 2, true); + const dAlongSide = random(-size / 2, size / 2, true); - if (total > 2 && side === 1) { - dAlongSide = random(0, size / 2, true); - } else if (total > 2 && side === 2) { - dAlongSide = random(-size / 2, 0, true); - } switch (side) { case 0: return [-dFromSide, dAlongSide]; @@ -283,10 +281,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { if (initPositions === InitialCondition.RANDOM) { position = this.getRandomPoint(); } else { - position = this.getRandomPointOnSide( - agent.id, - this.agents.length - ); + position = this.getRandomPointOnSide(agent.id); } const circle = new Circle( @@ -352,13 +347,15 @@ export default class BindingSimulator implements IClientSimulatorImpl { const agentData: number[] = []; for (let ii = 0; ii < this.instances.length; ++ii) { const instance = this.instances[ii]; + let typeId = instance.id; + if (instance.parent) { + typeId = this.getBoundTypeId(instance.id, instance.parent.id); + } else if (instance.child) { + typeId = this.getBoundTypeId(instance.id, instance.child.id); + } agentData.push(VisTypes.ID_VIS_TYPE_DEFAULT); // vis type agentData.push(ii); // instance id - agentData.push( - instance.bound || instance.child - ? 100 + instance.id - : instance.id - ); // type + agentData.push(typeId); // type agentData.push(instance.pos.x); // x agentData.push(instance.pos.y); // y agentData.push(0); // z @@ -452,9 +449,10 @@ export default class BindingSimulator implements IClientSimulatorImpl { if (unbound) { this.currentNumberOfUnbindingEvents++; this.currentNumberBound--; + this.incrementBoundCounts(a, b, -1); } } - if (a.partners.includes(b.id)) { + if (a.partners.includes(b.id) && !a.isBoundPair(b)) { // a is the ligand let bound = false; if (a.r < b.r) { @@ -466,6 +464,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { if (bound) { this.currentNumberOfBindingEvents++; this.currentNumberBound++; + this.incrementBoundCounts(a, b, 1); } } } else { @@ -542,13 +541,12 @@ export default class BindingSimulator implements IClientSimulatorImpl { if (!partner) { continue; } + const color = this.getProductColor(id, partnerId); + console.log("color", this.agents[i].name, color); typeMapping[complexId] = { name: `${this.agents[i].name}#${partner.name}`, geometry: { - color: - this.productColor.get(partnerId) || - this.productColor.get(id) || - "", + color: color, displayType: GeometryDisplayType.SPHERE, url: "", }, From a85f67be1ddc4a4ce7581b0555017afe36472569 Mon Sep 17 00:00:00 2001 From: meganrm Date: Fri, 21 Nov 2025 15:53:40 -0800 Subject: [PATCH 19/35] content adjustments --- src/content/Competitive.tsx | 58 ++++++++++++++++++------------------- src/content/LowAffinity.tsx | 2 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/content/Competitive.tsx b/src/content/Competitive.tsx index 1f48078b..60685f38 100644 --- a/src/content/Competitive.tsx +++ b/src/content/Competitive.tsx @@ -51,11 +51,10 @@ export const competitiveArray: PageContent[] = [ title: "Competitive Binding", content: ( <> - We now know the binding affinity of and . Our - competitive inhibitor, , also binds to and we want to - figure out how effective it is at blocking the binding of . - We cannot directly measure the formation of (which is why - it's shown in grey). + In the "High Affinity" section we determined the binding + affinity of and . Our new competitive inhibitor,{" "} + , also binds to . Once it binds to , can no + longer bind. ), layout: LayoutType.LiveSimulation, @@ -64,54 +63,56 @@ export const competitiveArray: PageContent[] = [ callToAction: "Press play and watch the how the two different complexes form over time.", }, - { title: "Start the experiment", content: ( <> - Now, let's use this simulation to make measurements. We'll start - with [] = 0 so we can get the formation of without - any competition. + We cannot directly measure the formation of (which is why + it's shown in grey). To figure out how strongly binds to{" "} + we're going to keep track of how adding more {" "} + decreases the formation of . ), + layout: LayoutType.LiveSimulation, + section: Section.Introduction, actionButton: , + progressionElement: START_EXPERIMENT_ID, callToAction: ( <> Click the Start experiment button to reset the simulation and begin by pressing play! ), - section: Section.Introduction, - layout: LayoutType.LiveSimulation, - progressionElement: START_EXPERIMENT_ID, }, { title: "Start the experiment", content: ( <> - Now, let's use this simulation to make measurements. We'll start - with [] = 0 so we can get the formation of without - any competition. + We cannot directly measure the formation of (which is why + it's shown in grey). To figure out how strongly binds to{" "} + we're going to keep track of how adding more affects + the amount of formed. ), + layout: LayoutType.LiveSimulation, + section: Section.Introduction, actionButton: , + progressionElement: PLAY_BUTTON_ID, callToAction: ( <> Click the Start experiment button to reset the simulation and begin by pressing play! ), - section: Section.Experiment, - layout: LayoutType.LiveSimulation, - progressionElement: PLAY_BUTTON_ID, }, + { title: "Find maximum complex formation", content: ( <> - With [] = 0, the complex is able to form without any - inhibition. This will be our baseline for the max amount of{" "} - that can form at these concentrations.{" "} + We are starting with [] = 0, so the complex is able + to form without any inhibition. This will be our baseline for + the max amount of that can form at these concentrations.{" "} ), callToAction: ( @@ -130,7 +131,7 @@ export const competitiveArray: PageContent[] = [ title: "Introduce the competitive inhibitor", content: ( <> - Now let's see how the addition of affects the formation of{" "} + Now let's see how the addition of effects the formation of{" "} . ), @@ -151,10 +152,10 @@ export const competitiveArray: PageContent[] = [ title: "Repeating the experiment", content: ( <> - We want to understand the effect - has on the formation of . Let’s repeat the - experiment with a new concentration of . We will keep the - concentration of and constant. + We want to understand the affect has on the formation of{" "} + . Let’s repeat the experiment with a new concentration of{" "} + . We will keep the concentration of and {" "} + constant. ), callToAction: ( @@ -175,8 +176,7 @@ export const competitiveArray: PageContent[] = [ The constant Ki is analogous to Kd for inhibitors. Ki can be determined in this experiment by finding the IC50, the concentration of inhibitor - where the amount of - complex is reduced by half. + where the amount of complex is reduced by half. ), moreInfo: ( @@ -197,7 +197,7 @@ export const competitiveArray: PageContent[] = [ }, { content: - "Congratulations, you’ve completed the High Affinity experiment!", + "Congratulations, you’ve completed the Competitive Binding experiment!", backButton: true, // nextButton: true, nextButtonText: "View examples", diff --git a/src/content/LowAffinity.tsx b/src/content/LowAffinity.tsx index f6726c86..665e9acc 100644 --- a/src/content/LowAffinity.tsx +++ b/src/content/LowAffinity.tsx @@ -59,7 +59,7 @@ export const lowAffinityContentArray: PageContent[] = [ }, { content: - "Congratulations, you’ve completed the High Affinity experiment!", + "Congratulations, you’ve completed the Low Affinity experiment!", backButton: true, // nextButton: true, nextButtonText: "View examples", From 8064b34e44a95c7fc65a48f8f636ce86b9d90621 Mon Sep 17 00:00:00 2001 From: meganrm Date: Fri, 21 Nov 2025 15:59:25 -0800 Subject: [PATCH 20/35] change vis control --- src/components/main-layout/LeftPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/main-layout/LeftPanel.tsx b/src/components/main-layout/LeftPanel.tsx index ba6ddf8a..851f368d 100644 --- a/src/components/main-layout/LeftPanel.tsx +++ b/src/components/main-layout/LeftPanel.tsx @@ -38,7 +38,6 @@ const LeftPanel: React.FC = ({ const eventsOverTimeExcludedPages = { [Module.A_B_AB]: [0, 1, 2], [Module.A_C_AC]: [], - [Module.A_B_D_AB]: [0, 1, 2, 3, 4], }; return ( <> @@ -53,6 +52,7 @@ const LeftPanel: React.FC = ({ Date: Fri, 21 Nov 2025 16:30:59 -0800 Subject: [PATCH 21/35] remove log --- src/simulation/BindingSimulator2D.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/simulation/BindingSimulator2D.ts b/src/simulation/BindingSimulator2D.ts index 0cc280dc..329e6687 100644 --- a/src/simulation/BindingSimulator2D.ts +++ b/src/simulation/BindingSimulator2D.ts @@ -542,7 +542,6 @@ export default class BindingSimulator implements IClientSimulatorImpl { continue; } const color = this.getProductColor(id, partnerId); - console.log("color", this.agents[i].name, color); typeMapping[complexId] = { name: `${this.agents[i].name}#${partner.name}`, geometry: { From 060b30eb8675058b6082f20f5a8fe3dc63f9339c Mon Sep 17 00:00:00 2001 From: meganrm Date: Fri, 21 Nov 2025 18:27:14 -0800 Subject: [PATCH 22/35] lift state up --- src/App.tsx | 17 ++++++++++++++ src/components/ViewSwitch.tsx | 43 +++++++---------------------------- src/simulation/context.tsx | 6 ++++- src/types/index.ts | 5 ++++ 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 82781ba9..1d9af659 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,7 @@ import { ScatterTrace, Section, TrajectoryStatus, + ViewType, } from "./types"; import LeftPanel from "./components/main-layout/LeftPanel"; import RightPanel from "./components/main-layout/RightPanel"; @@ -56,6 +57,7 @@ import useModule from "./hooks/useModule"; import LandingPage from "./components/LandingPage"; function App() { + const [currentView, setCurrentView] = useState(ViewType.Lab); const [page, setPage] = useState(FIRST_PAGE[Module.A_B_AB]); const [time, setTime] = useState(0); const [isPlaying, setIsPlaying] = useState(false); @@ -377,6 +379,7 @@ function App() { ] ); const totalReset = useCallback(() => { + setCurrentView(ViewType.Lab); const activeAgents = [AgentName.A, AgentName.B]; setCurrentModule(Module.A_B_AB); const concentrations = simulationData.getInitialConcentrations( @@ -521,6 +524,12 @@ function App() { clearAllAnalysisState(); setCurrentModule(module); setIsPlaying(false); + // the first module is the only one that starts with the lab view + if (module === Module.A_B_AB) { + setCurrentView(ViewType.Lab); + } else { + setCurrentView(ViewType.Simulation); + } }; const handleStartExperiment = () => { @@ -618,6 +627,12 @@ function App() { }, 3000); }; + const handleSwitchView = () => { + setCurrentView((prevView) => + prevView === ViewType.Lab ? ViewType.Simulation : ViewType.Lab + ); + }; + const handleRecordEquilibrium = () => { if (!clientSimulator) { return false; @@ -717,11 +732,13 @@ function App() { setModule, setPage, setViewportSize, + setViewportType: handleSwitchView, simulariumController, timeFactor, timeUnit: simulationData.timeUnit, trajectoryName, viewportSize, + viewportType: currentView, addCompletedModule, completedModules, }} diff --git a/src/components/ViewSwitch.tsx b/src/components/ViewSwitch.tsx index b68ddef2..21d03c0b 100644 --- a/src/components/ViewSwitch.tsx +++ b/src/components/ViewSwitch.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from "react"; +import React, { useContext } from "react"; import Viewer from "./Viewer"; import { SimulariumContext } from "../simulation/context"; @@ -9,47 +9,19 @@ import LabIcon from "./icons/Lab"; import Molecules from "./icons/Molecules"; import LabView from "./LabView"; import VisibilityControl from "./shared/VisibilityControl"; -import { Module, Section } from "../types"; +import { Module, ViewType } from "../types"; import { FIRST_PAGE } from "../content"; -import useModule from "../hooks/useModule"; import { VIEW_SWITCH_ID } from "../constants"; -enum View { - Lab = "lab", - Simulation = "simulation", -} - const ViewSwitch: React.FC = () => { - const [currentView, setCurrentView] = useState(View.Lab); - const [previousModule, setPreviousModule] = useState(Module.A_B_AB); + const { viewportType, setViewportType } = useContext(SimulariumContext); - const switchView = () => { - setCurrentView((prevView) => - prevView === View.Lab ? View.Simulation : View.Lab - ); - }; const { page, isPlaying, setIsPlaying, handleTimeChange, module } = useContext(SimulariumContext); const isFirstPageOfFirstModule = page === FIRST_PAGE[module] + 1 && module === Module.A_B_AB; - if (isFirstPageOfFirstModule && currentView === View.Simulation) { - setCurrentView(View.Lab); - } - - const { contentData } = useModule(module); - - // Show the sim view at the beginning of the module - if (module !== previousModule) { - setPreviousModule(module); - if (contentData[page].section === Section.Experiment) { - if (currentView === View.Lab) { - setCurrentView(View.Simulation); - } - } - } - let buttonStyle: React.CSSProperties = { top: 16, right: 16, @@ -73,22 +45,23 @@ const ViewSwitch: React.FC = () => { ) : ( ) } > - {currentView === View.Lab ? "Molecular" : "Lab"} view + {viewportType === ViewType.Lab ? "Molecular" : "Lab"}{" "} + view - {currentView === View.Lab ? : null} + {viewportType === ViewType.Lab ? : null} void; setPage: (value: number) => void; setViewportSize: (value: { width: number; height: number }) => void; + setViewportType: () => void; simulariumController: SimulariumController | null; timeFactor: number; timeUnit: string; trajectoryName: string; viewportSize: { width: number; height: number }; + viewportType: ViewType; addCompletedModule: (value: Module) => void; completedModules: Set; } @@ -65,11 +67,13 @@ export const SimulariumContext = createContext({ setModule: () => {}, setPage: () => {}, setViewportSize: () => {}, + setViewportType: () => {}, simulariumController: null, timeFactor: 30, timeUnit: NANO, trajectoryName: LIVE_SIMULATION_NAME, viewportSize: DEFAULT_VIEWPORT_SIZE, + viewportType: ViewType.Lab, addCompletedModule: () => {}, completedModules: new Set(), } as SimulariumContextType); diff --git a/src/types/index.ts b/src/types/index.ts index ba58126b..991c312c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -153,3 +153,8 @@ export enum TrajectoryStatus { LOADED, ERROR, } + +export enum ViewType { + Lab = "lab", + Simulation = "simulation", +} From b270d357b07cbb181cd89504aa6c89a489a55a47 Mon Sep 17 00:00:00 2001 From: Megan Riel-Mehan Date: Tue, 25 Nov 2025 09:46:27 -0800 Subject: [PATCH 23/35] Update src/content/Competitive.tsx Co-authored-by: toloudis --- src/content/Competitive.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/Competitive.tsx b/src/content/Competitive.tsx index 60685f38..d8bb8d6f 100644 --- a/src/content/Competitive.tsx +++ b/src/content/Competitive.tsx @@ -131,7 +131,7 @@ export const competitiveArray: PageContent[] = [ title: "Introduce the competitive inhibitor", content: ( <> - Now let's see how the addition of effects the formation of{" "} + Now let's see how the addition of affects the formation of{" "} . ), From c39141dc0d6ad07619e696a64e1bdda06bcd1a5f Mon Sep 17 00:00:00 2001 From: Megan Riel-Mehan Date: Thu, 4 Dec 2025 17:00:39 -0800 Subject: [PATCH 24/35] Update src/App.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 1d9af659..4ad241e2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -398,7 +398,7 @@ function App() { }); handleNewInputConcentration( adjustableAgentName, - concentrations[AgentName.B] ?? 4 + concentrations[AgentName.B] ?? LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.B] ); setIsPlaying(false); clearAllAnalysisState(); From 52a6650c2084acfc9b7d4aa62f61c9fc2e730383 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Dec 2025 17:07:58 -0800 Subject: [PATCH 25/35] changes based on comments --- src/content/Competitive.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/content/Competitive.tsx b/src/content/Competitive.tsx index d8bb8d6f..9aac9f58 100644 --- a/src/content/Competitive.tsx +++ b/src/content/Competitive.tsx @@ -68,9 +68,9 @@ export const competitiveArray: PageContent[] = [ content: ( <> We cannot directly measure the formation of (which is why - it's shown in grey). To figure out how strongly binds to{" "} - we're going to keep track of how adding more {" "} - decreases the formation of . + it's shown in grey). To figure out how strongly binds to{" "} + we're going to keep track of how adding more affects + the amount of formed. ), layout: LayoutType.LiveSimulation, @@ -89,7 +89,7 @@ export const competitiveArray: PageContent[] = [ content: ( <> We cannot directly measure the formation of (which is why - it's shown in grey). To figure out how strongly binds to{" "} + it's shown in grey). To figure out how strongly binds to{" "} we're going to keep track of how adding more affects the amount of formed. @@ -112,7 +112,8 @@ export const competitiveArray: PageContent[] = [ <> We are starting with [] = 0, so the complex is able to form without any inhibition. This will be our baseline for - the max amount of that can form at these concentrations.{" "} + the maximum amount of that can form at these + concentrations.{" "} ), callToAction: ( @@ -152,7 +153,7 @@ export const competitiveArray: PageContent[] = [ title: "Repeating the experiment", content: ( <> - We want to understand the affect has on the formation of{" "} + We want to understand the effect has on the formation of{" "} . Let’s repeat the experiment with a new concentration of{" "} . We will keep the concentration of and {" "} constant. From 800f22f79c5c0892203255b16cb62e88bccfdb17 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Dec 2025 17:30:52 -0800 Subject: [PATCH 26/35] changes based on comments --- src/components/concentration-display/ConcentrationSlider.tsx | 2 +- .../concentration-display/LiveConcentrationDisplay.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/concentration-display/ConcentrationSlider.tsx b/src/components/concentration-display/ConcentrationSlider.tsx index 2093aed3..4574b388 100644 --- a/src/components/concentration-display/ConcentrationSlider.tsx +++ b/src/components/concentration-display/ConcentrationSlider.tsx @@ -71,7 +71,7 @@ const ConcentrationSlider: React.FC = ({ 0 - ? (index.toFixed(1) as unknown as number) + ? Number(index.toFixed(1)) : index } disabledNumbers={disabledNumbers} diff --git a/src/components/concentration-display/LiveConcentrationDisplay.tsx b/src/components/concentration-display/LiveConcentrationDisplay.tsx index 4d674d93..9734a1a3 100644 --- a/src/components/concentration-display/LiveConcentrationDisplay.tsx +++ b/src/components/concentration-display/LiveConcentrationDisplay.tsx @@ -19,7 +19,7 @@ const LiveConcentrationDisplay: React.FC = ({ const { maxConcentration, getAgentColor } = useContext(SimulariumContext); // the steps have a 2px gap, so we are adjusting the // size of the step based on the total number we want - const steps = Math.max(maxConcentration, 10); + const steps = maxConcentration; const size = width / steps - 2; return (
From 4806e876b42a4cee46c9480586df68644e8d4dbf Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Dec 2025 17:33:58 -0800 Subject: [PATCH 27/35] write logic consistently --- src/simulation/BindingSimulator2D.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/simulation/BindingSimulator2D.ts b/src/simulation/BindingSimulator2D.ts index 329e6687..b758eac1 100644 --- a/src/simulation/BindingSimulator2D.ts +++ b/src/simulation/BindingSimulator2D.ts @@ -422,14 +422,13 @@ export default class BindingSimulator implements IClientSimulatorImpl { (this.currentComplexMap.get(complexName) || 0) + amount ); - const previousValue = this.currentComplexMap.get(a.id.toString()) || 0; - const nextValue = previousValue + amount; + const previousValueA = this.currentComplexMap.get(a.id.toString()) || 0; + const nextValueA = previousValueA + amount; + this.currentComplexMap.set(a.id.toString(), nextValueA); - this.currentComplexMap.set(a.id.toString(), nextValue); - this.currentComplexMap.set( - b.id.toString(), - (this.currentComplexMap.get(b.id.toString()) || 0) + amount - ); + const previousValueB = this.currentComplexMap.get(b.id.toString()) || 0; + const nextValueB = previousValueB + amount; + this.currentComplexMap.set(b.id.toString(), nextValueB); } private resolveBindingReactions() { From 1fd19aea4005d2aa707d1fa0e7f72ab43ed4a204 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Dec 2025 17:35:45 -0800 Subject: [PATCH 28/35] add comment --- src/simulation/BindingSimulator2D.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/simulation/BindingSimulator2D.ts b/src/simulation/BindingSimulator2D.ts index b758eac1..b38d0d40 100644 --- a/src/simulation/BindingSimulator2D.ts +++ b/src/simulation/BindingSimulator2D.ts @@ -102,6 +102,8 @@ export default class BindingSimulator implements IClientSimulatorImpl { } private getProductColor(id: number, partnerId: number) { + // one and only one of the agents should have a product color + // so the order here is not important return ( this.productColor.get(partnerId) || this.productColor.get(id) || "" ); From e02a457423a80a03882168af360c0014b435f276 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Dec 2025 17:37:44 -0800 Subject: [PATCH 29/35] fix error --- src/App.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 4ad241e2..48e4e2cf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -398,7 +398,10 @@ function App() { }); handleNewInputConcentration( adjustableAgentName, - concentrations[AgentName.B] ?? LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.B] + concentrations[AgentName.B] ?? + LiveSimulationData.INITIAL_CONCENTRATIONS[Module.A_B_AB][ + AgentName.B + ] ); setIsPlaying(false); clearAllAnalysisState(); From 7fe65d56f9aba600ea5313bcf60f1d03ca04d1e0 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Dec 2025 17:41:45 -0800 Subject: [PATCH 30/35] fix ic50 --- src/content/Competitive.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/Competitive.tsx b/src/content/Competitive.tsx index 9aac9f58..8353efa5 100644 --- a/src/content/Competitive.tsx +++ b/src/content/Competitive.tsx @@ -182,13 +182,13 @@ export const competitiveArray: PageContent[] = [ ), moreInfo: ( <> - K50 = [] (at equilibrium when [] is half + IC50 = [] (at equilibrium when [] is half of max) ), callToAction: ( <> - Let’s find K50 - Repeat the experiment by pausing, + Let’s find IC50 - Repeat the experiment by pausing, adjusting the concentration of and recording the equilibrium point until you have enough data. From 59193e7eebbbe3be966e07182abf9acadd7a794c Mon Sep 17 00:00:00 2001 From: Megan Riel-Mehan Date: Thu, 4 Dec 2025 17:42:24 -0800 Subject: [PATCH 31/35] Update src/simulation/BindingSimulator2D.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/simulation/BindingSimulator2D.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/simulation/BindingSimulator2D.ts b/src/simulation/BindingSimulator2D.ts index b38d0d40..b96b7371 100644 --- a/src/simulation/BindingSimulator2D.ts +++ b/src/simulation/BindingSimulator2D.ts @@ -215,7 +215,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { private getRandomPointOnSide(side: number) { const size = this.size; const buffer = size / 20; - const dFromSide = random(0 + buffer, size / 2, true); + const dFromSide = random(buffer, size / 2, true); const dAlongSide = random(-size / 2, size / 2, true); switch (side) { From abe65567f37653e8aed450fc239ebfbed5e7e704 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Dec 2025 17:46:20 -0800 Subject: [PATCH 32/35] remove duplicate call --- src/App.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 48e4e2cf..cfc5eb7b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -391,7 +391,6 @@ function App() { [AgentName.B]: concentrations[AgentName.B], [productName]: 0, }); - setCurrentModule(Module.A_B_AB); setInputConcentration({ [AgentName.A]: concentrations[AgentName.A], [AgentName.B]: concentrations[AgentName.B], From 3e0de3108fca480258e4cba69f8cd73d512ab82a Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Dec 2025 17:47:28 -0800 Subject: [PATCH 33/35] add comment and error check --- src/simulation/BindingSimulator2D.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/simulation/BindingSimulator2D.ts b/src/simulation/BindingSimulator2D.ts index b96b7371..e763da85 100644 --- a/src/simulation/BindingSimulator2D.ts +++ b/src/simulation/BindingSimulator2D.ts @@ -96,8 +96,11 @@ export default class BindingSimulator implements IClientSimulatorImpl { ) { if (agent1.id > agent2.id) { return `${agent1.id}#${agent2.id}`; - } else { + } else if (agent2.id > agent1.id) { return `${agent2.id}#${agent1.id}`; + } else { + // currently no self-binding allowed + throw new Error("Agents cannot bind to themselves"); } } From 142f897f5ea29d2e11ea04f94c560693b4d812a7 Mon Sep 17 00:00:00 2001 From: Megan Riel-Mehan Date: Thu, 4 Dec 2025 17:50:50 -0800 Subject: [PATCH 34/35] Update src/simulation/BindingSimulator2D.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/simulation/BindingSimulator2D.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/simulation/BindingSimulator2D.ts b/src/simulation/BindingSimulator2D.ts index e763da85..13e34d55 100644 --- a/src/simulation/BindingSimulator2D.ts +++ b/src/simulation/BindingSimulator2D.ts @@ -119,6 +119,15 @@ export default class BindingSimulator implements IClientSimulatorImpl { ]; } + /** + * Generates a unique type ID for a bound complex using a "magic formula": + * 100 + id * 10 + partnerId + * This formula assumes that both `id` and `partnerId` are single-digit integers (0-9). + * This ensures that for any pair (id, partnerId), the result is unique as long as + * id and partnerId are not greater than 9. If either value exceeds 9, collisions may occur. + * The offset of 100 is used to avoid overlapping with other type IDs. + * If agent IDs may exceed 9, this formula should be updated to avoid collisions. + */ private getBoundTypeId(id: number, partnerId: number) { return 100 + id * 10 + partnerId; } From ec81d10d9887baf965ecd75d195bd652dbf3a3a7 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Dec 2025 18:00:58 -0800 Subject: [PATCH 35/35] more explicit checks --- src/simulation/BindingSimulator2D.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/simulation/BindingSimulator2D.ts b/src/simulation/BindingSimulator2D.ts index 13e34d55..6e488fcc 100644 --- a/src/simulation/BindingSimulator2D.ts +++ b/src/simulation/BindingSimulator2D.ts @@ -107,9 +107,14 @@ export default class BindingSimulator implements IClientSimulatorImpl { private getProductColor(id: number, partnerId: number) { // one and only one of the agents should have a product color // so the order here is not important - return ( - this.productColor.get(partnerId) || this.productColor.get(id) || "" - ); + const color1 = this.productColor.get(id); + 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.` + ); + } + return color2 || color1 || ""; } private getRandomPoint() {