Skip to content

Commit

Permalink
feat: persist model time intervals in the backend
Browse files Browse the repository at this point in the history
- New Django model, `TimeInterval`, representing time intervals for secondary parameters.
- Update the combined model serialiser so that  it can save `model.time_intervals`.
- Update the OpenAPI schema in the backend and frontend.
- Update the React app to use the new OpenAPI models.
  • Loading branch information
eatyourgreens committed Nov 25, 2024
1 parent 8fc81d6 commit 3f21aee
Show file tree
Hide file tree
Showing 19 changed files with 355 additions and 93 deletions.
10 changes: 2 additions & 8 deletions frontend-v2/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,19 @@ import "react-toastify/dist/ReactToastify.css";
import { SimulationContext } from "./contexts/SimulationContext";
import { SimulateResponse } from "./app/backendApi";
import { CollapsibleSidebarProvider } from "./shared/contexts/CollapsibleSidebarContext";
import { useModelTimeIntervals } from "./hooks/useModelTimeIntervals";

export type TimeInterval = {
start: number;
end: number;
unit: { [key: string]: string };
};
type Threshold = { lower: number; upper: number };
export type Thresholds = { [key: string]: Threshold };

const TIME_INTERVALS: TimeInterval[] = [];

const THRESHOLDS: Thresholds = {};

function App() {
const dispatch = useAppDispatch();
const isAuth = useSelector(isAuthenticated);
const error = useSelector((state: RootState) => state.login.error);
const [intervals, setIntervals] = useModelTimeIntervals();
const [simulations, setSimulations] = useState<SimulateResponse[]>([]);
const [intervals, setIntervals] = useState<TimeInterval[]>(TIME_INTERVALS);
const [thresholds, setThresholds] = useState<Thresholds>(THRESHOLDS);
const simulationContext = {
simulations,
Expand Down
33 changes: 33 additions & 0 deletions frontend-v2/src/app/backendApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1959,10 +1959,40 @@ export type DerivedVariableRead = {
/** base variable in PK part of model */
pk_variable: number;
};
export type TimeInterval = {
/** true if object has been stored */
read_only?: boolean;
/** datetime the object was stored. */
datetime?: string | null;
/** start time of interval */
start_time: number;
/** end time of interval */
end_time: number;
/** PKPD model that this time interval is for */
pkpd_model: number;
/** unit of interval */
unit: number;
};
export type TimeIntervalRead = {
id: number;
/** true if object has been stored */
read_only?: boolean;
/** datetime the object was stored. */
datetime?: string | null;
/** start time of interval */
start_time: number;
/** end time of interval */
end_time: number;
/** PKPD model that this time interval is for */
pkpd_model: number;
/** unit of interval */
unit: number;
};
export type CombinedModelSpeciesEnum = "H" | "R" | "N" | "M";
export type CombinedModel = {
mappings: PkpdMapping[];
derived_variables: DerivedVariable[];
time_intervals: TimeInterval[];
/** true if object has been stored */
read_only?: boolean;
/** datetime the object was stored. */
Expand Down Expand Up @@ -2001,6 +2031,7 @@ export type CombinedModelRead = {
id: number;
mappings: PkpdMappingRead[];
derived_variables: DerivedVariableRead[];
time_intervals: TimeIntervalRead[];
components: string;
variables: number[];
mmt: string;
Expand Down Expand Up @@ -2043,6 +2074,7 @@ export type CombinedModelRead = {
export type PatchedCombinedModel = {
mappings?: PkpdMapping[];
derived_variables?: DerivedVariable[];
time_intervals?: TimeInterval[];
/** true if object has been stored */
read_only?: boolean;
/** datetime the object was stored. */
Expand Down Expand Up @@ -2081,6 +2113,7 @@ export type PatchedCombinedModelRead = {
id?: number;
mappings?: PkpdMappingRead[];
derived_variables?: DerivedVariableRead[];
time_intervals?: TimeIntervalRead[];
components?: string;
variables?: number[];
mmt?: string;
Expand Down
6 changes: 2 additions & 4 deletions frontend-v2/src/contexts/SimulationContext.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { createContext } from "react";
import { SimulateResponse } from "../app/backendApi";
import { TimeInterval, Thresholds } from "../App";
import { SimulateResponse, TimeIntervalRead } from "../app/backendApi";
import { Thresholds } from "../App";

export const SimulationContext = createContext({
simulations: [] as SimulateResponse[],
setSimulations: (simulations: SimulateResponse[]) => {},
intervals: [] as TimeInterval[],
setIntervals: (intervals: TimeInterval[]) => {},
thresholds: {} as Thresholds,
setThresholds: (thresholds: Thresholds) => {},
});
1 change: 1 addition & 0 deletions frontend-v2/src/features/model/Model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const Model: FC = () => {
project: projectIdOrZero,
mappings: [],
derived_variables: [],
time_intervals: [],
components: "",
variables: [],
mmt: "",
Expand Down
89 changes: 47 additions & 42 deletions frontend-v2/src/features/model/secondary/TimeIntervalsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, useContext } from "react";
import { FC, useState } from "react";
import {
Button,
FormControl,
Expand All @@ -18,16 +18,16 @@ import Delete from "@mui/icons-material/Delete";
import { useSelector } from "react-redux";

import {
TimeIntervalRead,
useProjectRetrieveQuery,
useUnitListQuery,
} from "../../../app/backendApi";
import { RootState } from "../../../app/store";
import { SimulationContext } from "../../../contexts/SimulationContext";
import { TimeInterval } from "../../../App";
import { useModelTimeIntervals } from "../../../hooks/useModelTimeIntervals";

type TimeUnitSelectProps = {
interval: TimeInterval;
onChange: (interval: TimeInterval) => void;
interval: TimeIntervalRead;
onChange: (interval: TimeIntervalRead) => void;
};

function useTimeUnits() {
Expand All @@ -47,25 +47,25 @@ function useTimeUnits() {
}

function TimeUnitSelect({ interval, onChange }: TimeUnitSelectProps) {
const defaultTimeUnit = 9; // set hours by default
const [selectedUnit, setSelectedUnit] = useState(
interval.unit || defaultTimeUnit,
);
const timeUnits = useTimeUnits();
const timeUnitOptions =
timeUnits?.map((unit) => ({ value: unit.symbol, label: unit.symbol })) ||
[];
const defaultTimeUnit = "h";
timeUnits?.map((unit) => ({ value: unit.id, label: unit.symbol })) || [];

function onChangeUnit(event: SelectChangeEvent) {
const unit = timeUnits?.find((unit) => unit.symbol === event.target.value);
const unit = timeUnits?.find((unit) => unit.id === event.target.value);
if (unit) {
onChange({ ...interval, unit });
setSelectedUnit(+unit.id);
onChange({ ...interval, unit: +unit.id });
}
}

return (
<FormControl>
<Select
value={interval.unit.symbol || defaultTimeUnit}
onChange={onChangeUnit}
>
<Select value={selectedUnit.toString()} onChange={onChangeUnit}>
{timeUnitOptions.map((option) => (
<MenuItem key={option.label} value={option.value}>
{option.label}
Expand All @@ -77,30 +77,32 @@ function TimeUnitSelect({ interval, onChange }: TimeUnitSelectProps) {
}

type IntervalRowProps = {
interval: TimeInterval;
interval: TimeIntervalRead;
onDelete: () => void;
onUpdate: (interval: TimeInterval) => void;
onUpdate: (interval: TimeIntervalRead) => void;
};

function IntervalRow({ interval, onDelete, onUpdate }: IntervalRowProps) {
const [start, setStart] = useState(interval.start_time);
const [end, setEnd] = useState(interval.end_time);
function onChangeStart(event: React.ChangeEvent<HTMLInputElement>) {
onUpdate({ ...interval, start: parseFloat(event.target.value) });
const newStartTime = parseFloat(event.target.value);
setStart(newStartTime);
onUpdate({ ...interval, start_time: newStartTime });
}
function onChangeEnd(event: React.ChangeEvent<HTMLInputElement>) {
onUpdate({ ...interval, end: parseFloat(event.target.value) });
const newEndTime = parseFloat(event.target.value);
setEnd(newEndTime);
onUpdate({ ...interval, end_time: newEndTime });
}

return (
<TableRow>
<TableCell>
<TextField
type="number"
value={interval.start}
onChange={onChangeStart}
/>
<TextField type="number" value={start} onChange={onChangeStart} />
</TableCell>
<TableCell>
<TextField type="number" value={interval.end} onChange={onChangeEnd} />
<TextField type="number" value={end} onChange={onChangeEnd} />
</TableCell>
<TableCell>
<TimeUnitSelect interval={interval} onChange={onUpdate} />
Expand All @@ -115,36 +117,39 @@ function IntervalRow({ interval, onDelete, onUpdate }: IntervalRowProps) {
}

const TimeIntervalsTable: FC<TableProps> = (props) => {
const { intervals, setIntervals } = useContext(SimulationContext);
const [intervals, setIntervals] = useModelTimeIntervals();
const timeUnits = useTimeUnits();
const hourUnit = timeUnits?.find((unit) => unit.symbol === "h");

function addInterval() {
const lastInterval = intervals[intervals.length - 1];
if (hourUnit) {
let newInterval: TimeInterval = { start: 0, end: 0, unit: hourUnit };
let newInterval = {
start_time: 0,
end_time: 0,
unit: +hourUnit.id,
};
if (lastInterval) {
const duration = lastInterval.end - lastInterval.start;
const duration = lastInterval.end_time - lastInterval.start_time;
newInterval = {
start: lastInterval.end,
end: lastInterval.end + duration,
start_time: lastInterval.end_time,
end_time: lastInterval.end_time + duration,
unit: lastInterval.unit,
};
}
setIntervals([...intervals, newInterval]);
}
}
function removeInterval(index: number) {
setIntervals(intervals.filter((_, i) => i !== index));
function removeInterval(id: number) {
const newIntervals = intervals.filter((i) => i.id !== id);
setIntervals(newIntervals);
}
function updateInterval(index: number, interval: TimeInterval) {
setIntervals(
intervals.map((i, iIndex) => (iIndex === index ? interval : i)),
);
function updateInterval(id: number, interval: TimeIntervalRead) {
setIntervals(intervals.map((i) => (i.id === id ? interval : i)));
}
const onDelete = (index: number) => () => removeInterval(index);
const onUpdate = (index: number) => (interval: TimeInterval) =>
updateInterval(index, interval);
const onDelete = (id: number) => () => removeInterval(id);
const onUpdate = (id: number) => (interval: TimeIntervalRead) =>
updateInterval(id, interval);

return (
<>
Expand All @@ -156,12 +161,12 @@ const TimeIntervalsTable: FC<TableProps> = (props) => {
<TableCell>Remove</TableCell>
</TableHead>
<TableBody>
{intervals.map((interval, index) => (
{intervals.map((interval) => (
<IntervalRow
key={index}
key={interval.start_time}
interval={interval}
onDelete={onDelete(index)}
onUpdate={onUpdate(index)}
onDelete={onDelete(interval.id)}
onUpdate={onUpdate(interval.id)}
/>
))}
</TableBody>
Expand Down
1 change: 1 addition & 0 deletions frontend-v2/src/features/projects/Projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ const ProjectTable: FC = () => {
project: newProject.data.id,
mappings: [],
derived_variables: [],
time_intervals: [],
},
}).then((combinedModel) => {
if (combinedModel?.data) {
Expand Down
18 changes: 10 additions & 8 deletions frontend-v2/src/features/results/ResultsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, useContext, useState } from "react";
import { FC, useState } from "react";
import {
FormControl,
InputLabel,
Expand All @@ -8,14 +8,13 @@ import {
Typography,
} from "@mui/material";

import { TimeInterval } from "../../App";
import { VariableRead } from "../../app/backendApi";
import { TimeIntervalRead, VariableRead } from "../../app/backendApi";
import useSubjectGroups from "../../hooks/useSubjectGroups";
import { SimulationContext } from "../../contexts/SimulationContext";

import { useConcentrationVariables } from "./useConcentrationVariables";
import { useParameters, Parameter } from "./useParameters";
import { ResultsTable } from "./ResultsTable";
import { useModelTimeIntervals } from "../../hooks/useModelTimeIntervals";

const options = [
{ name: "Parameters", value: "parameters" },
Expand All @@ -28,13 +27,13 @@ export type FilterIndex = number | "rows" | "columns";
export type RowData =
| { name: string }[]
| Parameter[]
| TimeInterval[]
| TimeIntervalRead[]
| VariableRead[];

type Item =
| { name: string }
| Parameter
| (TimeInterval & { name: string })
| (TimeIntervalRead & { name: string })
| VariableRead;
type RowFilter = {
filter: (event: SelectChangeEvent) => void;
Expand All @@ -45,7 +44,7 @@ type RowFilter = {

const ResultsTab: FC = () => {
const { groups = [] } = useSubjectGroups();
const { intervals } = useContext(SimulationContext);
const [intervals] = useModelTimeIntervals();
const concentrationVariables = useConcentrationVariables();
const parameters = useParameters();

Expand Down Expand Up @@ -126,7 +125,10 @@ const ResultsTab: FC = () => {
const intervalSelect = {
filter: handleIntervalChange,
value: interval,
items: intervals.map((i) => ({ name: `${i.start} - ${i.end}`, ...i })),
items: intervals.map((i) => ({
name: `${i.start_time} - ${i.end_time}`,
...i,
})),
label: "Interval",
};
const variableSelect = {
Expand Down
10 changes: 6 additions & 4 deletions frontend-v2/src/features/results/ResultsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import {
TableHead,
TableRow,
} from "@mui/material";
import { FC, useContext } from "react";
import { FC } from "react";

import { SimulationContext } from "../../contexts/SimulationContext";
import useSubjectGroups from "../../hooks/useSubjectGroups";

import { useParameters } from "./useParameters";
Expand All @@ -17,6 +16,7 @@ import { useConcentrationVariables } from "./useConcentrationVariables";
import { FilterIndex, RowData } from "./ResultsTab";
import { useTableRows } from "./useTableRows";
import { getTableHeight } from "../../shared/calculateTableHeights";
import { useModelTimeIntervals } from "../../hooks/useModelTimeIntervals";

const RESULTS_TABLE_HEIGHTS = [
{
Expand Down Expand Up @@ -101,7 +101,7 @@ export const ResultsTable: FC<ResultsTableProps> = ({
const { groups } = useSubjectGroups();
const parameters = useParameters();
const concentrationVariables = useConcentrationVariables();
const { intervals } = useContext(SimulationContext);
const [intervals] = useModelTimeIntervals();
const tableRows = useTableRows({
rows,
groupIndex,
Expand All @@ -121,7 +121,9 @@ export const ResultsTable: FC<ResultsTableProps> = ({
: variableIndex === "columns"
? concentrationVariables.map((variable) => variable.name)
: intervalIndex === "columns"
? intervals.map((interval) => `${interval.start}${interval.end}`)
? intervals.map(
(interval) => `${interval.start_time}${interval.end_time}`,
)
: groupIndex === "columns"
? groups
? [{ name: "Project" }, ...groups].map((group) => group.name)
Expand Down
Loading

0 comments on commit 3f21aee

Please sign in to comment.