Skip to content

Commit

Permalink
Merge pull request #1 from simularium/feature/concentration-plot
Browse files Browse the repository at this point in the history
Feature/concentration plot
  • Loading branch information
meganrm authored Feb 6, 2024
2 parents 746807d + 08c822b commit 7152063
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 71 deletions.
Binary file modified bun.lockb
Binary file not shown.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@
"@types/lodash": "^4.14.202",
"detect-collisions": "^9.2.2",
"lodash": "^4.17.21",
"plotly.js": "^2.28.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-plotly": "^1.0.0",
"react-plotly.js": "^2.6.0"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@types/react-plotly.js": "^2.6.3",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
Expand Down
69 changes: 51 additions & 18 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";

import "./App.css";
import Viewer from "./components/Viewer";
Expand All @@ -12,12 +12,17 @@ import {
import Concentration from "./components/Concentration";
import { AvailableAgentNames } from "./types";
import Slider from "./components/Slider";
import Plot from "./components/Plot";

const INITIAL_CONCENTRATIONS = { A: 10, B: 10, C: 10 };

const INITIAL_CONCENTRATIONS = { A: 10, B: 10, C: 0 };
function App() {
const [isPlaying, setIsPlaying] = useState(false);
const [concentration, setConcentration] = useState(INITIAL_CONCENTRATIONS);
const [timeFactor, setTimeFactor] = useState(25);
const [inputConcentration, setInputConcentration] = useState(INITIAL_CONCENTRATIONS);
const [timeFactor, setTimeFactor] = useState(30);
const [productOverTime, setProductOverTime] = useState({
[inputConcentration[AvailableAgentNames.B]]: [0],
});
const [activeAgents, setActiveAgents] = useState([
AvailableAgentNames.A,
AvailableAgentNames.B,
Expand All @@ -35,6 +40,18 @@ function App() {
return new BindingSimulator(trajectory);
}, [activeAgents]);

const handleTimeChange = () => {
const newValue = clientSimulator.getCurrentConcentrationBound();
const currentConcentration = inputConcentration[AvailableAgentNames.B];
const currentArray = productOverTime[currentConcentration];
const newData = [...currentArray, newValue];
const newState = {
...productOverTime,
[currentConcentration]: newData,
};
setProductOverTime(newState);
};

useEffect(() => {
simulariumController.setCameraType(true);
simulariumController.changeFile(
Expand All @@ -58,14 +75,18 @@ function App() {
clientSimulator.setTimeScale(timeFactor);
}, [timeFactor, clientSimulator]);

const handleConcentrationChange = (
name: AvailableAgentNames,
value: number
) => {
const agentId = AVAILABLE_AGENTS[name].id;
const handleNewInputConcentration = (name: string, value: number) => {
const agentName = name as AvailableAgentNames;
const agentId = AVAILABLE_AGENTS[agentName].id;
clientSimulator.changeConcentration(agentId, value);
setConcentration({ ...concentration, [name]: value });

setInputConcentration({ ...inputConcentration, [name]: value });
const time = simulariumController.time();
const newState = {
...productOverTime,
[value]: [],
};
setProductOverTime(newState);
simulariumController.gotoTime(time + 1);
};

Expand All @@ -83,25 +104,37 @@ function App() {
min={0}
max={100}
initialValue={timeFactor}
onChange={(name, value) => {
onChange={(_, value) => {
setTimeFactor(value);
}}
disabled={false}
name="time factor (ns)"
/>
<Concentration
agents={concentration}
onChange={handleConcentrationChange}
activeAgents={activeAgents}
concentration={inputConcentration}
onChange={handleNewInputConcentration}
disabled={isPlaying}
/>
<select
onChange={(e) => setActiveAgents(e.target.value.split(","))}
onChange={(e) =>
setActiveAgents(
e.target.value.split(",") as AvailableAgentNames[]
)
}
defaultValue={trajectories[0]}
>
<option value={trajectories[0]}>Low affinity</option>
<option value={trajectories[1]}>High affinity</option>
<option value={trajectories[0]}>High affinity</option>
<option value={trajectories[1]}>Low affinity</option>
<option value={trajectories[2]}>Competitive</option>
</select>

<Viewer controller={simulariumController} />
<div style={{ display: "flex" }}>
<Viewer
controller={simulariumController}
handleTimeChange={handleTimeChange}
/>
<Plot data={productOverTime} />
</div>
</div>
</>
);
Expand Down
83 changes: 59 additions & 24 deletions src/BindingSimulator2D.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,66 +141,68 @@ class BindingInstance extends Circle {
return [x, y];
}

public unBind(ligand: BindingInstance) {
public unBind(ligand: BindingInstance): boolean {
if (ligand.kOff === undefined) {
return;
return false;
}
const willUnBind = random(0, 1, true) > ligand.kOff;
const willUnBind = random(0, 1, true) < ligand.kOff;
if (!willUnBind) {
return;
return false;
}
this.child = null;
this.isTrigger = false;
ligand.bound = false;
ligand.isTrigger = false;
return;
return true;
}

public bind(ligand: BindingInstance) {
public bind(ligand: BindingInstance): boolean {
if (ligand.bound || this.child) {
// already have bound ligand or already bound
// can't bind to another ligand
return;
return false;
}
if (ligand.kOn === undefined) {
return;
return false;
}
const willBind = random(0, 1, true) > ligand.kOn;
const willBind = random(0, 1, true) < ligand.kOn;
if (!willBind) {
return;
return false;
}
this.child = ligand;
this.isTrigger = true;
ligand.bound = true;
ligand.isTrigger = true;
return;
return true;
}
}

const size = 100;

export default class BindingSimulator implements IClientSimulatorImpl {
instances: BindingInstance[];
instances: BindingInstance[] = [];
currentFrame: number;
agents: InputAgent[] = [];
system: System;
distanceFactor: number;
timeFactor: number;
static: boolean = false;
initialState: boolean = true;
currentNumberBound: number = 0;
onUpdate: (data: number) => void = () => {};
constructor(agents: InputAgent[], timeFactor: number = 25) {
this.system = new System();
this.agents = agents;
this.instances = [];
this.createBoundingLines();
this.distanceFactor = 10;
this.distanceFactor = 40;
this.timeFactor = timeFactor;
this.initializeAgents(agents);
this.currentFrame = 0;
this.system.separate();
}

private clearAgents() {
this.currentNumberBound = 0;
this.system = new System();
this.instances = [];
}
Expand Down Expand Up @@ -231,10 +233,6 @@ export default class BindingSimulator implements IClientSimulatorImpl {
}
}

public setTimeScale(timeScale: number) {
this.timeFactor = timeScale;
}

public changeConcentration(agentId: number, newConcentration: number) {
const agent = this.agents.find((agent) => agent.id === agentId);
if (!agent) {
Expand Down Expand Up @@ -303,11 +301,31 @@ export default class BindingSimulator implements IClientSimulatorImpl {
}

private convertConcentrationToCount(concentration: number) {
const volume = size * size * 1 * this.distanceFactor ** 2;
const count = concentration * volume * 10 ** -6 * 6.022;
// calculating the number of particles in the volume
// from the concentration in uM (micromoles per liter)
// volume is in nm^3
// 1 nm^3 = 10^-24 L
// 1 mole = 10^6 micromoles
// 10 ^(-24 - 6 + 23) = 10^-7
const depth = 1.0;
const volume =
size * this.distanceFactor * (size * this.distanceFactor) * depth;
const count = concentration * volume * 10 ** -7 * 6.022;
return count;
}

private convertCountToConcentration(count: number) {
// calculating the concentration in uM (micromoles per liter)
// volume is in nm^3 and count is the number of particles
// 1 nm^3 = 10^-24 L
// 1 mole = 6.022 x 10^23 particles (count)
const depth = 1.0;
const volume =
size * this.distanceFactor * (size * this.distanceFactor) * depth;
const concentration = count / (volume * 10 ** -7 * 6.022);
return concentration;
}

private getRandomPointOnSide(side: number, total: number) {
const buffer = size / 5;
const dFromSide = random(0 + buffer, size / 2, true);
Expand Down Expand Up @@ -342,6 +360,14 @@ export default class BindingSimulator implements IClientSimulatorImpl {
// },
}

public setTimeScale(timeScale: number) {
this.timeFactor = timeScale;
}

public getCurrentConcentrationBound() {
return this.convertCountToConcentration(this.currentNumberBound);
}

public staticUpdate() {
// update the number of agents without
// changing their positions
Expand Down Expand Up @@ -397,20 +423,28 @@ export default class BindingSimulator implements IClientSimulatorImpl {

if (response) {
if (a.isBoundPair(b)) {
let unbound = false;
if (a.r < b.r) {
b.unBind(a);
unbound = b.unBind(a);
} else {
// b is the ligand
a.unBind(b);
unbound = a.unBind(b);
}
if (unbound) {
this.currentNumberBound--;
}
}
if (a.partners.includes(b.id)) {
// a is the ligand
let bound = false;
if (a.r < b.r) {
b.bind(a);
bound = b.bind(a);
} else {
// b is the ligand
a.bind(b);
bound = a.bind(b);
}
if (bound) {
this.currentNumberBound++;
}
}
if (a.isTrigger && b.isTrigger && !a.isBoundPair(b)) {
Expand Down Expand Up @@ -455,6 +489,7 @@ export default class BindingSimulator implements IClientSimulatorImpl {
],
fileName: "hello world",
};

this.currentFrame++;
return frameData;
}
Expand Down
14 changes: 10 additions & 4 deletions src/components/Concentration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import { map } from "lodash";
import { AvailableAgentNames } from "../types";

interface AgentProps {
agents: { [key in AvailableAgentNames]: number };
onChange: (name: AvailableAgentNames, value: number) => void;
activeAgents: AvailableAgentNames[];
concentration: { [key in AvailableAgentNames]: number };
onChange: (name: string, value: number) => void;
disabled: boolean;
}

const Concentration: React.FC<AgentProps> = ({ agents, onChange }) => {
return map(agents, (concentration, agent: AvailableAgentNames) => {
const Concentration: React.FC<AgentProps> = ({ concentration, onChange, activeAgents, disabled }) => {
return map(concentration, (concentration, agent: AvailableAgentNames) => {
if (!activeAgents.includes(agent)) {
return null;
}
return (
<Slider
min={1}
Expand All @@ -18,6 +23,7 @@ const Concentration: React.FC<AgentProps> = ({ agents, onChange }) => {
initialValue={concentration}
onChange={onChange}
key={agent}
disabled={disabled}
/>
);
});
Expand Down
39 changes: 39 additions & 0 deletions src/components/Plot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { map } from "lodash";
import { PlotData } from "plotly.js";
import React from "react";
import Plot from "react-plotly.js";

interface PlotProps {
data: { [key: string]: number[] };
}

const LinePlot: React.FC<PlotProps> = ({ data }) => {
const traces = map(
data,
(yValues: number[], id: string): Partial<PlotData> => {
if (yValues.length <= 1) {
// when the concentration is first changed it,
// plays one frame to update, so there is one value
// already but not necessarily data yet
return {};
}
return {
x: yValues.map((_, i) => i),
y: yValues,
type: "scatter" as const,
mode: "lines" as const,
name: id,
};
}
);

const layout = {
title: "Concentration over Time",
xaxis: { title: "Time", range: [0, "auto"] },
yaxis: { title: "Concentration" },
};

return <Plot data={traces} layout={layout} />;
};

export default LinePlot;
Loading

0 comments on commit 7152063

Please sign in to comment.