Compare commits

..

19 Commits

Author SHA1 Message Date
130962a582 Merge remote-tracking branch 'origin' into front-2-fix-gen 2024-12-02 22:29:32 +04:00
b3d053b06f Add predictions to api (fixes) 2024-12-01 19:56:55 +04:00
f2224ed6b0 Add predictions to api 2024-12-01 19:07:44 +04:00
faa9bb3e26 Merge pull request 'prediction' (#10) from prediction into front-2-fix-gen
Reviewed-on: #10
2024-12-01 18:53:32 +04:00
325fe7c6f5 fixes 2 2024-12-01 18:53:00 +04:00
a229d96eda fixes 2024-12-01 18:49:24 +04:00
9602a1206c Merge pull request 'prediction' (#9) from prediction into front-2-fix-gen
Reviewed-on: #9
2024-12-01 18:30:09 +04:00
39cd0d0860 добавление авроры с использованием открытого датасета 2024-11-27 00:38:45 +04:00
3ffc4467bb metPy function with example Aurora 2024-11-26 23:17:03 +04:00
5b8de6374d Fix generation 2024-11-26 16:40:28 +04:00
eb686935d6 [front-2]: fix 2024-11-26 16:11:52 +04:00
cd59459e43 [front-2]: windmill data with park id 2024-11-26 13:25:24 +04:00
27e4a475bd Merge remote-tracking branch 'origin/test-wind-park-retrieve' into front-2 2024-11-26 13:23:25 +04:00
834ca2adfe [front-2]-floris 2024-11-26 13:02:12 +04:00
58ccbf9ef9 [front-2]: floris page 2024-11-24 22:43:23 +04:00
f30661c709 Попытка в работу с новыми сущностями и флорис. 2024-11-24 22:21:23 +04:00
7022fdb50b добавили комменты к строкам, возможно придется переписать бд🤩🤩🤩 2024-11-05 21:57:32 +04:00
cf0ff194d2 что-то накидала. мини-мини рефакторинг + прогнозирование бартренд + сохранение в бд 2024-11-05 03:06:11 +04:00
737af8e01d какой-то предикт хайповый, даша воркает над ним😎😎😎 2024-11-05 00:12:15 +04:00
167 changed files with 1239 additions and 76 deletions

View File

@ -1,11 +1,10 @@
from PyWeather.weather.stations.davis import VantagePro
import logging import logging
import time
import mariadb import mariadb
import serial.tools.list_ports import serial.tools.list_ports
import gc
import time
from pprint import pprint
from PyWeather.weather.stations.davis import VantagePro
logging.basicConfig(filename="Stations.log", logging.basicConfig(filename="Stations.log",
format='%(asctime)s %(message)s', format='%(asctime)s %(message)s',
@ -13,37 +12,10 @@ logging.basicConfig(filename="Stations.log",
logger = logging.getLogger('davis_api') logger = logging.getLogger('davis_api')
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
def write_data(device, station, send=True): console_handler.setLevel(logging.DEBUG)
try: console_handler.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
#device.parse() logger.addHandler(console_handler)
data = device.fields
print(data)
if len(data) < 1:
return
else:
print(data)
fields = ['BarTrend', 'CRC', 'DateStamp', 'DewPoint', 'HeatIndex', 'ETDay', 'HeatIndex',
'HumIn', 'HumOut', 'Pressure', 'RainDay', 'RainMonth', 'RainRate', 'RainStorm',
'RainYear', 'SunRise', 'SunSet', 'TempIn', 'TempOut', 'WindDir', 'WindSpeed',
'WindSpeed10Min']
if send:
placeholders = ', '.join(['%s'] * len(fields))
field_names = ', '.join(fields)
sql = f"INSERT INTO weather_data ({field_names}) VALUES ({placeholders})"
values = [data[field] for field in fields]
cursor.execute(sql, values)
conn.commit()
else:
pprint(data)
del data
del fields
gc.collect()
except Exception as e:
logger.error(str(e))
raise e
try: try:
conn = mariadb.connect( conn = mariadb.connect(
@ -57,23 +29,25 @@ try:
except mariadb.Error as e: except mariadb.Error as e:
logger.error('DB_ERR: ' + str(e)) logger.error('DB_ERR: ' + str(e))
raise e raise e
while True:
try:
ports = serial.tools.list_ports.comports()
available_ports = {}
try: for port in ports:
ports = serial.tools.list_ports.comports() if port.serial_number == '0001':
available_ports = {} available_ports[port.name] = port.vid
for port in ports: devices = [VantagePro(port) for port in available_ports.keys()]
if port.serial_number == '0001': while True:
available_ports[port.name] = port.vid for i in range(1):
if len(devices) != 0:
logger.info(devices)
else:
raise Exception('Can`t connect to device')
time.sleep(60)
except Exception as e:
logger.error('Device_error' + str(e))
time.sleep(60)
devices = [VantagePro(port) for port in available_ports.keys()] # todo переписать под influx, для линухи приколы сделать
print(available_ports)
while True:
for i in range(len(devices)):
print(devices[i].fields)
#write_data(devices[i], 'st' + str(available_ports[list(available_ports.keys())[i]]), True)
time.sleep(1)
except Exception as e:
logger.error('Device_error: ' + str(e))
raise e

200
davisAPI/prediction.py Normal file
View File

@ -0,0 +1,200 @@
from datetime import datetime
from pathlib import Path
import metpy.calc
import numpy as np
import requests
import torch
import xarray as xr
from aurora import AuroraSmall, Batch, Metadata
from metpy.units import units
def get_download_paths(date):
"""Создает список путей для загрузки данных."""
download_path = Path("~/downloads/hres_0.1").expanduser()
downloads = {}
var_nums = {
"2t": "167", "10u": "165", "10v": "166", "msl": "151", "t": "130",
"u": "131", "v": "132", "q": "133", "z": "129", "slt": "043", "lsm": "172",
}
for v in ["2t", "10u", "10v", "msl", "z", "slt", "lsm"]:
downloads[download_path / date.strftime(f"surf_{v}_%Y-%m-%d.grib")] = (
f"https://data.rda.ucar.edu/ds113.1/"
f"ec.oper.an.sfc/{date.year}{date.month:02d}/ec.oper.an.sfc.128_{var_nums[v]}_{v}."
f"regn1280sc.{date.year}{date.month:02d}{date.day:02d}.grb"
)
for v in ["z", "t", "u", "v", "q"]:
for hour in [0, 6, 12, 18]:
prefix = "uv" if v in {"u", "v"} else "sc"
downloads[download_path / date.strftime(f"atmos_{v}_%Y-%m-%d_{hour:02d}.grib")] = (
f"https://data.rda.ucar.edu/ds113.1/"
f"ec.oper.an.pl/{date.year}{date.month:02d}/ec.oper.an.pl.128_{var_nums[v]}_{v}."
f"regn1280{prefix}.{date.year}{date.month:02d}{date.day:02d}{hour:02d}.grb"
)
return downloads, download_path
def download_data(downloads):
"""Скачивает файлы, если они отсутствуют в целевой директории."""
for target, source in downloads.items():
if not target.exists():
print(f"Downloading {source}")
target.parent.mkdir(parents=True, exist_ok=True)
response = requests.get(source)
response.raise_for_status()
with open(target, "wb") as f:
f.write(response.content)
print("Downloads finished!")
def load_surf(v, v_in_file, download_path, date):
"""Загружает переменные поверхностного уровня или статические переменные."""
ds = xr.open_dataset(download_path / date.strftime(f"surf_{v}_%Y-%m-%d.grib"), engine="cfgrib")
data = ds[v_in_file].values[:2]
data = data[None]
return torch.from_numpy(data)
def load_atmos(v, download_path, date, levels):
"""Загружает атмосферные переменные для заданных уровней давления."""
ds_00 = xr.open_dataset(
download_path / date.strftime(f"atmos_{v}_%Y-%m-%d_00.grib"), engine="cfgrib"
)
ds_06 = xr.open_dataset(
download_path / date.strftime(f"atmos_{v}_%Y-%m-%d_06.grib"), engine="cfgrib"
)
ds_00 = ds_00[v].sel(isobaricInhPa=list(levels))
ds_06 = ds_06[v].sel(isobaricInhPa=list(levels))
data = np.stack((ds_00.values, ds_06.values), axis=0)
data = data[None]
return torch.from_numpy(data)
def create_batch(date, levels, downloads, download_path):
"""Создает объект Batch с данными для модели."""
ds = xr.open_dataset(next(iter(downloads.keys())), engine="cfgrib")
batch = Batch(
surf_vars={
"2t": load_surf("2t", "t2m", download_path, date),
"10u": load_surf("10u", "u10", download_path, date),
"10v": load_surf("10v", "v10", download_path, date),
"msl": load_surf("msl", "msl", download_path, date),
},
static_vars={
"z": load_surf("z", "z", download_path, date)[0, 0],
"slt": load_surf("slt", "slt", download_path, date)[0, 0],
"lsm": load_surf("lsm", "lsm", download_path, date)[0, 0],
},
atmos_vars={
"t": load_atmos("t", download_path, date, levels),
"u": load_atmos("u", download_path, date, levels),
"v": load_atmos("v", download_path, date, levels),
"q": load_atmos("q", download_path, date, levels),
"z": load_atmos("z", download_path, date, levels),
},
metadata=Metadata(
lat=torch.from_numpy(ds.latitude.values),
lon=torch.from_numpy(ds.longitude.values),
time=(date.replace(hour=6),),
atmos_levels=levels,
),
)
return batch.regrid(res=0.1)
def create_batch_random(levels: tuple[int], date: tuple):
"""Создает объект Batch с рандомными данными для модели."""
return Batch(
surf_vars={k: torch.randn(1, 2, 17, 32) for k in ("2t", "10u", "10v", "msl")},
static_vars={k: torch.randn(17, 32) for k in ("lsm", "z", "slt")},
atmos_vars={k: torch.randn(1, 2, 4, 17, 32) for k in ("z", "u", "v", "t", "q")},
metadata=Metadata(
lat=torch.linspace(90, -90, 17),
lon=torch.linspace(0, 360, 32 + 1)[:-1],
time=date,
atmos_levels=levels,
),
)
def run_model(batch):
"""Инициализирует модель AuroraSmall и выполняет предсказание."""
model = AuroraSmall()
model.load_checkpoint("microsoft/aurora", "aurora-0.25-small-pretrained.ckpt")
model.eval()
model = model.to("cpu")
with torch.inference_mode():
prediction = model.forward(batch)
return prediction
def get_wind_speed_and_direction(prediction, batch: Batch, lat: float, lon: float):
target_lat = lat
target_lon = lon
lat_idx = torch.abs(batch.metadata.lat - target_lat).argmin()
lon_idx = torch.abs(batch.metadata.lon - target_lon).argmin()
u_values = prediction.atmos_vars["u"][:, :, :, lat_idx, lon_idx]
v_values = prediction.atmos_vars["v"][:, :, :, lat_idx, lon_idx]
wind_speeds=[]
wind_directions=[]
for i in range(u_values.numel()):
u_scalar = u_values.view(-1)[i].item() # Разворачиваем тензор в одномерный и берем элемент
v_scalar = v_values.view(-1)[i].item()
print("u value:", u_scalar)
print("v value:", v_scalar)
u_with_units = u_scalar * units("m/s")
v_with_units = v_scalar * units("m/s")
# Рассчитайте направление и скорость ветра
wind_dir = metpy.calc.wind_direction(u_with_units, v_with_units)
wind_speed = metpy.calc.wind_speed(u_with_units, v_with_units)
wind_speeds.append(wind_speed.magnitude.item())
wind_directions.append(wind_dir.magnitude.item())
return wind_speeds,wind_directions
def wind_direction_to_text(wind_dir_deg):
directions = [
"север", "северо-восток", "восток", "юго-восток",
"юг", "юго-запад", "запад", "северо-запад"
]
idx = int((wind_dir_deg + 22.5) // 45) % 8
return directions[idx]
def get_weather_predict(
dates: tuple[datetime],
latitude: float,
longitude: float,
):
levels = (100,)
batch_actual = create_batch_random(levels, dates)
prediction_actual = run_model(batch_actual)
return get_wind_speed_and_direction(prediction_actual, batch_actual, latitude, longitude)
def main():
levels = (100,)
date1 = datetime(2024, 11, 27, 12)
date2 = datetime(2024, 11, 28, 12)
date_tuple = (date1, date2,)
# downloads, download_path = get_download_paths(date)
# download_data(downloads) # Скачиваем данные, если их нет
# batch_actual = create_batch(date, levels, downloads, download_path)
batch_actual = create_batch_random(levels, date_tuple)
prediction_actual = run_model(batch_actual)
wind_speed_and_direction = get_wind_speed_and_direction(prediction_actual, batch_actual, 50, 20)
return wind_speed_and_direction
if __name__ == "__main__":
main()
print("Prediction completed!")

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 13 13" style="enable-background:new 0 0 13 13;" xml:space="preserve">
<path d="M9.5,6H7V3.5C7,3.22,6.78,3,6.5,3S6,3.22,6,3.5V6H3.5C3.22,6,3,6.22,3,6.5S3.22,7,3.5,7H6v2.5C6,9.78,6.22,10,6.5,10
S7,9.78,7,9.5V7h2.5C9.78,7,10,6.78,10,6.5S9.78,6,9.5,6z"/>
</svg>

After

Width:  |  Height:  |  Size: 541 B

View File

@ -0,0 +1,16 @@
import { FlorisPlot } from './types';
export const FLORIS_ENDPOINTS = {
getWindmillData: 'api/floris/get_windmill_data',
};
export const FLORIS_PLOTS: Record<string, FlorisPlot> = {
horizontalPlane: {
name: 'horizontal_plane',
label: 'Horizontal Plane',
},
verticalPlane: {
name: 'vertical_plane',
label: 'Vertical Plane',
},
};

View File

@ -0,0 +1,2 @@
export * from './constants';
export * from './service';

View File

@ -0,0 +1,14 @@
import { api } from '@api/api';
import { FlorisFormValues } from '@components/ux/floris-form/types';
import { FLORIS_ENDPOINTS } from './constants';
import { WindmillData } from './types';
import { getWindmillDataRequestParams } from './utils';
export const getWindmillData = (formValues: Partial<FlorisFormValues>) => {
const { park } = formValues;
const params = getWindmillDataRequestParams(formValues);
const parkPath = park ? `/${park.id}/` : '';
const url = `${FLORIS_ENDPOINTS.getWindmillData}${parkPath}?${params}`;
return api.get<WindmillData>(url);
};

View File

@ -0,0 +1,9 @@
export type FlorisPlot = {
name: string;
label: string;
};
export type WindmillData = {
data: number[][];
fileName: Record<string, string>;
};

View File

@ -0,0 +1,29 @@
import { FlorisFormValues } from '@components/ux/floris-form/types';
import { FLORIS_PLOTS } from './constants';
export const getWindmillDataRequestParams = (
formValues: Partial<FlorisFormValues>,
) => {
let params = '';
if (formValues.turbines) {
const layoutX = formValues.turbines
?.map((row) => `layout_x=${row.x}`)
.join('&');
const layoutY = formValues.turbines
?.map((row) => `layout_y=${row.y}`)
.join('&');
const yawAngle = formValues.turbines
?.map((row) => `yaw_angle=${row.angle}`)
.join('&');
params += `${layoutX}&${layoutY}&${yawAngle}`;
}
const plots = Object.values(FLORIS_PLOTS)
.filter((_, i) => formValues.plots?.[i])
.map((p) => `plots=${p.name}`)
.join('&');
const dateStart = `date_start=${formValues.dateFrom?.substring(0, 10)}`;
const dateEnd = `date_end=${formValues.dateTo?.substring(0, 10)}`;
params += `&${plots}&${dateStart}&${dateEnd}`;
return params;
};

View File

@ -8,6 +8,7 @@ import {
TurbineTypePage, TurbineTypePage,
TurbineTypesPage, TurbineTypesPage,
} from '@components/pages'; } from '@components/pages';
import { FlorisPage } from '@components/pages/floris-page/component';
import { ROUTES } from '@utils/route'; import { ROUTES } from '@utils/route';
import React from 'react'; import React from 'react';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
@ -24,6 +25,7 @@ export function App() {
<Route path={ROUTES.turbineType.path} element={<TurbineTypePage />} /> <Route path={ROUTES.turbineType.path} element={<TurbineTypePage />} />
<Route path={ROUTES.parks.path} element={<ParksPage />} /> <Route path={ROUTES.parks.path} element={<ParksPage />} />
<Route path={ROUTES.park.path} element={<ParkPage />} /> <Route path={ROUTES.park.path} element={<ParkPage />} />
<Route path={ROUTES.floris.path} element={<FlorisPage />} />
</Route> </Route>
<Route <Route
path="*" path="*"

View File

@ -0,0 +1,32 @@
import { WindmillData } from '@api/floris/types';
import { Heading } from '@components/ui';
import { FlorisForm, FlorisPlots, PowerSection } from '@components/ux';
import { useRoute } from '@utils/route';
import React, { useState } from 'react';
import styles from './styles.module.scss';
export function FlorisPage() {
const [data, setData] = useState<WindmillData>(null);
const [dateFrom, setDateFrom] = useState<string>(null);
const route = useRoute();
const handleFormSuccess = (data: WindmillData, dateFrom: string) => {
setData(data);
console.log(data);
setDateFrom(dateFrom);
};
return (
<div className={styles.page}>
<Heading tag="h1">{route.title}</Heading>
<FlorisForm onSuccess={handleFormSuccess} onFail={() => {}} />
{data && (
<>
<PowerSection power={data.data} dateFrom={dateFrom} />
<FlorisPlots filenames={data.fileName} />
</>
)}
</div>
);
}

View File

@ -0,0 +1,7 @@
.page {
display: grid;
padding: 40px 20px;
gap: 30px;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto auto auto auto 1fr;
}

View File

@ -8,8 +8,8 @@ import { CheckboxGroupProps } from './types';
export function CheckboxGroup<T>({ export function CheckboxGroup<T>({
name, name,
value,
items, items,
value = items.map(() => false),
onChange, onChange,
getItemKey, getItemKey,
getItemLabel, getItemLabel,
@ -19,7 +19,7 @@ export function CheckboxGroup<T>({
const classNames = clsx(styles.checkBoxGroup, styles[scale]); const classNames = clsx(styles.checkBoxGroup, styles[scale]);
const handleChange = (index: number) => { const handleChange = (index: number) => {
onChange(value.with(index, !value[index])); onChange?.(value.with(index, !value[index]));
}; };
return ( return (

View File

@ -2,9 +2,9 @@ import { Scale } from '../types';
export type CheckboxGroupProps<T> = { export type CheckboxGroupProps<T> = {
name: string; name: string;
value: boolean[]; value?: boolean[];
items: T[]; items: T[];
onChange: (value: boolean[]) => void; onChange?: (value: boolean[]) => void;
getItemKey: (item: T) => React.Key; getItemKey: (item: T) => React.Key;
getItemLabel: (item: T) => string; getItemLabel: (item: T) => string;
scale?: Scale; scale?: Scale;

View File

@ -0,0 +1,144 @@
import { FLORIS_PLOTS, getWindmillData } from '@api/floris';
import { getParks, Park } from '@api/wind';
import {
Autocomplete,
Button,
Checkbox,
CheckboxGroup,
DateInput,
Heading,
} from '@components/ui';
import { Controller, useForm } from '@utils/form';
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import { FlorisTable } from '../floris-table/component';
import styles from './styles.module.scss';
import { FlorisFormProps, FlorisFormValues } from './types';
export function FlorisForm({
onSuccess,
onFail,
className,
...props
}: FlorisFormProps) {
const [pending, setPending] = useState<boolean>(false);
const [parks, setParks] = useState<Park[]>([]);
const [isManualEntry, setIsManualEntry] = useState<boolean>(false);
const { control, reset, getValues } = useForm<FlorisFormValues>({});
const fetchParks = async () => {
const res = await getParks();
setParks(res.data);
};
useEffect(() => {
fetchParks();
}, []);
const validate = (values: Partial<FlorisFormValues>) => {
console.log(values);
return true;
};
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
const values = getValues();
if (!validate(values)) {
return;
}
setPending(true);
const { data, error } = await getWindmillData(values);
if (data) {
onSuccess(data, values.dateFrom);
} else {
onFail(error.message);
}
setPending(false);
};
const handleResetButtonClick = () => {
reset({});
};
const handleManulEntryCheckboxChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const { checked } = event.target;
setIsManualEntry(checked);
if (checked) {
reset({ ...getValues(), park: undefined });
} else {
reset({ ...getValues(), turbines: undefined });
}
};
return (
<form
onSubmit={handleSubmit}
className={clsx(className, styles.form)}
{...props}
>
<Heading tag="h3">Turbines properties</Heading>
<div className={styles.content}>
<div className={styles.part}>
<div className={styles.dateRangeBox}>
<Controller
{...control('dateFrom')}
render={(params) => <DateInput placeholder="from" {...params} />}
/>
<Controller
{...control('dateTo')}
render={(params) => <DateInput placeholder="to" {...params} />}
/>
</div>
<Checkbox
label={{ text: 'Manual entry', position: 'right' }}
onChange={handleManulEntryCheckboxChange}
/>
{isManualEntry && (
<Controller
{...control('turbines')}
render={(params) => <FlorisTable {...params} />}
/>
)}
{!isManualEntry && (
<Controller
{...control('park')}
render={(params) => (
<Autocomplete
options={parks}
getOptionKey={(p) => p.id}
getOptionLabel={(p) => p.name}
{...params}
/>
)}
/>
)}
</div>
<div>
<Controller
{...control('plots')}
render={(params) => (
<CheckboxGroup
items={Object.values(FLORIS_PLOTS)}
getItemKey={(i) => i.name}
getItemLabel={(i) => i.label}
label="Plots"
{...params}
/>
)}
/>
</div>
</div>
<div className={styles.buttonBox}>
<Button type="submit" pending={pending}>
Submit
</Button>
<Button variant="secondary" onClick={handleResetButtonClick}>
Reset
</Button>
</div>
</form>
);
}

View File

@ -0,0 +1 @@
export * from './component';

View File

@ -0,0 +1,35 @@
.form {
display: grid;
padding: 20px;
border-radius: 15px;
background-color: var(--clr-layer-200);
box-shadow: 0px 1px 2px var(--clr-shadow-100);
gap: 20px;
& > * {
width: 100%;
}
}
.content {
display: grid;
gap: 30px;
grid-template-columns: 3fr 2fr;
}
.part {
display: grid;
gap: 10px;
}
.dateRangeBox {
display: grid;
gap: 10px;
grid-template-columns: 1fr 1fr;
}
.buttonBox {
display: flex;
justify-content: end;
gap: 10px;
}

View File

@ -0,0 +1,17 @@
import { WindmillData } from '@api/floris/types';
import { Park } from '@api/wind';
import { FlorisTableTurbine } from '../floris-table/types';
export type FlorisFormValues = {
dateFrom: string;
dateTo: string;
turbines: FlorisTableTurbine[];
plots: boolean[];
park: Park;
};
export type FlorisFormProps = {
onSuccess: (response: WindmillData, dateFrom: string) => void;
onFail: (message: string) => void;
} & React.ComponentProps<'form'>;

View File

@ -0,0 +1,24 @@
import { BASE_URL } from '@api/constants';
import { FLORIS_PLOTS } from '@api/floris';
import { Heading, Span } from '@components/ui';
import React from 'react';
import styles from './styles.module.scss';
import { FlorisPlotsProps } from './types';
export function FlorisPlots({ filenames }: FlorisPlotsProps) {
return (
<div className={styles.plots}>
<Heading tag="h3">Plots</Heading>
{Object?.keys(filenames).map((key) => {
const url = `${BASE_URL}/api/floris/download_image/${filenames[key]}`;
return (
<div className={styles.plot}>
<Span>{FLORIS_PLOTS[key]?.label ?? '???'}</Span>
<img src={url} className={styles.image} alt="Plot" />
</div>
);
})}
</div>
);
}

View File

@ -0,0 +1 @@
export * from './component';

View File

@ -0,0 +1,21 @@
.plots {
display: flex;
flex-direction: column;
padding: 20px;
border-radius: 15px;
background-color: var(--clr-layer-200);
box-shadow: 0px 1px 2px var(--clr-shadow-100);
gap: 20px;
}
.plot {
display: grid;
gap: 10px;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto 1fr;
}
.image {
width: 100%;
border-radius: 10px;
}

View File

@ -0,0 +1,3 @@
export type FlorisPlotsProps = {
filenames: Record<string, string>;
};

View File

@ -0,0 +1,58 @@
import { IconButton, Span } from '@components/ui';
import DeleteIcon from '@public/images/svg/delete.svg';
import PlusIcon from '@public/images/svg/plus.svg';
import React, { useState } from 'react';
import { FlorisTableRow } from './components';
import styles from './styles.module.scss';
import { FlorisTableProps, FlorisTableTurbine } from './types';
export function FlorisTable({ value = [], onChange }: FlorisTableProps) {
const [selectedRows, setSelectedRows] = useState<Record<string, boolean>>({});
const handleDeleteButtonClick = () => {
onChange?.(value.filter((_, i) => !selectedRows[i]));
setSelectedRows({});
};
const handlePlusButtonClick = () => {
onChange?.([...value, { x: '', y: '', angle: '' }]);
};
const handleRowChange = (index: number, turbine: FlorisTableTurbine) => {
onChange?.(value.with(index, turbine));
};
const handleRowSelect = (index: number) => {
const checked = !selectedRows[index];
setSelectedRows({ ...selectedRows, [index]: checked });
};
return (
<div className={styles.table}>
<header className={styles.header}>
<Span className={styles.span} />
<Span className={styles.span}>x</Span>
<Span className={styles.span}>y</Span>
<Span className={styles.span}>angle</Span>
</header>
{value.map((v, i) => (
<FlorisTableRow
key={i}
value={v}
onChange={(turbine) => handleRowChange(i, turbine)}
onSelect={() => handleRowSelect(i)}
selected={selectedRows[i] ?? false}
/>
))}
<footer className={styles.footer}>
<IconButton onClick={handleDeleteButtonClick}>
<DeleteIcon />
</IconButton>
<IconButton onClick={handlePlusButtonClick}>
<PlusIcon />
</IconButton>
</footer>
</div>
);
}

View File

@ -0,0 +1,42 @@
import { Checkbox, NumberInput } from '@components/ui';
import React from 'react';
import { FlorisTableTurbine } from '../../types';
import styles from './styles.module.scss';
import { FlorisTableRowProps } from './types';
export function FlorisTableRow({
value,
onChange,
onSelect,
selected,
}: FlorisTableRowProps) {
const handleChange = (number: string, key: keyof FlorisTableTurbine) => {
onChange({ ...value, [key]: number });
};
return (
<div className={styles.row}>
<Checkbox
label={{ className: styles.checkboxLabel }}
onChange={onSelect}
checked={selected}
/>
<NumberInput
value={value.x}
onChange={(number) => handleChange(number, 'x')}
wrapper={{ className: styles.textInput }}
/>
<NumberInput
value={value.y}
onChange={(number) => handleChange(number, 'y')}
wrapper={{ className: styles.textInput }}
/>
<NumberInput
value={value.angle}
onChange={(number) => handleChange(number, 'angle')}
wrapper={{ className: styles.textInput }}
/>
</div>
);
}

View File

@ -0,0 +1 @@
export * from './component';

View File

@ -0,0 +1,16 @@
.row {
display: grid;
grid-template-columns: auto 1fr 1fr 1fr;
}
.checkboxLabel {
width: 46px;
justify-content: center;
border: 1px solid var(--clr-border-200);
}
.textInput {
border-radius: 0;
background-color: var(--clr-layer-200);
box-shadow: none;
}

View File

@ -0,0 +1,8 @@
import { FlorisTableTurbine } from '../../types';
export type FlorisTableRowProps = {
value: FlorisTableTurbine;
onChange: (value: FlorisTableTurbine) => void;
onSelect: () => void;
selected: boolean;
};

View File

@ -0,0 +1 @@
export * from './floris-table-row';

View File

@ -0,0 +1,32 @@
.table {
border-radius: 10px;
background-color: var(--clr-layer-200);
box-shadow: 0px 2px 2px var(--clr-shadow-100);
}
.header {
display: grid;
grid-template-columns: 46px 1fr 1fr 1fr;
}
.span {
padding: 13px;
border: 1px solid var(--clr-border-200);
background-color: var(--clr-layer-300);
text-align: center;
&:first-of-type {
border-top-left-radius: 10px;
}
&:last-of-type {
border-top-right-radius: 10px;
}
}
.footer {
padding: 5px;
border: 1px solid var(--clr-border-200);
border-radius: 0 0 10px 10px;
background-color: var(--clr-layer-300);
}

View File

@ -0,0 +1,10 @@
export type FlorisTableTurbine = {
x: string;
y: string;
angle: string;
};
export type FlorisTableProps = {
value?: FlorisTableTurbine[];
onChange?: (value: FlorisTableTurbine[]) => void;
};

View File

@ -1,5 +1,8 @@
export { FlorisForm } from './floris-form';
export { FlorisPlots } from './floris-plots';
export { Header } from './header'; export { Header } from './header';
export { ParkTurbineTable } from './park-turbine-table'; export { ParkTurbineTable } from './park-turbine-table';
export { ParkTurbines } from './park-turbines'; export { ParkTurbines } from './park-turbines';
export { PowerSection } from './power-section';
export { Sidebar } from './sidebar'; export { Sidebar } from './sidebar';
export { ThemeSelect } from './theme-select'; export { ThemeSelect } from './theme-select';

View File

@ -3,4 +3,5 @@ import { ROUTES } from '@utils/route';
export const NAVIGATION_LINKS = [ export const NAVIGATION_LINKS = [
{ path: ROUTES.turbineTypes.path, title: ROUTES.turbineTypes.title }, { path: ROUTES.turbineTypes.path, title: ROUTES.turbineTypes.title },
{ path: ROUTES.parks.path, title: ROUTES.parks.title }, { path: ROUTES.parks.path, title: ROUTES.parks.title },
{ path: ROUTES.floris.path, title: ROUTES.floris.title },
]; ];

View File

@ -0,0 +1,46 @@
import { Heading, Span } from '@components/ui';
import clsx from 'clsx';
import React from 'react';
import styles from './style.module.scss';
import { PowerSectionProps } from './types';
export function PowerSection({ power, dateFrom }: PowerSectionProps) {
const gridTemplateColumns = `repeat(${power[0].length + 1}, 1fr)`;
const date = new Date(dateFrom);
return (
<section className={styles.section}>
<Heading tag="h3">Power, watt per hour</Heading>
<div>
<div className={styles.row} style={{ gridTemplateColumns }}>
<Span className={clsx(styles.cell, styles.mainCell)}></Span>
{power[0].map((_, i) => (
<Span className={clsx(styles.cell, styles.mainCell)} key={i}>
{i + 1}
</Span>
))}
</div>
{power.map((row, r) => {
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const dateStr = `${day}.${month}.${year}`;
date.setDate(date.getDate() + 1);
return (
<div className={styles.row} style={{ gridTemplateColumns }} key={r}>
<Span className={clsx(styles.cell, styles.mainCell)}>
{dateStr}
</Span>
{row.map((value, c) => (
<Span className={styles.cell} color="t300" key={c}>
{value}
</Span>
))}
</div>
);
})}
</div>
</section>
);
}

View File

@ -0,0 +1 @@
export * from './component';

View File

@ -0,0 +1,43 @@
.section {
display: grid;
padding: 20px;
border-radius: 15px;
background-color: var(--clr-layer-200);
box-shadow: 0px 1px 2px var(--clr-shadow-100);
gap: 20px;
}
.row {
display: grid;
&:first-child {
.cell {
&:first-of-type {
border-top-left-radius: 10px;
}
&:last-of-type {
border-top-right-radius: 10px;
}
}
}
&:last-child {
.cell {
&:first-of-type {
border-bottom-left-radius: 10px;
}
&:last-of-type {
border-bottom-right-radius: 10px;
}
}
}
}
.cell {
padding: 10px;
border: 1px solid var(--clr-border-200);
}
.mainCell {
background-color: var(--clr-layer-300);
}

View File

@ -0,0 +1,4 @@
export type PowerSectionProps = {
power: number[][];
dateFrom: string;
};

View File

@ -7,6 +7,7 @@ export const ROUTES: Record<AppRouteName, AppRoute> = {
turbineType: { path: '/turbine-types/:id', title: 'Turbine Type' }, turbineType: { path: '/turbine-types/:id', title: 'Turbine Type' },
parks: { path: '/parks', title: 'Parks' }, parks: { path: '/parks', title: 'Parks' },
park: { path: '/parks/:id', title: 'Park' }, park: { path: '/parks/:id', title: 'Park' },
floris: { path: '/floris', title: 'Floris' },
}; };
export const routeArray = Object.values(ROUTES); export const routeArray = Object.values(ROUTES);

View File

@ -1,4 +1,9 @@
export type AppRouteName = 'turbineTypes' | 'turbineType' | 'parks' | 'park'; export type AppRouteName =
| 'turbineTypes'
| 'turbineType'
| 'parks'
| 'park'
| 'floris';
export type AppRoute = { export type AppRoute = {
path: string; path: string;

View File

@ -26,8 +26,8 @@ class OpenMeteoClient:
return responses return responses
def process_response(self, response): def process_response(self, response):
# Process hourly data
daily = response.Daily() daily = response.Daily()
daily_wind_speed_10m = daily.Variables(0).ValuesAsNumpy() daily_wind_speed_10m = daily.Variables(0).ValuesAsNumpy()
daily_wind_direction_10m = daily.Variables(1).ValuesAsNumpy() daily_wind_direction_10m = daily.Variables(1).ValuesAsNumpy()
@ -36,5 +36,4 @@ class OpenMeteoClient:
def get_weather_info(self, start_date, end_date, latitude=54.35119762746125, longitude=48.389356992149345): def get_weather_info(self, start_date, end_date, latitude=54.35119762746125, longitude=48.389356992149345):
responses = self.fetch_weather_data(latitude, longitude, start_date, end_date) responses = self.fetch_weather_data(latitude, longitude, start_date, end_date)
response = responses[0] response = responses[0]
self.process_response(response)
return self.process_response(response) return self.process_response(response)

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Some files were not shown because too many files have changed in this diff Show More