diff --git a/src/App.tsx b/src/App.tsx index 443fb32..0417a8d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -275,6 +275,7 @@ function App() { uniqMeasuredConcentrations.length >= 3 ); }, [hasAValueAboveKd, hasAValueBelowKd, uniqMeasuredConcentrations]); + const handleNewInputConcentration = useCallback( (name: string, value: number) => { if (value === 0) { @@ -292,10 +293,10 @@ function App() { name as keyof typeof LiveSimulationData.AVAILABLE_AGENTS; const agentId = LiveSimulationData.AVAILABLE_AGENTS[agentName].id; clientSimulator.changeConcentration(agentId, value); - simulariumController.gotoTime(time + 1); + simulariumController.gotoTime(1); // the number isn't used, but it triggers the update resetCurrentRunAnalysisState(); }, - [clientSimulator, time, simulariumController] + [clientSimulator, simulariumController] ); const totalReset = useCallback(() => { setLiveConcentration({ @@ -348,6 +349,7 @@ function App() { usePageNumber( page, (page) => + currentModule === Module.A_B_AB && page === PROMPT_TO_ADJUST_B && isPlaying && recordedInputConcentration.length > 0 && @@ -415,14 +417,14 @@ function App() { switchToLiveSimulation, ]); + const { section } = content[currentModule][page]; 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]); + }, [section]); const addProductionTrace = (previousConcentration: number) => { const traces = productOverTimeTraces; @@ -447,7 +449,6 @@ function App() { const handleStartExperiment = () => { simulariumController.pause(); totalReset(); - setTimeFactor(LiveSimulationData.DEFAULT_TIME_FACTOR); setPage(page + 1); }; diff --git a/src/components/AdminUi.tsx b/src/components/AdminUi.tsx index a94c959..66e287e 100644 --- a/src/components/AdminUi.tsx +++ b/src/components/AdminUi.tsx @@ -3,8 +3,9 @@ import React, { useContext, useEffect } from "react"; import Slider from "./shared/Slider"; import { BG_DARK, LIGHT_GREY } from "../constants/colors"; import { SimulariumContext } from "../simulation/context"; -import { SliderSingleProps } from "antd"; +import { InputNumber, SliderSingleProps } from "antd"; import { zStacking } from "../constants/z-stacking"; +import { Module } from "../types"; interface AdminUIProps { totalPages: number; @@ -72,18 +73,15 @@ const AdminUI: React.FC = ({ name="time factor (ns)" />

Module number

- { - setModule(value); + value={Number(module)} + onChange={(value): void => { + setModule(value as Module); }} - overrideValue={module} - marks={moduleMarks} disabled={false} - name="time factor (ns)" />
@@ -94,6 +92,7 @@ const AdminUI: React.FC = ({ max={100} step={1} initialValue={timeFactor} + overrideValue={timeFactor} onChange={(_, value) => { setTimeFactor(value); }} diff --git a/src/components/ScaleBar.tsx b/src/components/ScaleBar.tsx index 9669e18..7300644 100644 --- a/src/components/ScaleBar.tsx +++ b/src/components/ScaleBar.tsx @@ -11,7 +11,8 @@ interface ScaleBarProps { const ScaleBar: React.FC = ({ productColor }) => { const { maxConcentration } = useContext(SimulariumContext); const labelArray = []; - for (let i = maxConcentration; i >= 0; i = i - 2) { + const interval = maxConcentration / 5; + for (let i = maxConcentration; i >= 0; i = i - interval) { labelArray.push(i); } return ( diff --git a/src/components/concentration-display/ConcentrationSlider.tsx b/src/components/concentration-display/ConcentrationSlider.tsx index c82296d..f978d23 100644 --- a/src/components/concentration-display/ConcentrationSlider.tsx +++ b/src/components/concentration-display/ConcentrationSlider.tsx @@ -62,24 +62,22 @@ const ConcentrationSlider: React.FC = ({ }) => { // eslint-disable-next-line react-hooks/exhaustive-deps const disabledNumbers = [0]; - + const stepSize = useMemo(() => (max - min) / 5, [min, max]); const marks = useMemo(() => { const marks: SliderSingleProps["marks"] = {}; - for (let index = min; index <= max; index = index + 2) { + for (let index = min; index <= max; index = index + stepSize) { marks[index] = { label: ( - onChangeComplete && onChangeComplete(name, index) - } + onMouseUp={() => onChangeComplete?.(name, index)} /> ), }; } return marks; - }, [min, max, disabledNumbers, onChangeComplete, name]); + }, [min, max, disabledNumbers, onChangeComplete, name, stepSize]); return ( = ({ name={name} min={min} max={max} - step={2} + step={stepSize} onChange={onChange} onChangeComplete={onChangeComplete} marks={marks} diff --git a/src/components/plots/EquilibriumPlot.tsx b/src/components/plots/EquilibriumPlot.tsx index fab1427..6357227 100644 --- a/src/components/plots/EquilibriumPlot.tsx +++ b/src/components/plots/EquilibriumPlot.tsx @@ -37,7 +37,8 @@ const EquilibriumPlot: React.FC = ({ getAgentColor, adjustableAgentName, } = useContext(SimulariumContext); - + const xMax = Math.max(...x); + const xAxisMax = Math.max(kd * 2, xMax * 1.1); const hintOverlay = (
= ({ }; const horizontalLine = { - x: [0, kd * 2], + x: [0, xAxisMax], y: [5, 5], mode: "lines", name: "50% bound", @@ -74,7 +75,7 @@ const EquilibriumPlot: React.FC = ({ line: lineOptions, }; const horizontalLineMax = { - x: [0, kd * 2], + x: [0, xAxisMax], y: [10, 10], mode: "lines", name: "Initial [A]", @@ -108,7 +109,7 @@ const EquilibriumPlot: React.FC = ({ height: Math.max(130, height), xaxis: { ...AXIS_SETTINGS, - range: [0, kd * 2], + range: [0, xAxisMax], title: `[${adjustableAgentName}] ${MICRO}M`, titlefont: { ...AXIS_SETTINGS.titlefont, diff --git a/src/components/quiz-questions/KdQuestion.tsx b/src/components/quiz-questions/KdQuestion.tsx index a8773cf..1970336 100644 --- a/src/components/quiz-questions/KdQuestion.tsx +++ b/src/components/quiz-questions/KdQuestion.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { valueType } from "antd/es/statistic/utils"; import { Flex } from "antd"; @@ -8,6 +8,7 @@ import InputNumber from "../shared/InputNumber"; import { FormState } from "./types"; import styles from "./popup.module.css"; import { MICRO } from "../../constants"; +import { SimulariumContext } from "../../simulation/context"; interface KdQuestionProps { kd: number; @@ -18,6 +19,41 @@ const KdQuestion: React.FC = ({ kd, canAnswer }) => { const [selectedAnswer, setSelectedAnswer] = useState(null); const [formState, setFormState] = useState(FormState.Clear); + const { module } = useContext(SimulariumContext); + + useEffect(() => { + setSelectedAnswer(null); + setFormState(FormState.Clear); + }, [module]); + + const getSuccessMessage = (selectedAnswer: number) => { + if (selectedAnswer < 5) { + return ( + <> + {selectedAnswer} {MICRO}M is considered a{" "} + + low Kd + + , which means A and B have a high affinity{" "} + for one another because it takes a low amount of B to create + the complex. + + ); + } else { + return ( + <> + {selectedAnswer} {MICRO}M is considered a{" "} + + high Kd + + , which means A and C have a low affinity{" "} + for one another because it takes a lot of C to create the + complex. + + ); + } + }; + const handleAnswerSelection = (answer: valueType | null) => { setSelectedAnswer(Number(answer)); @@ -77,17 +113,7 @@ const KdQuestion: React.FC = ({ kd, canAnswer }) => { title="What is the binding affinity?" formContent={formContent} onSubmit={handleSubmit} - successMessage={ - <> - {selectedAnswer} {MICRO}M is considered a{" "} - - low Kd - - , which means A and B have a{" "} - high affinity for one another because - it takes a low amount of B to create the complex. - - } + successMessage={getSuccessMessage(selectedAnswer!)} failureMessage="Visit the “Learn how to derive Kd” button above, then use the Equilibrium concentration plot to answer." formState={formState} id="Kd Value" diff --git a/src/simulation/BindingInstance.ts b/src/simulation/BindingInstance.ts index 8d60e64..8435f1d 100644 --- a/src/simulation/BindingInstance.ts +++ b/src/simulation/BindingInstance.ts @@ -1,5 +1,7 @@ -import { Circle, Vector } from "detect-collisions"; +import { Circle } from "detect-collisions"; import { random } from "lodash"; +import { Vector } from "sat"; + import LiveSimulationData from "./LiveSimulationData"; class BindingInstance extends Circle { @@ -48,7 +50,6 @@ class BindingInstance extends Circle { } private releaseFromParent() { - this.isTrigger = false; this.bound = false; this.parent = null; } @@ -57,7 +58,6 @@ class BindingInstance extends Circle { parent: BindingInstance, overlapV: Vector ): BindingInstance { - this.isTrigger = true; this.parent = parent; // adjust the ligand to the exact edge of the parent this.moveInstance(-overlapV.x, -overlapV.y); @@ -68,6 +68,8 @@ class BindingInstance extends Circle { /** PUBLIC METHODS BELOW */ public moveInstance(x: number, y: number) { + // if we're trying to move a bound instance, move the parent instead + // and then we'll resolve the child position if (this.parent) { this.parent.moveInstance(x, y); } else { @@ -161,7 +163,6 @@ class BindingInstance extends Circle { return false; } this.child = null; - this.isTrigger = false; ligand.releaseFromParent(); // QUESTION: should the ligand be moved to a random position? return true; @@ -180,7 +181,6 @@ class BindingInstance extends Circle { if (!willBind) { return false; } - this.isTrigger = true; this.child = ligand.bindToParent(this, overlapV); return true; } diff --git a/src/simulation/BindingSimulator2D.ts b/src/simulation/BindingSimulator2D.ts index f3cf5c3..36b8d3e 100644 --- a/src/simulation/BindingSimulator2D.ts +++ b/src/simulation/BindingSimulator2D.ts @@ -169,59 +169,6 @@ export default class BindingSimulator implements IClientSimulatorImpl { } } - private resolveCollision( - a: BindingInstance, - b: BindingInstance, - overlapV: Vector, - numberOfPasses: number = 0 - ) { - let toCheck = null; - const { x, y } = overlapV; - // if neither is a trigger, then they - // will both get moved by system.separate() - if (!a.isTrigger && !b.isTrigger) { - return; - } - if (numberOfPasses > 8) { - return; - } - - // prefer to move an instance that is not bound, ie, not isTrigger - // because after it's moved any additional overlaps will be resolved by the system - if (!a.isTrigger) { - a.moveInstance(-x, -y); - toCheck = a; - } else if (!b.isTrigger && b.type === "Circle") { - b.moveInstance(x, y); - toCheck = b; - } else { - a.moveInstance(-x, -y); - toCheck = a; - } - if (toCheck) { - numberOfPasses++; - // after moving the instance, check if it overlaps with any other instance - this.system.checkOne(toCheck, (response: Response) => { - if (response) { - const { a, b, overlapV, overlap } = response; - if (!a.isBoundPair(b)) { - // This check is to keep from having to resolve - // a ridiculous number of collisions - // the value is half the radius of the larger agent - if (overlap > 1) { - return this.resolveCollision( - a, - b, - overlapV, - numberOfPasses - ); - } - } - } - }); - } - } - private relax(maxCycles: number = 30) { let cycles = 0; while (cycles < maxCycles) { @@ -320,11 +267,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { return concentrations; } - public staticUpdate() { - // update the number of agents without - // changing their positions - this.relax(); - + private getAgentData() { const agentData: number[] = []; for (let ii = 0; ii < this.instances.length; ++ii) { const instance = this.instances[ii]; @@ -344,30 +287,10 @@ export default class BindingSimulator implements IClientSimulatorImpl { agentData.push(instance.r); // collision radius agentData.push(0); // subpoints } - const frameData: VisDataMessage = { - // TODO get msgType out of here - msgType: ClientMessageEnum.ID_VIS_DATA_ARRIVE, - bundleStart: this.currentFrame, - bundleSize: 1, // frames - bundleData: [ - { - data: agentData, - frameNumber: this.currentFrame, - time: this.currentFrame, - }, - ], - fileName: "hello world", - }; - this.static = false; - this.currentFrame++; - return frameData; + return agentData; } - public update(): VisDataMessage { - if (this.static || this.initialState) { - return this.staticUpdate(); - } - + private updateAgentsPositions() { for (let i = 0; i < this.instances.length; ++i) { const unbindingOccurred = this.instances[i].oneStep( this.size, @@ -378,11 +301,38 @@ export default class BindingSimulator implements IClientSimulatorImpl { this.currentNumberBound--; } } - // reset to zero for every tenth time point - if (this.currentFrame % 10 === 0) { - this.currentNumberOfBindingEvents = 0; - this.currentNumberOfUnbindingEvents = 0; + } + + private resolveChildPositions() { + for (let i = 0; i < this.instances.length; ++i) { + const instance = this.instances[i]; + // if the instance is a child, we're going to move it to be + // perfectly bound to its parent (with a slight overlap) + if (instance.parent) { + const bindingOverlap = instance.r * 0.5; + const parentPosition = instance.parent.pos; + const childPosition = instance.pos; + const distanceVector = new Vector( + childPosition.x - parentPosition.x, + childPosition.y - parentPosition.y + ); + const distance = Math.sqrt( + distanceVector.x ** 2 + distanceVector.y ** 2 + ); + const perfectBoundDistance = + instance.parent.r + instance.r - bindingOverlap; + const tolerance = 0.1; + if (Math.abs(distance - perfectBoundDistance) > tolerance) { + const ratio = perfectBoundDistance / distance; + const x = parentPosition.x + distanceVector.x * ratio; + const y = parentPosition.y + distanceVector.y * ratio; + instance.setPosition(x, y); + } + } } + } + + private resolveBindingReactions() { this.system.checkAll((response: Response) => { const { a, b, overlapV } = response; @@ -415,39 +365,33 @@ export default class BindingSimulator implements IClientSimulatorImpl { this.currentNumberBound++; } } - // Now that binding has been resolved, resolve collisions - // if they are not bound the system will resolve the collision - if (!a.isBoundPair(b)) { - if (!a.isStatic && !b.isStatic) { - this.resolveCollision(a, b, overlapV); - } - } } else { console.log("no response"); } }); - this.relax(5); - // fill agent data. - const agentData: number[] = []; + } - for (let ii = 0; ii < this.instances.length; ++ii) { - const instance = this.instances[ii]; - 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(instance.pos.x); // x - agentData.push(instance.pos.y); // y - agentData.push(0); // z - agentData.push(0); // rx - agentData.push(0); // ry - agentData.push(0); // rz - agentData.push(instance.r); // collision radius - agentData.push(0); // subpoints + public update(): VisDataMessage { + let agentData: number[] = []; + if (this.static || this.initialState) { + // update number of agents + // without changing positions + this.relax(); + agentData = this.getAgentData(); + this.static = false; + } else { + this.updateAgentsPositions(); + // reset to zero for every tenth time point + if (this.currentFrame % 10 === 0) { + this.currentNumberOfBindingEvents = 0; + this.currentNumberOfUnbindingEvents = 0; + } + this.resolveBindingReactions(); + this.relax(3); + this.resolveChildPositions(); + agentData = this.getAgentData(); } + const frameData: VisDataMessage = { // TODO get msgType out of here msgType: ClientMessageEnum.ID_VIS_DATA_ARRIVE, @@ -513,7 +457,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { position: { x: 0, y: 0, - z: 65, + z: 70, }, }, typeMapping: typeMapping, diff --git a/src/simulation/LiveSimulationData.ts b/src/simulation/LiveSimulationData.ts index 60622b4..ad780d6 100644 --- a/src/simulation/LiveSimulationData.ts +++ b/src/simulation/LiveSimulationData.ts @@ -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]: 0.75, - [Module.A_C_AC]: 10, + [Module.A_C_AC]: 74, [Module.A_B_C_AB_AC]: 5, }; @@ -77,7 +77,7 @@ export default class LiveSimulation implements ISimulationData { static INITIAL_CONCENTRATIONS = { [AgentName.A]: 10, [AgentName.B]: 4, - [AgentName.C]: 10, + [AgentName.C]: 30, }; PRODUCT = { [Module.A_B_AB]: ProductName.AB, @@ -112,7 +112,7 @@ export default class LiveSimulation implements ISimulationData { maxConcentration = 10; break; case Module.A_C_AC: - maxConcentration = 20; //TODO: adjust these as needed + maxConcentration = 75; break; case Module.A_B_C_AB_AC: maxConcentration = 20; //TODO: adjust these as needed diff --git a/src/utils/index.ts b/src/utils/index.ts index 0ebeddf..0d80674 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -53,7 +53,7 @@ export const isSlopeZero = (array: number[]) => { array.slice(-sliceSize * 2, -sliceSize).reduce((a, b) => a + b) / sliceSize; const slope = averageOfLastFive - averageOfFirstFive; - if (Math.abs(slope) < 0.01) { + if (Math.abs(slope) < 0.05) { return true; } else { return false;