Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/concentration plot #1

Merged
merged 11 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
68 changes: 50 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];
interim17 marked this conversation as resolved.
Show resolved Hide resolved
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);
interim17 marked this conversation as resolved.
Show resolved Hide resolved
};

Expand All @@ -83,25 +104,36 @@ function App() {
min={0}
max={100}
initialValue={timeFactor}
onChange={(name, value) => {
onChange={(_, value) => {
setTimeFactor(value);
}}
name="time factor (ns)"
meganrm marked this conversation as resolved.
Show resolved Hide resolved
/>
<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} />
interim17 marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
</>
);
Expand Down
63 changes: 42 additions & 21 deletions src/BindingSimulator2D.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,57 +141,58 @@ 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 = () => {};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for future reference, I think IClientSimulatorImpl should have some API for transmitting plot data back to viewer.

constructor(agents: InputAgent[], timeFactor: number = 25) {
this.system = new System();
this.agents = agents;
this.instances = [];
this.createBoundingLines();
this.distanceFactor = 10;
this.timeFactor = timeFactor;
Expand All @@ -201,6 +202,7 @@ export default class BindingSimulator implements IClientSimulatorImpl {
}

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 @@ -308,6 +306,12 @@ export default class BindingSimulator implements IClientSimulatorImpl {
return count;
}

private convertCountToConcentration(count: number) {
const volume = size * size * 1 * this.distanceFactor ** 2;
ShrimpCryptid marked this conversation as resolved.
Show resolved Hide resolved
const concentration = count / (volume * 10 ** -6 * 6.022);
ShrimpCryptid marked this conversation as resolved.
Show resolved Hide resolved
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 +346,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 +409,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 +475,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