diff --git a/Backend/components/component_router.py b/Backend/components/component_router.py index ad0d4cb4..54c66ddd 100644 --- a/Backend/components/component_router.py +++ b/Backend/components/component_router.py @@ -1,7 +1,8 @@ from fastapi import APIRouter -from . import graph_api, record_data +from . import graph_api, record_data, forecast router = APIRouter() router.include_router(graph_api.router) -router.include_router(record_data.router) \ No newline at end of file +router.include_router(record_data.router) +router.include_router(forecast.router) \ No newline at end of file diff --git a/Backend/components/forecast.py b/Backend/components/forecast.py new file mode 100644 index 00000000..53ea10ce --- /dev/null +++ b/Backend/components/forecast.py @@ -0,0 +1,52 @@ +from fastapi import APIRouter +from core import db +import config +from statsmodels.tsa.arima.model import ARIMA +from pmdarima.arima import auto_arima +import pandas as pd +import time +import asyncio +from concurrent.futures import ProcessPoolExecutor + +router = APIRouter() + +def train_arima(df, forecast_step): + # Train the model + model = auto_arima(df, seasonal=False, m=12) + # Forecast the future data + forecast = model.predict(n_periods=forecast_step) + return forecast + +@router.get("/forecast") +async def get_forecast(data: str, start_time: int = 0, end_time: int = 0, forecast_step: int = 0): + '''Using ARIMA to predict the future data on selected dataset + :param data: str: dataset name + :param start_time: int: start time of the training data + :param end_time: int: end time of the training data + :param forecast_step: int: the time to forecast + ''' + # If time is not specified, use current time as end time and 5 min ago as start time for forecast 1 min + if end_time == 0: + end_time = round(time.time() * 1000) + if start_time == 0: + start_time = end_time - 300000 + if forecast_step == 0: + forecast_step = 100 + + # Query the data + df = await db.query([data], start_time, end_time, ['avg'], (end_time - start_time) // 60) + + # relabel the column to 'x','y' + df.columns = ['y'] + # Spawn a new async task to train ARIMA in a separate process + loop = asyncio.get_event_loop() + with ProcessPoolExecutor() as pool: + future = loop.run_in_executor(pool, train_arima, df, forecast_step) + forecast = await future + + # relabel the index to start from 1 to length + forecast.reset_index(drop=True, inplace=True) + forecast.index = (forecast.index * (end_time - start_time) // 60) + end_time + # create json response + response = [{'x': int(forecast.index[i]), 'y': forecast[forecast.index[i]]} for i in range(len(forecast))] + return {'response': {data: response}} diff --git a/Backend/core/comms.py b/Backend/core/comms.py index 2af0cda6..4a26d318 100644 --- a/Backend/core/comms.py +++ b/Backend/core/comms.py @@ -32,6 +32,9 @@ def unpack_data(data): unpacked_data = struct.unpack(format_string, data) for i in range(len(properties)): fields[properties[i]] = unpacked_data[i] + # if the data is -inf or inf, we set it to -10000 + if fields[properties[i]] == float('inf') or fields[properties[i]] == float('-inf'): + fields[properties[i]] = -10000 return fields diff --git a/Frontend/src/Components/Graph/CustomGraph.js b/Frontend/src/Components/Graph/CustomGraph.js index 605fb610..fc5ead10 100644 --- a/Frontend/src/Components/Graph/CustomGraph.js +++ b/Frontend/src/Components/Graph/CustomGraph.js @@ -21,6 +21,7 @@ import { Text, useColorMode, useDisclosure, + useInterval, } from "@chakra-ui/react"; import { Chart as ChartJS, @@ -71,7 +72,7 @@ for (const category of GraphData.Output) { } // the options fed into the graph object, save regardless of datasets -function getOptions(now, secondsRetained, colorMode, optionInfo, extremes) { +function getOptions(now, secondsRetained, colorMode, optionInfo, extremes, xaxisbounds) { const gridColor = getColor("grid", colorMode); const gridBorderColor = getColor("gridBorder", colorMode); const ticksColor = getColor("ticks", colorMode); @@ -125,12 +126,22 @@ function getOptions(now, secondsRetained, colorMode, optionInfo, extremes) { label: (item) => { // custom dataset label // name: value unit + if (item.dataset.key.indexOf("_forcast") !== -1) { + return `${item.dataset.label}: ${item.formattedValue}`; + } return `${item.dataset.label}: ${item.formattedValue} ${optionInfo[item.dataset.key].unit}`; }, labelColor: (item) => { - return { - borderColor: optionInfo[item.dataset.key].borderColor, - backgroundColor: optionInfo[item.dataset.key].backgroundColor + if (item.dataset.key.indexOf("_forcast") !== -1) { + return { + borderColor: "red", + backgroundColor: "rgba(255, 0, 0, 0.5)" + } + } else { + return { + borderColor: optionInfo[item.dataset.key].borderColor, + backgroundColor: optionInfo[item.dataset.key].backgroundColor + } } }, }, @@ -157,9 +168,9 @@ function getOptions(now, secondsRetained, colorMode, optionInfo, extremes) { borderWidth: 2, }, - // show the last secondsRetained seconds - max: DateTime.fromMillis(Math.floor(now/1000) * 1000).toISO(), - min: DateTime.fromMillis((Math.floor(now/1000) - secondsRetained) * 1000).toISO(), + // round to the nearest second + max: Math.round(xaxisbounds.max/1000)*1000, + min: Math.round(xaxisbounds.min/1000)*1000, }, y: { suggestedMin: extremes[0], @@ -200,9 +211,10 @@ function getOptions(now, secondsRetained, colorMode, optionInfo, extremes) { * @constructor */ function Graph(props) { - const { querylist, histLen, colorMode, optionInfo, extremes } = props; + const { querylist, histLen, colorMode, optionInfo, extremes, forcastKey } = props; // response from the server const [data, setData] = useState([]); + const [forcastData, setForcastData] = useState([]); const [fetchDep, setFetchDep] = useState(true); const fetchData = useCallback(async () => { @@ -239,7 +251,19 @@ function Graph(props) { } }, [querylist, histLen]); useEffect(fetchData, [fetchDep]); - + + // fetch forcast data every 10 seconds + useInterval(() => { + if (forcastKey) { + const now = Date.now(); + fetch(ROUTES.GET_FORECAST_DATA + `?data=${forcastKey}&start_time=${now - (histLen + 1) * 1000}&end_time=${now}&forecast_step=0`) + .then((response) => response.json()) + .then((data) => { + setForcastData(data.response); + }); + } + }, 10000); + console.log(forcastData); // get the latest timestamp in the packet let tstamp = 0; if ("timestamps" in data) { @@ -250,6 +274,8 @@ function Graph(props) { let formattedData = {}; formattedData['datasets'] = []; + let maximum = 0; + let minimum = Infinity; for(const key in data) { if(key !== 'timestamps' && optionInfo[key]) formattedData['datasets'].push({ @@ -259,8 +285,26 @@ function Graph(props) { borderColor: optionInfo[key].borderColor, backgroundColor: optionInfo[key].backgroundColor }); + maximum = Math.max(maximum, ...data[key].map((x) => x.x)); + minimum = Math.min(minimum, ...data[key].map((x) => x.x)); + } + if ('timestamps' in data) { + maximum = Math.max(...data['timestamps']); + minimum = Math.min(...data['timestamps']); } + if(forcastKey && forcastKey in forcastData) { + formattedData['datasets'].push({ + key: forcastKey + "_forcast", + label: optionInfo[forcastKey].label, + data: forcastData[forcastKey], + borderColor: "red", + backgroundColor: "rgba(255, 0, 0, 0.5)" + }); + maximum = Math.max(maximum, ...forcastData[forcastKey].map((x) => x.x)); + minimum = Math.min(minimum, ...forcastData[forcastKey].map((x) => x.x)); + } + const xbound = {'min': minimum, 'max': maximum}; return ( @@ -303,6 +348,7 @@ export default function CustomGraph(props) { const [noShowKeys, setNoShowKeys] = useState({}); // Information about options for styling purposes, // reduced and combined so that minimal information is passed to other components + const [forcastKey, setForcastKey] = useState(null); const [optionInfo, yRange] = useMemo(() => { return datasetKeys .filter((key) => !noShowKeys[key]) @@ -423,17 +469,29 @@ export default function CustomGraph(props) { return (