From a9229262b684d17476e86c2495a55ed5a7bee417 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 25 Feb 2025 20:31:38 -0800 Subject: [PATCH 01/12] added two different complexes --- src/types/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/index.ts b/src/types/index.ts index 8009979..155228f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -30,7 +30,8 @@ export enum AgentFunction { Fixed = "Fixed", Adjustable = "Adjustable", Competitor = "Competitor", - Complex = "Complex", + Complex_1 = "Complex_1", + Complex_2 = "Complex_2", } export enum AgentName { From 9860a978bf68b2e632d323bb55bf44d33ebb9944 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 25 Feb 2025 20:32:15 -0800 Subject: [PATCH 02/12] use complex 1 --- src/simulation/PreComputedSimulationData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/simulation/PreComputedSimulationData.ts b/src/simulation/PreComputedSimulationData.ts index d7303b4..913c9d3 100644 --- a/src/simulation/PreComputedSimulationData.ts +++ b/src/simulation/PreComputedSimulationData.ts @@ -16,7 +16,7 @@ export default class PreComputedSimulationData implements ISimulationData { static NAME_TO_FUNCTION_MAP = { [AgentName.Antibody]: AgentFunction.Fixed, [AgentName.Antigen]: AgentFunction.Adjustable, - [ProductName.AntibodyAntigen]: AgentFunction.Complex, + [ProductName.AntibodyAntigen]: AgentFunction.Complex_1, }; static EXAMPLE_TRAJECTORY_URLS = { [Module.A_B_AB]: From 537c3ab84547a19079ae410b236b843d5eb250ed Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 25 Feb 2025 20:33:45 -0800 Subject: [PATCH 03/12] add second module content --- src/content/HighAffinity.tsx | 220 +++++++++++++++++++++++++++++++++ src/content/LowAffinity.tsx | 50 ++++++++ src/content/index.tsx | 233 ++--------------------------------- 3 files changed, 277 insertions(+), 226 deletions(-) create mode 100644 src/content/HighAffinity.tsx create mode 100644 src/content/LowAffinity.tsx diff --git a/src/content/HighAffinity.tsx b/src/content/HighAffinity.tsx new file mode 100644 index 0000000..5be96b6 --- /dev/null +++ b/src/content/HighAffinity.tsx @@ -0,0 +1,220 @@ +import BindingDiagrams from "../components/BindingDiagrams"; +import StartExperiment from "../components/StartExperiment"; +import { AB, A, B } from "../components/agent-symbols"; +import Definition from "../components/shared/Definition"; +import { PageContent, Section, LayoutType } from "../types"; + +export const highAffinityContentArray: PageContent[] = [ + // making the content array 1 indexed to match the page numbers + { + content: "", + section: Section.Introduction, + layout: LayoutType.FullScreenOverlay, + }, + { + content: + "Congratulations! You’ve just joined a biology lab. Your mentor has asked you to measure the strength of the binding interaction between two types of molecules. These molecules are too small to see, even under a microscope. But what if we could somehow see what the molecules in the tube are doing? What do you think you’d see?", + callToAction: + "Click or tap the animated button to switch your view to a molecular simulation.", + section: Section.Introduction, + layout: LayoutType.LiveSimulation, + }, + { + content: ( + <> + Computer simulations can help us explore phenomena that are + difficult to experience in reality. In this{" "} + , the molecules are + represented simply as circles. + + ), + callToAction: "What happens to the molecules once you press play?", + section: Section.Introduction, + layout: LayoutType.LiveSimulation, + }, + { + content: ( + <> + As the simulation plays, the molecules move by a random walk. + Watch what happens when they bump into each other. What do you + think the "Reaction events over time" graph is showing? + + ), + callToAction: + "After you've observed the simulation, click 'Lab view' to see what the cuvette looks like now.", + section: Section.Introduction, + layout: LayoutType.LiveSimulation, + }, + { + content: ( + <> + The clear liquid is slowly turning yellow as the simulation + progresses. Can you estimate the concentration of ? + + ), + callToAction: "Click 'Molecular view' to switch back.", + section: Section.Introduction, + layout: LayoutType.LiveSimulation, + }, + { + title: "Start the Experiment", + content: ( + <> + Now, let's use this simulation to make measurements. We're going + to increase the timestep so the experiments are fast. + + ), + actionButton: , + callToAction: + "Click the 'Start experiment' button to reset the simulation and begin by pressing play!", + section: Section.Introduction, + layout: LayoutType.LiveSimulation, + }, + { + title: "Start the Experiment", + content: ( + <> + Now, let's use this simulation to make measurements. We're going + to increase the timestep so the experiments are fast. + + ), + actionButton: , + callToAction: + "Click the 'Start experiment' button to reset the simulation and begin by pressing play!", + section: Section.Experiment, + layout: LayoutType.LiveSimulation, + }, + { + content: ( + <> + When the reaction first starts, the concentration of each + molecule changes rapidly as the two molecules, and , + bind to form . Eventually, the reaction reaches{" "} + , at which point the + concentration of stays more or less consistent. + + ), + 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, + }, + { + content: ( + <> + The goal of these experiments is to figure out how tightly two + molecules bind, or stick to each other. We call this{" "} + . + + ), + 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, + }, + { + content: ( + <> + We want to understand the affinity of{" "} + and{" "} + regardless of the + concentration of substrate. Let’s repeat the experiment with a + new concentration of . We will keep the concentration of{" "} + constant to avoid introducing more than one{" "} + at a time. + + ), + 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, + }, + { + content: ( + <> + We use the constant{" "} + + Kd + + } + />{" "} + to compare affinities. Kd can by determined in this + experiment by finding the concentration of at equilibrium + where half the binding sites of are occupied. + + ), + moreInfo: ( + <> + Kd = [] (at equilibrium when 50% of is + bound to ) + + ), + callToAction: ( + <> + Let’s find Kd - 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!", + backButton: true, + nextButton: true, + nextButtonText: "View examples", + section: Section.BonusContent, + layout: LayoutType.FullScreenOverlay, + }, + { + title: "Real-world example: Antibodies and antigens, high affinity binders", + content: ( + <> + The molecules you experimented with had a high affinity - + similar to an and an{" "} + it recognizes. Even if the antigen + is present at low concentrations, the antibody stays tightly + bound for a long time. This increases the total number of + complexes at any given time and alerts the immune system of the + antigen’s presence. + + ), + nextButton: true, + section: Section.BonusContent, + layout: LayoutType.PreComputedSimulation, + trajectoryUrl: + "https://aics-simularium-data.s3.us-east-2.amazonaws.com/trajectory/binding-affinity_antibodies.simularium", + }, + { + title: "Real-world example: Affinity is determined by intermolecular forces", + content: ( + <> + A pair of molecules’ binding affinity is determined by{" "} + . The first Antibody + has a lower binding affinity for the Antigen than the second + Antibody. Why, do you think, is this the case? + + ), + backButton: true, + nextButton: true, + nextButtonText: "Finish", + visualContent: , + section: Section.BonusContent, + layout: LayoutType.NoSidePanels, + }, +]; diff --git a/src/content/LowAffinity.tsx b/src/content/LowAffinity.tsx new file mode 100644 index 0000000..082e7d3 --- /dev/null +++ b/src/content/LowAffinity.tsx @@ -0,0 +1,50 @@ +import { A, B, C } from "../components/agent-symbols"; +import { LayoutType, PageContent, Section } from "../types"; + +export const lowAffinityContentArray: PageContent[] = [ + // making the content array 1 indexed to match the page numbers + { + content: "", + section: Section.Introduction, + layout: LayoutType.FullScreenOverlay, + }, + { + content: ( + <> + Molecule has a different binding affinity with molecule{" "} + . Let’s repeat our simulation experiments with and{" "} + + to determine the binding affinity Kd for this pair of + molecules. + + ), + layout: LayoutType.LiveSimulation, + section: Section.Experiment, + callToAction: + "Choose different concentrations of C and repeat the experiment to record the new equilibrium concentrations.", + }, + { + content: ( + <> + We can use the same method we used for and to + calculate the binding affinity (Kd) for and{" "} + . + + ), + layout: LayoutType.LiveSimulation, + section: Section.Experiment, + moreInfo: ( + <> + Kd = [] (at equilibrium when 50% of is + bound to ) + + ), + callToAction: ( + <> + Let’s find Kd - Repeat the experiment by pausing, + adjusting the concentration of and recording the + equilibrium point until you have enough data. + + ), + }, +]; diff --git a/src/content/index.tsx b/src/content/index.tsx index 5fe4d1c..7aecb7a 100644 --- a/src/content/index.tsx +++ b/src/content/index.tsx @@ -1,229 +1,6 @@ -import BindingDiagrams from "../components/BindingDiagrams"; -import StartExperiment from "../components/StartExperiment"; -import { A, AB, B } from "../components/agent-symbols"; -import Definition from "../components/shared/Definition"; -import { LayoutType, Module, PageContent, Section } from "../types"; - -export const highAffinityContentArray: PageContent[] = [ - // making the content array 1 indexed to match the page numbers - { - content: "", - section: Section.Introduction, - layout: LayoutType.FullScreenOverlay, - }, - { - content: - "Congratulations! You’ve just joined a biology lab. Your mentor has asked you to measure the strength of the binding interaction between two types of molecules. These molecules are too small to see, even under a microscope. But what if we could somehow see what the molecules in the tube are doing? What do you think you’d see?", - callToAction: - "Click or tap the animated button to switch your view to a molecular simulation.", - section: Section.Introduction, - layout: LayoutType.LiveSimulation, - }, - { - content: ( - <> - Computer simulations can help us explore phenomena that are - difficult to experience in reality. In this{" "} - , the molecules are - represented simply as circles. - - ), - callToAction: "What happens to the molecules once you press play?", - section: Section.Introduction, - layout: LayoutType.LiveSimulation, - }, - { - content: ( - <> - As the simulation plays, the molecules move by a random walk. - Watch what happens when they bump into each other. What do you - think the "Reaction events over time" graph is showing? - - ), - callToAction: - "After you've observed the simulation, click 'Lab view' to see what the cuvette looks like now.", - section: Section.Introduction, - layout: LayoutType.LiveSimulation, - }, - { - content: ( - <> - The clear liquid is slowly turning yellow as the simulation - progresses. Can you estimate the concentration of ? - - ), - callToAction: "Click 'Molecular view' to switch back.", - section: Section.Introduction, - layout: LayoutType.LiveSimulation, - }, - { - title: "Start the Experiment", - content: ( - <> - Now, let's use this simulation to make measurements. We're going - to increase the timestep so the experiments are fast. - - ), - actionButton: , - callToAction: - "Click the 'Start experiment' button to reset the simulation and begin by pressing play!", - section: Section.Introduction, - layout: LayoutType.LiveSimulation, - }, - { - title: "Start the Experiment", - content: ( - <> - Now, let's use this simulation to make measurements. We're going - to increase the timestep so the experiments are fast. - - ), - actionButton: , - callToAction: - "Click the 'Start experiment' button to reset the simulation and begin by pressing play!", - section: Section.Experiment, - layout: LayoutType.LiveSimulation, - }, - { - content: ( - <> - When the reaction first starts, the concentration of each - molecule changes rapidly as the two molecules, and , - bind to form . Eventually, the reaction reaches{" "} - , at which point the - concentration of stays more or less consistent. - - ), - 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, - }, - { - content: ( - <> - The goal of these experiments is to figure out how tightly two - molecules bind, or stick to each other. We call this{" "} - . - - ), - 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 B and play the simulation again. - - ), - section: Section.Experiment, - layout: LayoutType.LiveSimulation, - }, - { - content: ( - <> - We want to understand the affinity of{" "} - and{" "} - regardless of the - concentration of substrate. Let’s repeat the experiment with a - new concentration of . We will keep the concentration of{" "} - constant to avoid introducing more than one{" "} - at a time. - - ), - 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, - }, - { - content: ( - <> - We use the constant{" "} - - Kd - - } - />{" "} - to compare affinities. Kd can by determined in this - experiment by finding the concentration of at equilibrium - where half the binding sites of are occupied. - - ), - moreInfo: ( - <> - Kd = [] (at equilibrium when 50% of is - bound to ) - - ), - callToAction: ( - <> - Let’s find Kd - 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!", - backButton: true, - nextButton: true, - nextButtonText: "View examples", - section: Section.BonusContent, - layout: LayoutType.FullScreenOverlay, - }, - { - title: "Real-world example: Antibodies and antigens, high affinity binders", - content: ( - <> - The molecules you experimented with had a high affinity - - similar to an and an{" "} - it recognizes. Even if the antigen - is present at low concentrations, the antibody stays tightly - bound for a long time. This increases the total number of - complexes at any given time and alerts the immune system of the - antigen’s presence. - - ), - nextButton: true, - section: Section.BonusContent, - layout: LayoutType.PreComputedSimulation, - trajectoryUrl: - "https://aics-simularium-data.s3.us-east-2.amazonaws.com/trajectory/binding-affinity_antibodies.simularium", - }, - { - title: "Real-world example: Affinity is determined by intermolecular forces", - content: ( - <> - A pair of molecules’ binding affinity is determined by{" "} - . The first Antibody - has a lower binding affinity for the Antigen than the second - Antibody. Why, do you think, is this the case? - - ), - backButton: true, - nextButton: true, - nextButtonText: "Finish", - visualContent: , - section: Section.BonusContent, - layout: LayoutType.NoSidePanels, - }, - { - title: "", - content: <>Next Up: Low affinity binding!, - section: Section.BonusContent, - layout: LayoutType.FullScreenOverlay, - }, -]; +import { Module, PageContent } from "../types"; +import { highAffinityContentArray } from "./HighAffinity"; +import { lowAffinityContentArray } from "./LowAffinity"; export const moduleNames = { [Module.A_B_AB]: "High Affinity", @@ -231,6 +8,10 @@ export const moduleNames = { [Module.A_B_C_AB_AC]: "Competitive Binding", }; +export const FIRST_PAGE = 1; + export default { [Module.A_B_AB]: highAffinityContentArray, + [Module.A_C_AC]: lowAffinityContentArray, + [Module.A_B_C_AB_AC]: [], // Add appropriate PageContent[] for Competitive Binding } as { [key in Module]: PageContent[] }; From 325205e15435855d10ddd49ed7b855992c3c9983 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 25 Feb 2025 20:36:28 -0800 Subject: [PATCH 04/12] change in visibility and progression control --- src/components/LabView.tsx | 3 ++- src/components/PlayButton.tsx | 11 +++++++-- src/components/RecordEquilibriumButton.tsx | 9 ++++++- src/components/ViewSwitch.tsx | 26 +++++++++++--------- src/components/main-layout/LeftPanel.tsx | 19 ++++++++++++-- src/components/shared/ProgressionControl.tsx | 17 +++++++------ src/components/shared/VisibilityControl.tsx | 12 ++++----- 7 files changed, 65 insertions(+), 32 deletions(-) diff --git a/src/components/LabView.tsx b/src/components/LabView.tsx index 115df3b..ba4e29a 100644 --- a/src/components/LabView.tsx +++ b/src/components/LabView.tsx @@ -7,6 +7,7 @@ import styles from "./labview.module.css"; import classNames from "classnames"; import ScaleBar from "./ScaleBar"; import VisibilityControl from "./shared/VisibilityControl"; +import { Module } from "../types"; const LabView: React.FC = () => { const { currentProductionConcentration, maxConcentration, page } = @@ -24,7 +25,7 @@ const LabView: React.FC = () => { { [styles.top]: page === 1 }, ])} > - +
diff --git a/src/components/PlayButton.tsx b/src/components/PlayButton.tsx index 2f46540..58bd298 100644 --- a/src/components/PlayButton.tsx +++ b/src/components/PlayButton.tsx @@ -5,6 +5,7 @@ import { SimulariumContext } from "../simulation/context"; import ProgressionControl from "./shared/ProgressionControl"; import VisibilityControl from "./shared/VisibilityControl"; import { OverlayButton } from "./shared/ButtonLibrary"; +import { Module } from "../types"; const PlayButton: React.FC = () => { const { isPlaying, setIsPlaying } = useContext(SimulariumContext); @@ -16,8 +17,14 @@ const PlayButton: React.FC = () => { const iconStyle = { fontSize: 26 }; return ( - - + + { return ( - + Record ); diff --git a/src/components/ViewSwitch.tsx b/src/components/ViewSwitch.tsx index 3301cb9..82d6c12 100644 --- a/src/components/ViewSwitch.tsx +++ b/src/components/ViewSwitch.tsx @@ -8,8 +8,8 @@ import { OverlayButton } from "./shared/ButtonLibrary"; import LabIcon from "./icons/Lab"; import Molecules from "./icons/Molecules"; import LabView from "./LabView"; -import usePageNumber from "../hooks/usePageNumber"; import VisibilityControl from "./shared/VisibilityControl"; +import { Module } from "../types"; enum View { Lab = "lab", @@ -24,17 +24,19 @@ const ViewSwitch: React.FC = () => { prevView === View.Lab ? View.Simulation : View.Lab ); }; - const { page, isPlaying, setIsPlaying, handleTimeChange } = + const { page, isPlaying, setIsPlaying, handleTimeChange, module } = useContext(SimulariumContext); - usePageNumber( - page, - (page) => page === 1 && currentView === View.Simulation, - () => { - // reset home - setCurrentView(View.Lab); - } - ); + const isFirstPageOfFirstModule = page === 1 && module === 1; + const isFirstPageOfSecondModule = page === 1 && module === 2; + if (currentView === View.Simulation && isFirstPageOfFirstModule) { + // reset home + setCurrentView(View.Lab); + } + if (currentView === View.Lab && isFirstPageOfSecondModule) { + // reset home + setCurrentView(View.Simulation); + } let buttonStyle: React.CSSProperties = { top: 16, @@ -45,7 +47,7 @@ const ViewSwitch: React.FC = () => { "background 0.2s, color 0.2s, border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1)", }; - if (page === 1) { + if (isFirstPageOfFirstModule) { buttonStyle = { ...buttonStyle, right: "50%", @@ -56,7 +58,7 @@ const ViewSwitch: React.FC = () => { return (
- + = ({ unbindingEventsOverTime, adjustableAgent, }) => { + const concentrationExcludedPages = { + [Module.A_B_AB]: [0, 1], + [Module.A_C_AC]: [], + [Module.A_B_C_AB_AC]: [], + }; + + const eventsOverTimeExcludedPages = { + [Module.A_B_AB]: [0, 1, 2], + [Module.A_C_AC]: [], + [Module.A_B_C_AB_AC]: [], + }; return ( <> - + = ({ adjustableAgent={adjustableAgent} /> - + ; interface ProgressionControlProps { children: ProgressionControlChild; - onPage: number | number[]; + onPage: { [key in Module]?: number | number[] }; } /** @@ -23,14 +23,14 @@ const ProgressionControl: React.FC = ({ children, onPage, }) => { - const { page, setPage } = useContext(SimulariumContext); - + const { page, setPage, module } = useContext(SimulariumContext); + const pagesToAdvance = onPage[module]; const progress = () => { - if (Array.isArray(onPage)) { - if (onPage.includes(page)) { + if (Array.isArray(pagesToAdvance)) { + if (pagesToAdvance.includes(page)) { setPage(page + 1); } - } else if (page === onPage) { + } else if (page === pagesToAdvance) { setPage(page + 1); } }; @@ -52,7 +52,8 @@ const ProgressionControl: React.FC = ({ }; const showHighlight = - (Array.isArray(onPage) && onPage[0] === page) || page === onPage; + (Array.isArray(onPage) && onPage[0] === page) || + page === pagesToAdvance; const className = showHighlight ? styles.hintHighlight : ""; diff --git a/src/components/shared/VisibilityControl.tsx b/src/components/shared/VisibilityControl.tsx index 1c015f0..2eaf598 100644 --- a/src/components/shared/VisibilityControl.tsx +++ b/src/components/shared/VisibilityControl.tsx @@ -1,12 +1,12 @@ import React, { useContext } from "react"; import { SimulariumContext } from "../../simulation/context"; -import { Section } from "../../types"; +import { Module, Section } from "../../types"; interface VisibilityControlProps { children: React.ReactNode; startPage?: number; - excludedPages?: number[]; - includedPages?: number[]; + excludedPages?: { [key in Module]?: number[] }; + includedPages?: { [key in Module]?: number[] }; conditionalRender?: boolean; notInBonusMaterial?: boolean; notInIntroduction?: boolean; @@ -21,7 +21,7 @@ const VisibilityControl: React.FC = ({ notInIntroduction, startPage, }) => { - const { page, section } = useContext(SimulariumContext); + const { page, section, module } = useContext(SimulariumContext); if (conditionalRender === false) { return null; } @@ -29,9 +29,9 @@ const VisibilityControl: React.FC = ({ let shouldRender = true; if (includedPages) { - shouldRender = includedPages.includes(page); + shouldRender = includedPages[module]?.includes(page) ?? false; } else if (excludedPages) { - shouldRender = !excludedPages.includes(page); + shouldRender = !excludedPages[module]?.includes(page); } if (startPage && page < startPage) { shouldRender = false; From def84b785e59ee68fa6b010e873a23283938d304 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 25 Feb 2025 21:05:11 -0800 Subject: [PATCH 05/12] changes to data for new simulation --- src/simulation/LiveSimulationData.ts | 30 +++++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/simulation/LiveSimulationData.ts b/src/simulation/LiveSimulationData.ts index 23c9f55..793eae5 100644 --- a/src/simulation/LiveSimulationData.ts +++ b/src/simulation/LiveSimulationData.ts @@ -32,8 +32,8 @@ const agentB: InputAgent = { initialConcentration: 0, radius: 1, partners: [0], - kOn: 0.6, - kOff: 0.5, + kOn: 0.9, + kOff: 0.01, color: AGENT_B_COLOR, }; @@ -41,16 +41,16 @@ const agentC: InputAgent = { id: 2, name: AgentName.C, initialConcentration: 0, - radius: 1, + radius: 0.4, partners: [0], - kOn: 0.5, - kOff: 0.8, + kOn: 0.3, + kOff: 0.9, color: AGENT_C_COLOR, }; const kds = { - [Module.A_B_AB]: 1, - [Module.A_C_AC]: 10, + [Module.A_B_AB]: 0.75, + [Module.A_C_AC]: 74, [Module.A_B_C_AB_AC]: 5, }; @@ -59,8 +59,13 @@ export default class LiveSimulation implements ISimulationData { [AgentName.A]: AgentFunction.Fixed, [AgentName.B]: AgentFunction.Adjustable, [AgentName.C]: AgentFunction.Competitor, - [ProductName.AB]: AgentFunction.Complex, - [ProductName.AC]: AgentFunction.Complex, + [ProductName.AB]: AgentFunction.Complex_1, + [ProductName.AC]: AgentFunction.Complex_2, + }; + static ADJUSTABLE_AGENT_MAP = { + [Module.A_B_AB]: AgentName.B, + [Module.A_C_AC]: AgentName.C, + [Module.A_B_C_AB_AC]: AgentName.B, }; static INITIAL_TIME_FACTOR: number = 30; static DEFAULT_TIME_FACTOR: number = 90; @@ -72,8 +77,9 @@ export default class LiveSimulation implements ISimulationData { static INITIAL_CONCENTRATIONS = { [AgentName.A]: 10, [AgentName.B]: 4, - [AgentName.C]: 10, + [AgentName.C]: 40, }; + PRODUCT = { [Module.A_B_AB]: ProductName.AB, [Module.A_C_AC]: ProductName.AC, @@ -107,10 +113,10 @@ export default class LiveSimulation implements ISimulationData { maxConcentration = 10; break; case Module.A_C_AC: - maxConcentration = 20; //TODO: adjust these as needed + maxConcentration = 100; break; case Module.A_B_C_AB_AC: - maxConcentration = 20; //TODO: adjust these as needed + maxConcentration = 20; break; } return maxConcentration; From 68776c9371bcc37d46aefcfb10d600a8fbde1584 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 25 Feb 2025 21:06:20 -0800 Subject: [PATCH 06/12] new colors and conc --- .../ConcentrationSlider.tsx | 7 +- src/components/main-layout/RightPanel.tsx | 7 +- src/components/plots/EquilibriumPlot.tsx | 32 +++++---- src/components/plots/EventsOverTimePlot.tsx | 66 ++++++++++++++----- src/components/quiz-questions/KdQuestion.tsx | 8 +-- src/simulation/ISimulationData.ts | 6 +- 6 files changed, 82 insertions(+), 44 deletions(-) diff --git a/src/components/concentration-display/ConcentrationSlider.tsx b/src/components/concentration-display/ConcentrationSlider.tsx index c82296d..5527a74 100644 --- a/src/components/concentration-display/ConcentrationSlider.tsx +++ b/src/components/concentration-display/ConcentrationSlider.tsx @@ -62,10 +62,11 @@ const ConcentrationSlider: React.FC = ({ }) => { // eslint-disable-next-line react-hooks/exhaustive-deps const disabledNumbers = [0]; - + const stepSize = useRef(0); const marks = useMemo(() => { + stepSize.current = (max - min) / 5; const marks: SliderSingleProps["marks"] = {}; - for (let index = min; index <= max; index = index + 2) { + for (let index = min; index <= max; index = index + stepSize.current) { marks[index] = { label: ( = ({ name={name} min={min} max={max} - step={2} + step={stepSize.current} onChange={onChange} onChangeComplete={onChangeComplete} marks={marks} diff --git a/src/components/main-layout/RightPanel.tsx b/src/components/main-layout/RightPanel.tsx index 345bcef..b061b55 100644 --- a/src/components/main-layout/RightPanel.tsx +++ b/src/components/main-layout/RightPanel.tsx @@ -7,12 +7,12 @@ import RecordEquilibriumButton from "../RecordEquilibriumButton"; import { ProductOverTimeTrace } from "../plots/types"; import styles from "./layout.module.css"; -import { AB } from "../agent-symbols"; +import { AB, AC } from "../agent-symbols"; import ResizeContainer from "../shared/ResizeContainer"; import { SimulariumContext } from "../../simulation/context"; import HelpPopup from "../HelpPopup"; import InfoText from "../shared/InfoText"; -import { UiElement } from "../../types"; +import { ProductName, UiElement } from "../../types"; interface RightPanelProps { productOverTimeTraces: ProductOverTimeTrace[]; @@ -63,7 +63,8 @@ const RightPanel: React.FC = ({ >

- Concentration over time for {" "} + Concentration over time for{" "} + {productName === ProductName.AB ? : }{" "}

= ({ colors, kd, }) => { - const { maxConcentration } = useContext(SimulariumContext); + const { + fixedAgentStartingConcentration, + productName, + getAgentColor, + adjustableAgentName, + } = useContext(SimulariumContext); const hintOverlay = (
= ({ shape: "spline" as const, width: 1, }, - hovertemplate: - "[B]: %{x:.1f}
[AB]: %{y:.1f}", + hovertemplate: `[${adjustableAgentName}]: %{x:.1f}
[${productName}]: %{y:.1f}`, }, ]; @@ -109,22 +109,26 @@ const EquilibriumPlot: React.FC = ({ xaxis: { ...AXIS_SETTINGS, range: [0, kd * 2], - title: `[B] ${MICRO}M`, + title: `[${adjustableAgentName}] ${MICRO}M`, titlefont: { ...AXIS_SETTINGS.titlefont, - color: AGENT_B_COLOR, + color: getAgentColor(adjustableAgentName), }, }, yaxis: { ...AXIS_SETTINGS, - range: [0, maxConcentration + 0.25], // the line gets cut off without the extra 0.25 - title: `[AB] ${MICRO}M`, + range: [0, fixedAgentStartingConcentration + 0.25], // the line gets cut off without the extra 0.25 + title: `[${productName}] ${MICRO}M`, titlefont: { ...AXIS_SETTINGS.titlefont, - color: AGENT_AB_COLOR, + color: getAgentColor(productName), }, tickmode: "array" as const, - tickvals: [0, maxConcentration / 2, maxConcentration], + tickvals: [ + 0, + fixedAgentStartingConcentration / 2, + fixedAgentStartingConcentration, + ], }, }; return ( diff --git a/src/components/plots/EventsOverTimePlot.tsx b/src/components/plots/EventsOverTimePlot.tsx index 3b2be86..0448032 100644 --- a/src/components/plots/EventsOverTimePlot.tsx +++ b/src/components/plots/EventsOverTimePlot.tsx @@ -9,14 +9,14 @@ import { CONFIG, } from "./constants"; import { SimulariumContext } from "../../simulation/context"; -import { A, B, AB } from "../agent-symbols"; +import { A, B, AB, C, AC } from "../agent-symbols"; import { MICRO } from "../../constants"; import plotStyles from "./plots.module.css"; import layoutStyles from "./events-over-time.module.css"; import ResizeContainer from "../shared/ResizeContainer"; import InfoText from "../shared/InfoText"; -import { UiElement } from "../../types"; +import { Module, UiElement } from "../../types"; interface PlotProps { bindingEventsOverTime: number[]; @@ -27,7 +27,10 @@ const EventsOverTimePlot: React.FC = ({ bindingEventsOverTime, unbindingEventsOverTime, }) => { - const { timeFactor } = useContext(SimulariumContext); + const { timeFactor, module } = useContext(SimulariumContext) as { + timeFactor: number; + module: Module; + }; const [width, setWidth] = useState(0); // the two arrays will always be the same length @@ -84,6 +87,47 @@ const EventsOverTimePlot: React.FC = ({ }, }; + const forwardReaction: { [key in Module]?: JSX.Element } = { + [Module.A_B_AB]: ( + <> +
+ + + + + + + ), + [Module.A_C_AC]: ( + <> + + + + + + + + ), + }; + + const backwardReaction: { [key in Module]?: JSX.Element } = { + [Module.A_B_AB]: ( + <> + + + + + + + + ), + [Module.A_C_AC]: ( + <> + + + + + + + + ), + }; return (

@@ -93,13 +137,7 @@ const EventsOverTimePlot: React.FC = ({
Count of reactions
-
+
{forwardReaction[module]}
= ({ /> -
+
{backwardReaction[module]}
= ({ kd, canAnswer }) => { setFormState(FormState.Clear); return; } - - if ( - selectedAnswer <= correctAnswer + tolerance && - selectedAnswer >= correctAnswer - tolerance - ) { + const closeness = + Math.abs(selectedAnswer - correctAnswer) / correctAnswer; + if (closeness <= tolerance) { setFormState(FormState.Correct); } else { setFormState(FormState.Incorrect); diff --git a/src/simulation/ISimulationData.ts b/src/simulation/ISimulationData.ts index 276da52..b65296f 100644 --- a/src/simulation/ISimulationData.ts +++ b/src/simulation/ISimulationData.ts @@ -1,5 +1,6 @@ import { AGENT_AB_COLOR, + AGENT_AC_COLOR, AGENT_A_COLOR, AGENT_B_COLOR, AGENT_C_COLOR, @@ -17,7 +18,8 @@ export const AGENT_AND_PRODUCT_COLORS = { [AgentFunction.Fixed]: AGENT_A_COLOR, [AgentFunction.Adjustable]: AGENT_B_COLOR, [AgentFunction.Competitor]: AGENT_C_COLOR, - [AgentFunction.Complex]: AGENT_AB_COLOR, + [AgentFunction.Complex_1]: AGENT_AB_COLOR, + [AgentFunction.Complex_2]: AGENT_AC_COLOR, }; export enum TrajectoryType { @@ -30,7 +32,7 @@ interface ISimulationData { getCurrentProduct: (module: Module) => ProductName; getMaxConcentration: (module: Module) => number; getAgentFunction: (name: AgentName | ProductName) => AgentFunction; - getAgentColor: (agentName: AgentName) => string; + getAgentColor: (agentName: AgentName | ProductName) => string; getActiveAgents: (currentModule: Module) => AgentName[]; getInitialConcentrations: ( activeAgents: AgentName[] From d0f0297c6a3f58d12e4b315eca03c51487d1f03d Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 25 Feb 2025 21:06:45 -0800 Subject: [PATCH 07/12] make page update in ui --- src/components/AdminUi.tsx | 1 + src/components/shared/Slider.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/AdminUi.tsx b/src/components/AdminUi.tsx index 704d758..fe775d9 100644 --- a/src/components/AdminUi.tsx +++ b/src/components/AdminUi.tsx @@ -57,6 +57,7 @@ const AdminUI: React.FC = ({ min={0} max={totalPages} step={1} + overRideValue={page} initialValue={page} onChange={(_, value): void => { setPage(value); diff --git a/src/components/shared/Slider.tsx b/src/components/shared/Slider.tsx index f6cf053..62ef3c9 100644 --- a/src/components/shared/Slider.tsx +++ b/src/components/shared/Slider.tsx @@ -13,6 +13,7 @@ interface SliderProps { marks?: SliderSingleProps["marks"]; className?: string; disabledNumbers?: number[]; + overRideValue?: number; } const Slider: React.FC = ({ @@ -27,6 +28,7 @@ const Slider: React.FC = ({ className, disabledNumbers, onChangeComplete, + overRideValue, }) => { const [value, setValue] = useState(initialValue); const handleSliderChange = (newValue: number) => { @@ -55,7 +57,7 @@ const Slider: React.FC = ({ max={max} style={{ width: "100%" }} step={step} - value={value} + value={overRideValue ?? value} onChange={handleSliderChange} onChangeComplete={handleSliderChangeComplete} disabled={disabled} From 4584de75dcd3ec4e7c94bee18c43c33cb46e4536 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 25 Feb 2025 21:06:55 -0800 Subject: [PATCH 08/12] changes to copy --- src/content/LowAffinity.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/content/LowAffinity.tsx b/src/content/LowAffinity.tsx index 082e7d3..aa2a368 100644 --- a/src/content/LowAffinity.tsx +++ b/src/content/LowAffinity.tsx @@ -13,15 +13,14 @@ export const lowAffinityContentArray: PageContent[] = [ <> Molecule has a different binding affinity with molecule{" "}
. Let’s repeat our simulation experiments with and{" "} - - to determine the binding affinity Kd for this pair of - molecules. + to determine the binding affinity Kd for this + pair of molecules. ), layout: LayoutType.LiveSimulation, section: Section.Experiment, callToAction: - "Choose different concentrations of C and repeat the experiment to record the new equilibrium concentrations.", + "Press play and watch the Concentration over time plot until you think the reaction has reached equilibrium. Then, press the “Record” button to record the equilibrium concentration.", }, { content: ( @@ -47,4 +46,13 @@ export const lowAffinityContentArray: PageContent[] = [ ), }, + { + content: + "Congratulations, you’ve completed the High Affinity experiment!", + backButton: true, + // nextButton: true, + nextButtonText: "View examples", + section: Section.BonusContent, + layout: LayoutType.FullScreenOverlay, + }, ]; From c183cd900ad9680d842172e9e9d7cbf0154512a8 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 25 Feb 2025 21:07:09 -0800 Subject: [PATCH 09/12] fix typo --- src/components/agent-symbols/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/agent-symbols/index.tsx b/src/components/agent-symbols/index.tsx index 198b7ba..8b06349 100644 --- a/src/components/agent-symbols/index.tsx +++ b/src/components/agent-symbols/index.tsx @@ -1,6 +1,7 @@ import React from "react"; import { AGENT_AB_COLOR, + AGENT_AC_COLOR, AGENT_A_COLOR, AGENT_B_COLOR, AGENT_C_COLOR, @@ -23,5 +24,5 @@ export const AB: React.FC<{ name?: string }> = (props) => { }; export const AC: React.FC = () => { - return AB; + return AC; }; From 775276fab49c601e6d1daa4a143a636d9e4b8314 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 25 Feb 2025 21:07:22 -0800 Subject: [PATCH 10/12] go to next module --- src/components/shared/NextButton.tsx | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/components/shared/NextButton.tsx b/src/components/shared/NextButton.tsx index 7b52759..c7dbdc2 100644 --- a/src/components/shared/NextButton.tsx +++ b/src/components/shared/NextButton.tsx @@ -1,14 +1,33 @@ import { useContext } from "react"; import { SimulariumContext } from "../../simulation/context"; import { PrimaryButton } from "./ButtonLibrary"; +import { FIRST_PAGE } from "../../content"; interface NextButtonProps { text?: string; } const NextButton = ({ text }: NextButtonProps) => { - const { page, setPage } = useContext(SimulariumContext); - + const { + page, + setPage, + moduleLength, + module, + setCurrentModule, + setIsPlaying, + } = useContext(SimulariumContext); + if (page + 1 >= moduleLength) { + const goToNextModule = () => { + setPage(FIRST_PAGE); + setCurrentModule(module + 1); + setIsPlaying(false); + }; + return ( + + {text || "Finish"} + + ); + } return ( setPage(page + 1)}> {text || "Next"} From 51f715f56a0b5bbde8b5053e4ddc13d92121ec14 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 25 Feb 2025 21:07:32 -0800 Subject: [PATCH 11/12] functional changes --- src/App.tsx | 120 ++++++++++++------ .../plots/ProductConcentrationPlot.tsx | 21 +-- src/simulation/BindingSimulator2D.ts | 94 +++++++++++--- src/simulation/context.tsx | 14 +- 4 files changed, 183 insertions(+), 66 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 262f95f..645cdb3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import { AgentName, CurrentConcentration, InputConcentration, + LayoutType, Module, ProductName, ScatterTrace, @@ -22,7 +23,7 @@ import LeftPanel from "./components/main-layout/LeftPanel"; import RightPanel from "./components/main-layout/RightPanel"; import ReactionDisplay from "./components/main-layout/ReactionDisplay"; import ContentPanelTimer from "./components/main-layout/ContentPanelTimer"; -import content, { moduleNames } from "./content"; +import content, { FIRST_PAGE, moduleNames } from "./content"; import { PROMPT_TO_ADJUST_B, DEFAULT_VIEWPORT_SIZE, @@ -48,10 +49,8 @@ import LiveSimulationData from "./simulation/LiveSimulationData"; import { PLOT_COLORS } from "./components/plots/constants"; import useModule from "./hooks/useModule"; -const ADJUSTABLE_AGENT = AgentName.B; - function App() { - const [page, setPage] = useState(1); + const [page, setPage] = useState(FIRST_PAGE); const [time, setTime] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [trajectoryStatus, setTrajectoryStatus] = useState( @@ -86,12 +85,18 @@ function App() { const [timeFactor, setTimeFactor] = useState( LiveSimulationData.INITIAL_TIME_FACTOR ); - const [viewportSize, setViewportSize] = useState(DEFAULT_VIEWPORT_SIZE); + const [viewportWidth, setViewportWidth] = useState( + DEFAULT_VIEWPORT_SIZE.width + ); + const [viewportHeight, setViewportHeight] = useState( + DEFAULT_VIEWPORT_SIZE.height + ); + /** * Analysis state * used to create plots and feedback */ - const [trajectoryPlotData, setTrajectoryPlotData] = + const [preComputedTrajectoryPlotData, setPreComputedTrajectoryPlotData] = useState(); const [liveConcentration, setLiveConcentration] = useState({ @@ -161,21 +166,22 @@ function App() { if (!trajectory) { return null; } - const longestAxis = Math.max(viewportSize.width, viewportSize.height); + const longestAxis = Math.max(viewportWidth, viewportHeight); return new BindingSimulator(trajectory, longestAxis / 3); - }, [currentModule, viewportSize, simulationData]); + }, [viewportWidth, viewportHeight, simulationData, currentModule]); const preComputedPlotDataManager = useMemo(() => { - if (!trajectoryPlotData) { + if (!preComputedTrajectoryPlotData) { return null; } - return new PreComputedPlotData(trajectoryPlotData); - }, [trajectoryPlotData]); + return new PreComputedPlotData(preComputedTrajectoryPlotData); + }, [preComputedTrajectoryPlotData]); useEffect(() => { if (!clientSimulator) { return; } + simulariumController.changeFile( { clientSimulator: clientSimulator, @@ -244,6 +250,7 @@ function App() { uniqMeasuredConcentrations.length >= 3 ); }, [hasAValueAboveKd, hasAValueBelowKd, uniqMeasuredConcentrations]); + const handleNewInputConcentration = useCallback( (name: string, value: number) => { if (value === 0) { @@ -261,6 +268,7 @@ function App() { name as keyof typeof LiveSimulationData.AVAILABLE_AGENTS; const agentId = LiveSimulationData.AVAILABLE_AGENTS[agentName].id; clientSimulator.changeConcentration(agentId, value); + simulariumController.gotoTime(time + 1); resetCurrentRunAnalysisState(); }, @@ -282,13 +290,18 @@ function App() { LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.B], }); handleNewInputConcentration( - ADJUSTABLE_AGENT, + LiveSimulationData.ADJUSTABLE_AGENT_MAP[currentModule], LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.B] ); setIsPlaying(false); clearAllAnalysisState(); setTimeFactor(LiveSimulationData.INITIAL_TIME_FACTOR); - }, [clearAllAnalysisState, handleNewInputConcentration, productName]); + }, [ + clearAllAnalysisState, + handleNewInputConcentration, + productName, + currentModule, + ]); // Special events in page navigation // usePageNumber takes a page number, a conditional and a callback @@ -299,7 +312,10 @@ function App() { // clicked the home button usePageNumber( page, - (page) => page === 1 && currentProductConcentrationArray.length > 1, + (page) => + page === 1 && + currentModule === 1 && + currentProductConcentrationArray.length > 1, () => { totalReset(); } @@ -313,17 +329,35 @@ function App() { isPlaying && recordedInputConcentration.length > 0 && recordedInputConcentration[0] !== - inputConcentration[ADJUSTABLE_AGENT], + inputConcentration[ + LiveSimulationData.ADJUSTABLE_AGENT_MAP[currentModule] + ], () => { setPage(page + 1); } ); + // handle trajectory changes based on content changes useEffect(() => { + const currentPage = content[currentModule][page]; const nextPage = content[currentModule][page + 1]; + if (!nextPage?.trajectoryUrl && !currentPage.trajectoryUrl) { + if (trajectoryStatus === TrajectoryStatus.LOADED) { + simulariumController.clearFile(); + setTrajectoryStatus(TrajectoryStatus.INITIAL); + } + if ( + currentPage.layout === LayoutType.LiveSimulation && + simulariumController.getFile() !== LIVE_SIMULATION_NAME + ) { + setTrajectoryName(LIVE_SIMULATION_NAME); + } + } + if (!nextPage) { return; } + // if the next page has a trajectory, load it const url = nextPage.trajectoryUrl; if (trajectoryStatus === TrajectoryStatus.INITIAL && url) { const changeTrajectory = async () => { @@ -334,7 +368,7 @@ function App() { await fetch3DTrajectory( url, simulariumController, - setTrajectoryPlotData + setPreComputedTrajectoryPlotData ); setTrajectoryStatus(TrajectoryStatus.LOADED); }; @@ -349,15 +383,14 @@ function App() { totalReset, ]); - usePageNumber( - page, - (page) => page === finalPageNumber, - () => { - if (simulariumController.getFile()) { - simulariumController.clearFile(); - } + useEffect(() => { + const { section } = content[currentModule][page]; + if (section === Section.Experiment) { + setTimeFactor(LiveSimulationData.DEFAULT_TIME_FACTOR); + } else if (section === Section.Introduction) { + setTimeFactor(LiveSimulationData.INITIAL_TIME_FACTOR); } - ); + }, [currentModule, page]); const addProductionTrace = (previousConcentration: number) => { const traces = productOverTimeTraces; @@ -376,7 +409,6 @@ function App() { const handleStartExperiment = () => { simulariumController.pause(); totalReset(); - setTimeFactor(LiveSimulationData.DEFAULT_TIME_FACTOR); setPage(page + 1); }; @@ -386,12 +418,7 @@ function App() { // 2d trajectory // switch to orthographic camera simulariumController.setCameraType(true); - const { section } = content[currentModule][page]; - if (section === Section.Experiment) { - setTimeFactor(LiveSimulationData.DEFAULT_TIME_FACTOR); - } else { - setTimeFactor(LiveSimulationData.INITIAL_TIME_FACTOR); - } + setPreComputedTrajectoryPlotData(undefined); setFinalTime(-1); } else { // 3d trajectory @@ -414,7 +441,6 @@ function App() { } let concentrations: CurrentConcentration = {}; let previousData = currentProductConcentrationArray; - if (preComputedPlotDataManager) { if (timeData.time === 0) { // for the 3D trajectory, @@ -482,16 +508,19 @@ function App() { setEquilibriumFeedbackTimeout("Not yet!"); return false; } + const adjustableAgentName = + LiveSimulationData.ADJUSTABLE_AGENT_MAP[currentModule]; // this will always be defined for the current run, but since there are // different agents in each module, typescript fears it will be undefined - const currentInputConcentration = inputConcentration[ADJUSTABLE_AGENT]; + const currentInputConcentration = + inputConcentration[adjustableAgentName]; if (currentInputConcentration === undefined) { return false; } const concentrations = clientSimulator.getCurrentConcentrations(productName); const productConcentration = concentrations[productName]; - const reactantConcentration = concentrations[ADJUSTABLE_AGENT]; + const reactantConcentration = concentrations[adjustableAgentName]; const currentTime = indexToTime( currentProductConcentrationArray.length, @@ -538,7 +567,8 @@ function App() { const { totalMainContentPages } = useModule(currentModule); const lastPageOfExperiment = page === totalMainContentPages; - + const adjustableAgentName = + LiveSimulationData.ADJUSTABLE_AGENT_MAP[currentModule]; return ( <>
@@ -546,12 +576,17 @@ function App() { value={{ trajectoryName, productName, + adjustableAgentName, + fixedAgentStartingConcentration: + inputConcentration[AgentName.A] || 0, currentProductionConcentration: liveConcentration[productName] || 0, maxConcentration: simulationData.getMaxConcentration(currentModule), handleStartExperiment, section: content[currentModule][page].section, + moduleLength: content[currentModule].length, + module: currentModule, getAgentColor: simulationData.getAgentColor, isPlaying, setIsPlaying, @@ -559,11 +594,18 @@ function App() { handleTimeChange, page, setPage, + setCurrentModule, timeFactor, timeUnit: simulationData.timeUnit, handleTrajectoryChange, - viewportSize, - setViewportSize, + viewportSize: { + width: viewportWidth, + height: viewportHeight, + }, + setViewportSize: ({ width, height }) => { + setViewportWidth(width); + setViewportHeight(height); + }, recordedConcentrations: recordedInputConcentration, }} > @@ -611,7 +653,7 @@ function App() { unbindingEventsOverTime={ unBindingEventsOverTime } - adjustableAgent={ADJUSTABLE_AGENT} + adjustableAgent={adjustableAgentName} /> } centerPanel={ @@ -636,7 +678,7 @@ function App() { handleRecordEquilibrium } currentAdjustableAgentConcentration={ - inputConcentration[ADJUSTABLE_AGENT] || 0 + inputConcentration[adjustableAgentName] || 0 } equilibriumData={{ inputConcentrations: diff --git a/src/components/plots/ProductConcentrationPlot.tsx b/src/components/plots/ProductConcentrationPlot.tsx index 938a1b4..7484d0f 100644 --- a/src/components/plots/ProductConcentrationPlot.tsx +++ b/src/components/plots/ProductConcentrationPlot.tsx @@ -12,11 +12,10 @@ import { } from "./constants"; import { ProductOverTimeTrace } from "./types"; import { SimulariumContext } from "../../simulation/context"; -import { AGENT_AB_COLOR } from "../../constants/colors"; import { MICRO } from "../../constants"; +import { getColorIndex, indexToTime } from "../../utils"; import plotStyles from "./plots.module.css"; -import { getColorIndex, indexToTime } from "../../utils"; interface ProductConcentrationPlotProps { data: ProductOverTimeTrace[]; @@ -35,8 +34,14 @@ const ProductConcentrationPlot: React.FC = ({ dotsX = [], dotsY = [], }) => { - const { timeFactor, productName, timeUnit, maxConcentration } = - useContext(SimulariumContext); + const { + timeFactor, + timeUnit, + maxConcentration, + productName, + adjustableAgentName, + getAgentColor, + } = useContext(SimulariumContext); const hasData = useRef(false); if (data.length === 0) { hasData.current = false; @@ -81,8 +86,8 @@ const ProductConcentrationPlot: React.FC = ({ }, hovertemplate: "time: %{x:.1f}
" + - "[AB]: %{y:.1f}
" + - "Initial [B]:
" + + `[${productName}]: %{y:.1f}
` + + `Initial [${adjustableAgentName}]:
` + `${inputConcentration.toString()} ` + "", }; @@ -100,7 +105,7 @@ const ProductConcentrationPlot: React.FC = ({ height: Math.max(130, height), legend: { title: { - text: "Initial [B]", + text: `Initial [${adjustableAgentName}]`, side: "top right" as const, font: { family: "sans-serif", @@ -127,7 +132,7 @@ const ProductConcentrationPlot: React.FC = ({ rangemode: "tozero" as const, titlefont: { ...AXIS_SETTINGS.titlefont, - color: AGENT_AB_COLOR, + color: getAgentColor(productName), }, }, shapes: [], diff --git a/src/simulation/BindingSimulator2D.ts b/src/simulation/BindingSimulator2D.ts index 139b003..ed2ce8f 100644 --- a/src/simulation/BindingSimulator2D.ts +++ b/src/simulation/BindingSimulator2D.ts @@ -1,5 +1,5 @@ import { System, Circle, Response } from "detect-collisions"; -import { find, random } from "lodash"; +import { find, isEqual, random } from "lodash"; import { Vector } from "sat"; import { @@ -13,7 +13,7 @@ import { VisTypes, } from "@aics/simularium-viewer"; import { InputAgent, ProductName, StoredAgent } from "../types"; -import { AGENT_AB_COLOR } from "../constants/colors"; +import { AGENT_AB_COLOR, AGENT_AC_COLOR } from "../constants/colors"; import LiveSimulationData from "./LiveSimulationData"; import { LIVE_SIMULATION_NAME } from "../constants"; @@ -157,7 +157,12 @@ class BindingInstance extends Circle { } this.setPosition(this.pos.x + xStep, this.pos.y + yStep); if (this.child) { - this.rotateGroup(xStep, yStep); + const unbind = this.checkWillUnbind(this.child); + if (!unbind) { + this.rotateGroup(xStep, yStep); + } else { + return true; + } } } @@ -172,6 +177,7 @@ class BindingInstance extends Circle { this.child = null; this.isTrigger = false; ligand.releaseFromParent(); + // QUESTION: should the ligand be moved to a random position? return true; } @@ -213,6 +219,10 @@ export default class BindingSimulator implements IClientSimulatorImpl { numberAgentOnRight: number = 0; _isMixed: boolean = false; size: number; + numberLowSlopes: number = 0; + productColor: string = AGENT_AB_COLOR; + productName: ProductName = ProductName.AB; + prevProductConcentration: number = 0; constructor( agents: InputAgent[], size: number, @@ -225,6 +235,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { this.timeFactor = timeFactor; this.agents = this.initializeAgents(agents); this.currentFrame = 0; + this.numberLowSlopes = 0; this.system.separate(); } @@ -274,6 +285,9 @@ export default class BindingSimulator implements IClientSimulatorImpl { this.instances.push(instance); } } + this.productName = this.getProductName(agents); + this.productColor = this.getProductColor(agents); + this.numberLowSlopes = 0; return agents as StoredAgent[]; } @@ -284,22 +298,32 @@ export default class BindingSimulator implements IClientSimulatorImpl { if (agentInstance.id !== this.mixCheckAgent) { return; } - // checking the rightmost third of the simulation // and the left most third of the simulation - if (agentInstance.pos.x < -this.size / 3) { + if (agentInstance.pos.x < 0) { this.numberAgentOnLeft++; - } else if (agentInstance.pos.x > this.size / 3) { + } else if (agentInstance.pos.x > 0) { this.numberAgentOnRight++; } } - private compareAgentsOnEachSide() { + private checkChangeInTotalProduct() { // once the simulation is mixed, if it dips momentarily // that's not a sign that equilibrium has been reversed if (this._isMixed) { return; } + const lastValue = this.prevProductConcentration; + const newValue = this.getCurrentConcentrations(this.productName)[ + this.productName + ]; + const slope = newValue - lastValue; + this.prevProductConcentration = newValue; + if (Math.abs(slope) < 0.1) { + this.numberLowSlopes++; + } else if (Math.abs(slope) > 0.3) { + this.numberLowSlopes = 0; + } // if either of the agents is a limiting reactant // then the system is mixed when it's been used up @@ -316,11 +340,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { this._isMixed = true; return; } - - const diff = Math.abs(this.numberAgentOnLeft - this.numberAgentOnRight); - const total = this.numberAgentOnLeft + this.numberAgentOnRight; - const percentUnmixed = (diff / total) * 100; - if (percentUnmixed < 10) { + if (this.numberLowSlopes >= 4) { this._isMixed = true; } } @@ -460,6 +480,28 @@ export default class BindingSimulator implements IClientSimulatorImpl { } } + private getProductName(agents: InputAgent[]) { + const agentNames = agents.map((agent) => agent.name); + if (isEqual(agentNames, ["A", "B"])) { + return ProductName.AB; + } + if (isEqual(agentNames, ["A", "C"])) { + return ProductName.AC; + } + return ProductName.AB; + } + + private getProductColor(agents: InputAgent[]) { + const agentNames = agents.map((agent) => agent.name); + if (isEqual(agentNames, ["A", "B"])) { + return AGENT_AB_COLOR; + } + if (isEqual(agentNames, ["A", "C"])) { + return AGENT_AC_COLOR; + } + return AGENT_AB_COLOR; + } + /** PUBLIC METHODS BELOW */ public isMixed() { @@ -478,6 +520,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { if (!agent) { return; } + this.numberLowSlopes = 0; const newCount = this.convertConcentrationToCount(newConcentration); const oldCount = agent.count || 0; agent.count = newCount; @@ -601,17 +644,28 @@ export default class BindingSimulator implements IClientSimulatorImpl { if (this.static || this.initialState) { return this.staticUpdate(); } + this.clearMixCounts(); for (let i = 0; i < this.instances.length; ++i) { - this.instances[i].oneStep(this.size, this.timeFactor); + const unbindingOccurred = this.instances[i].oneStep( + this.size, + this.timeFactor + ); + if (unbindingOccurred) { + this.currentNumberOfUnbindingEvents++; + this.currentNumberBound--; + } this.countNumberOfInstancesOnEachSide(this.instances[i]); } // reset to zero for every tenth time point if (this.currentFrame % 10 === 0) { this.currentNumberOfBindingEvents = 0; this.currentNumberOfUnbindingEvents = 0; - this.compareAgentsOnEachSide(); + } + // check if the system is mixed every 50 time points + if (this.currentFrame % 50 === 0) { + this.checkChangeInTotalProduct(); } this.system.checkAll((response: Response) => { const { a, b, overlapV } = response; @@ -620,13 +674,19 @@ export default class BindingSimulator implements IClientSimulatorImpl { // if they are bound, check if they should unbind if (a.isBoundPair(b)) { let unbound = false; + let ligand = null; + let target = null; if (a.r < b.r) { - unbound = b.checkWillUnbind(a); + target = b; + ligand = a; } else { // b is the ligand - unbound = a.checkWillUnbind(b); + ligand = b; + target = a; } + unbound = target.checkWillUnbind(ligand); if (unbound) { + // ligand.move(-overlapV.x, -overlapV.y); this.currentNumberOfUnbindingEvents++; this.currentNumberBound--; } @@ -718,7 +778,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { typeMapping[this.agents[i].id + 100] = { name: `${this.agents[i].name}#bound`, geometry: { - color: AGENT_AB_COLOR, + color: this.productColor, displayType: GeometryDisplayType.SPHERE, url: "", }, diff --git a/src/simulation/context.tsx b/src/simulation/context.tsx index 7060ba5..b682246 100644 --- a/src/simulation/context.tsx +++ b/src/simulation/context.tsx @@ -9,13 +9,15 @@ import { LIVE_SIMULATION_NAME, NANO, } from "../constants"; -import { AgentName, ProductName, Section } from "../types"; +import { AgentName, Module, ProductName, Section } from "../types"; interface SimulariumContextType { trajectoryName: string; productName: ProductName; + adjustableAgentName: AgentName; + fixedAgentStartingConcentration: number; maxConcentration: number; - getAgentColor: (agentName: AgentName) => string; + getAgentColor: (agentName: AgentName | ProductName) => string; currentProductionConcentration: number; isPlaying: boolean; setIsPlaying: (value: boolean) => void; @@ -24,6 +26,9 @@ interface SimulariumContextType { handleStartExperiment: () => void; section: Section; setPage: (value: number) => void; + module: Module; + setCurrentModule: (value: Module) => void; + moduleLength: number; page: number; timeFactor: number; timeUnit: string; @@ -35,7 +40,9 @@ interface SimulariumContextType { export const SimulariumContext = createContext({ trajectoryName: LIVE_SIMULATION_NAME, + adjustableAgentName: AgentName.B, productName: ProductName.AB, + fixedAgentStartingConcentration: 0, maxConcentration: 10, getAgentColor: () => "", currentProductionConcentration: 0, @@ -47,6 +54,9 @@ export const SimulariumContext = createContext({ handleStartExperiment: () => {}, setPage: () => {}, page: 0, + setCurrentModule: () => {}, + module: Module.A_B_AB, + moduleLength: 0, timeFactor: 30, timeUnit: NANO, handleTrajectoryChange: () => {}, From 7d901d913eb4463ae7928e662c6a1d3457e2e5bd Mon Sep 17 00:00:00 2001 From: meganrm Date: Wed, 26 Feb 2025 13:15:43 -0800 Subject: [PATCH 12/12] fix update errror --- src/App.tsx | 2 ++ src/simulation/BindingSimulator2D.ts | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 645cdb3..3e83978 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -289,6 +289,7 @@ function App() { [AgentName.B]: LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.B], }); + // advances time handleNewInputConcentration( LiveSimulationData.ADJUSTABLE_AGENT_MAP[currentModule], LiveSimulationData.INITIAL_CONCENTRATIONS[AgentName.B] @@ -434,6 +435,7 @@ function App() { const handleTimeChange = (timeData: TimeData) => { const { time } = timeData; setTime(time); + console.log("new time", time); // can't use isLastFrame here because the time is not updated // in state yet if (finalTime > 0 && time >= finalTime - timeFactor && isPlaying) { diff --git a/src/simulation/BindingSimulator2D.ts b/src/simulation/BindingSimulator2D.ts index ed2ce8f..2b74f40 100644 --- a/src/simulation/BindingSimulator2D.ts +++ b/src/simulation/BindingSimulator2D.ts @@ -640,7 +640,10 @@ export default class BindingSimulator implements IClientSimulatorImpl { return frameData; } - public update(): VisDataMessage { + public update(frame?: number): VisDataMessage { + if (frame) { + this.currentFrame = frame; + } if (this.static || this.initialState) { return this.staticUpdate(); }