diff --git a/src/App.tsx b/src/App.tsx index 72387c9..cfc5eb7 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); @@ -85,9 +87,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 +114,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 +192,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 +220,6 @@ function App() { resetCurrentRunAnalysisState, viewportSize.width, viewportSize.height, - productName, sectionType, ]); @@ -304,6 +318,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 +379,28 @@ function App() { ] ); const totalReset = useCallback(() => { + setCurrentView(ViewType.Lab); + const activeAgents = [AgentName.A, AgentName.B]; + setCurrentModule(Module.A_B_AB); + const concentrations = simulationData.getInitialConcentrations( + activeAgents, + Module.A_B_AB + ); 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] ?? + LiveSimulationData.INITIAL_CONCENTRATIONS[Module.A_B_AB][ + AgentName.B + ] ); setIsPlaying(false); clearAllAnalysisState(); @@ -376,6 +410,7 @@ function App() { handleNewInputConcentration, productName, adjustableAgentName, + simulationData, ]); // Special events in page navigation // usePageNumber takes a page number, a conditional and a callback @@ -491,11 +526,17 @@ 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 = () => { - simulariumController.pause(); - totalReset(); + clearAllAnalysisState(); + setExperiment(); setPage(page + 1); }; @@ -588,6 +629,12 @@ function App() { }, 3000); }; + const handleSwitchView = () => { + setCurrentView((prevView) => + prevView === ViewType.Lab ? ViewType.Simulation : ViewType.Lab + ); + }; + const handleRecordEquilibrium = () => { if (!clientSimulator) { return false; @@ -687,11 +734,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 b68ddef..21d03c0 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} = ({ marks[index] = { label: ( 0 + ? Number(index.toFixed(1)) + : index + } disabledNumbers={disabledNumbers} onMouseUp={() => onChangeComplete?.(name, index)} /> diff --git a/src/components/concentration-display/LiveConcentrationDisplay.tsx b/src/components/concentration-display/LiveConcentrationDisplay.tsx index 235c5b0..9734a1a 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 = maxConcentration; const size = width / steps - 2; return (
diff --git a/src/components/main-layout/LeftPanel.tsx b/src/components/main-layout/LeftPanel.tsx index a2310af..851f368 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]: [], }; return ( <> @@ -53,6 +52,7 @@ const LeftPanel: React.FC = ({ + 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, + 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: ( + <> + 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: START_EXPERIMENT_ID, + callToAction: ( + <> + Click the Start experiment button to reset the + simulation and begin by pressing play! + + ), + }, + { + title: "Start the experiment", + 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 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! + + ), }, + { + title: "Find maximum complex formation", + content: ( + <> + We are starting with [] = 0, so the complex is able + to form without any inhibition. This will be our baseline for + the maximum 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: ( + <> + IC50 = [] (at equilibrium when [] is half + of max) + + ), + callToAction: ( + <> + Let’s find IC50 - 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!", + "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 f6726c8..665e9ac 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", diff --git a/src/simulation/BindingInstance.ts b/src/simulation/BindingInstance.ts index 8435f1d..5b886d5 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 7f85d79..6e488fc 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,62 @@ 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 if (agent2.id > agent1.id) { + return `${agent2.id}#${agent1.id}`; + } else { + // currently no self-binding allowed + throw new Error("Agents cannot bind to themselves"); + } + } + + 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 + 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() { return [ @@ -72,6 +124,19 @@ 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; + } + private initializeAgents( agents: InputAgent[], initPositions: InitialCondition = InitialCondition.SORTED @@ -86,6 +151,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) { @@ -93,10 +163,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), @@ -162,17 +229,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 dFromSide = random(buffer, 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]; @@ -238,10 +300,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( @@ -290,13 +349,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; } @@ -304,13 +366,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 @@ -325,13 +389,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 +430,26 @@ 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 previousValueA = this.currentComplexMap.get(a.id.toString()) || 0; + const nextValueA = previousValueA + amount; + this.currentComplexMap.set(a.id.toString(), nextValueA); + + const previousValueB = this.currentComplexMap.get(b.id.toString()) || 0; + const nextValueB = previousValueB + amount; + this.currentComplexMap.set(b.id.toString(), nextValueB); + } + private resolveBindingReactions() { this.system.checkAll((response: Response) => { const { a, b, overlapV } = response; @@ -382,9 +467,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) { @@ -396,6 +482,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { if (bound) { this.currentNumberOfBindingEvents++; this.currentNumberBound++; + this.incrementBoundCounts(a, b, 1); } } } else { @@ -454,6 +541,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 +551,25 @@ 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; + } + const color = this.getProductColor(id, partnerId); + typeMapping[complexId] = { + name: `${this.agents[i].name}#${partner.name}`, + geometry: { + color: color, + 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 b65296f..dc6a269 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 a3dd36a..2bddcb1 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 89742c3..3b5e6e7 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/simulation/context.tsx b/src/simulation/context.tsx index 3857639..82cb55d 100644 --- a/src/simulation/context.tsx +++ b/src/simulation/context.tsx @@ -10,7 +10,7 @@ import { NANO, ProgressionElement, } from "../constants"; -import { AgentName, Module, ProductName, Section } from "../types"; +import { AgentName, Module, ProductName, Section, ViewType } from "../types"; interface SimulariumContextType { adjustableAgentName: AgentName; @@ -34,11 +34,13 @@ interface SimulariumContextType { setModule: (value: Module) => 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 26b7d37..991c312 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 { @@ -148,3 +153,8 @@ export enum TrajectoryStatus { LOADED, ERROR, } + +export enum ViewType { + Lab = "lab", + Simulation = "simulation", +}