Skip to content

Commit

Permalink
Showing 15 changed files with 288 additions and 249 deletions.
4 changes: 2 additions & 2 deletions antarest/__init__.py
Original file line number Diff line number Diff line change
@@ -7,9 +7,9 @@

# Standard project metadata

__version__ = "2.15.0"
__version__ = "2.15.1"
__author__ = "RTE, Antares Web Team"
__date__ = "2023-09-30"
__date__ = "2023-10-05"
# noinspection SpellCheckingInspection
__credits__ = "(c) Réseau de Transport de l’Électricité (RTE)"

13 changes: 12 additions & 1 deletion antarest/study/web/raw_studies_blueprint.py
Original file line number Diff line number Diff line change
@@ -136,7 +136,18 @@ def get_study(
# because it's better to avoid raising an exception.
return Response(content=output, media_type="application/octet-stream")

return JSONResponse(content=output)
# We want to allow `NaN`, `+Infinity`, and `-Infinity` values in the JSON response
# even though they are not standard JSON values because they are supported in JavaScript.
# Additionally, we cannot use `orjson` because, despite its superior performance, it converts
# `NaN` and other values to `null`, even when using a custom encoder.
json_response = json.dumps(
output,
ensure_ascii=False,
allow_nan=True,
indent=None,
separators=(",", ":"),
).encode("utf-8")
return Response(content=json_response, media_type="application/json")

@bp.post(
"/studies/{uuid}/raw",
23 changes: 23 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
Antares Web Changelog
=====================

v2.15.1 (2023-10-05)
--------------------

### Features

* **ui-results:** move filters on top of matrices [`#1751`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1751)
* **ui-outputs:** add outputs synthesis view [`#1737`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1737)


### Bug Fixes

* **api:** allow `NaN`, `+Infinity`, and `-Infinity` values in JSON response [`7394248`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/7394248821ad5e2e8e5b51d389896c745740225d)
* **ui-xpansion:** display issue in form [`#1754`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1754)
* **raw:** impossible to see matrix containing NaN values [`#1714`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1714)
* **raw:** allow NaN in matrices [`0cad1a9`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/0cad1a969fd14e81cf502aecb821df4b2d7abcb6)


### Tests

* **tests:** add a test to ensure GET `/raw` endpoint reads NaN values [`29b1f71`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/29b1f71856463542dcc0170fe97bc6832ec4a72a)



v2.15.0 (2023-09-30)
--------------------

2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@

setup(
name="AntaREST",
version="2.15.0",
version="2.15.1",
description="Antares Server",
long_description=Path("README.md").read_text(encoding="utf-8"),
long_description_content_type="text/markdown",
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
@@ -6,5 +6,5 @@ sonar.exclusions=antarest/gui.py,antarest/main.py
sonar.python.coverage.reportPaths=coverage.xml
sonar.python.version=3.8
sonar.javascript.lcov.reportPaths=webapp/coverage/lcov.info
sonar.projectVersion=2.15.0
sonar.projectVersion=2.15.1
sonar.coverage.exclusions=antarest/gui.py,antarest/main.py,antarest/singleton_services.py,antarest/worker/archive_worker_service.py,webapp/**/*
18 changes: 14 additions & 4 deletions tests/integration/raw_studies_blueprint/test_fetch_raw_data.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
import shutil
from urllib.parse import urlencode

import numpy as np
import pytest
from starlette.testclient import TestClient

@@ -41,6 +42,7 @@ def test_get_study(
with db():
study: RawStudy = db.session.get(Study, study_id)
study_dir = pathlib.Path(study.path)
headers = {"Authorization": f"Bearer {user_access_token}"}

shutil.copytree(
ASSETS_DIR.joinpath("user"),
@@ -55,7 +57,7 @@ def test_get_study(
query_string = urlencode({"path": f"/{rel_path}", "depth": 1})
res = client.get(
f"/v1/studies/{study_id}/raw?{query_string}",
headers={"Authorization": f"Bearer {user_access_token}"},
headers=headers,
)
res.raise_for_status()
if file_path.suffix == ".json":
@@ -81,7 +83,7 @@ def test_get_study(
query_string = urlencode({"path": f"/{rel_path.as_posix()}", "depth": 1})
res = client.get(
f"/v1/studies/{study_id}/raw?{query_string}",
headers={"Authorization": f"Bearer {user_access_token}"},
headers=headers,
)
res.raise_for_status()
actual = res.content
@@ -95,7 +97,7 @@ def test_get_study(
query_string = urlencode({"path": f"/{rel_path.as_posix()}", "depth": 1})
res = client.get(
f"/v1/studies/{study_id}/raw?{query_string}",
headers={"Authorization": f"Bearer {user_access_token}"},
headers=headers,
)
assert res.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY

@@ -104,7 +106,15 @@ def test_get_study(
query_string = urlencode({"path": "/input/areas/list", "depth": 1})
res = client.get(
f"/v1/studies/{study_id}/raw?{query_string}",
headers={"Authorization": f"Bearer {user_access_token}"},
headers=headers,
)
res.raise_for_status()
assert res.json() == ["DE", "ES", "FR", "IT"]

# asserts that the GET /raw endpoint is able to read matrix containing NaN values
res = client.get(
f"/v1/studies/{study_id}/raw?path=output/20201014-1427eco/economy/mc-all/areas/de/id-monthly",
headers=headers,
)
assert res.status_code == 200
assert np.isnan(res.json()["data"][0]).any()
8 changes: 2 additions & 6 deletions tests/integration/test_integration_xpansion.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import io
from pathlib import Path

from fastapi import FastAPI
from starlette.testclient import TestClient

from antarest.study.business.area_management import AreaType
from antarest.study.business.xpansion_management import XpansionCandidateDTO


def test_integration_xpansion(app: FastAPI, tmp_path: Path):
client = TestClient(app, raise_server_exceptions=False)
res = client.post("/v1/login", json={"username": "admin", "password": "admin"})
admin_credentials = res.json()
headers = {"Authorization": f'Bearer {admin_credentials["access_token"]}'}
def test_integration_xpansion(client: TestClient, tmp_path: Path, admin_access_token: str):
headers = {"Authorization": f"Bearer {admin_access_token}"}

created = client.post(
"/v1/studies?name=foo",
4 changes: 2 additions & 2 deletions webapp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "antares-web",
"version": "2.15.0",
"version": "2.15.1",
"private": true,
"engines": {
"node": "18.16.1"
1 change: 0 additions & 1 deletion webapp/public/locales/en/main.json
Original file line number Diff line number Diff line change
@@ -479,7 +479,6 @@
"study.modelization.bindingConst.question.deleteConstraintTerm": "Are you sure you want to delete this constraint term?",
"study.modelization.bindingConst.question.deleteBindingConstraint": "Are you sure you want to delete this binding constraint?",
"study.results.mc": "Monte-Carlo",
"study.results.mc.year": "Year",
"study.results.display": "Display",
"study.results.temporality": "Temporality",
"study.results.noData": "No data available",
5 changes: 2 additions & 3 deletions webapp/public/locales/fr/main.json
Original file line number Diff line number Diff line change
@@ -110,7 +110,7 @@
"form.submit.inProgress": "Le formulaire est en cours de soumission. Etes-vous sûr de vouloir quitter la page ?",
"form.asyncDefaultValues.error": "Impossible d'obtenir les valeurs",
"form.field.required": "Champ requis",
"form.field.duplicate": "Cette valeur existe déjà: {{0}}",
"form.field.duplicate": "Cette valeur existe déjà: {{0}}",
"form.field.minLength": "{{0}} caractère(s) minimum",
"form.field.minValue": "La valeur minimum est {{0}}",
"form.field.maxValue": "La valeur maximum est {{0}}",
@@ -198,7 +198,7 @@
"study.timeLimitHelper": "Limite de temps en heures (max: {{max}}h)",
"study.nbCpu": "Nombre de coeurs",
"study.clusterLoad": "Charge du cluster",
"study.synthesis": "Synthesis",
"study.synthesis": "Synthèse",
"study.level": "Niveau",
"study.years": "Années",
"study.type": "Type",
@@ -479,7 +479,6 @@
"study.modelization.bindingConst.question.deleteConstraintTerm": "Êtes-vous sûr de vouloir supprimer ce terme ?",
"study.modelization.bindingConst.question.deleteBindingConstraint": "Êtes-vous sûr de vouloir supprimer cette contrainte couplante ?",
"study.results.mc": "Monte-Carlo",
"study.results.mc.year": "année",
"study.results.display": "Affichage",
"study.results.temporality": "Temporalité",
"study.results.noData": "Aucune donnée disponible",

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Box,
Button,
Paper,
Skeleton,
ToggleButton,
ToggleButtonGroup,
@@ -31,14 +32,23 @@ import EditableMatrix from "../../../../../common/EditableMatrix";
import PropertiesView from "../../../../../common/PropertiesView";
import SplitLayoutView from "../../../../../common/SplitLayoutView";
import ListElement from "../../common/ListElement";
import SelectionDrawer, { SelectionDrawerProps } from "./SelectionDrawer";
import { createPath, DataType, OutputItemType, Timestep } from "./utils";
import {
createPath,
DataType,
MAX_YEAR,
OutputItemType,
SYNTHESIS_ITEMS,
Timestep,
} from "./utils";
import UsePromiseCond, {
mergeResponses,
} from "../../../../../common/utils/UsePromiseCond";
import useStudySynthesis from "../../../../../../redux/hooks/useStudySynthesis";
import { downloadMatrix } from "../../../../../../utils/matrixUtils";
import ButtonBack from "../../../../../common/ButtonBack";
import BooleanFE from "../../../../../common/fieldEditors/BooleanFE";
import SelectFE from "../../../../../common/fieldEditors/SelectFE";
import NumberFE from "../../../../../common/fieldEditors/NumberFE";

function ResultDetails() {
const { study } = useOutletContext<{ study: StudyMetadata }>();
@@ -53,10 +63,10 @@ function ResultDetails() {
const [dataType, setDataType] = useState(DataType.General);
const [timestep, setTimeStep] = useState(Timestep.Hourly);
const [year, setYear] = useState(-1);
const [showFilter, setShowFilter] = useState(false);
const [itemType, setItemType] = useState(OutputItemType.Areas);
const [selectedItemId, setSelectedItemId] = useState("");
const [searchValue, setSearchValue] = useState("");
const isSynthesis = itemType === OutputItemType.Synthesis;
const { t } = useTranslation();
const navigate = useNavigate();

@@ -67,15 +77,19 @@ function ResultDetails() {
) as Array<{ id: string; name: string; label?: string }>;

const filteredItems = useMemo(() => {
return items.filter((item) =>
isSearchMatching(searchValue, item.label || item.name)
);
}, [items, searchValue]);
return isSynthesis
? SYNTHESIS_ITEMS
: items.filter((item) =>
isSearchMatching(searchValue, item.label || item.name)
);
}, [isSynthesis, items, searchValue]);

const selectedItem = filteredItems.find(
(item) => item.id === selectedItemId
) as (Area & { id: string }) | LinkElement | undefined;

const maxYear = output?.nbyears ?? MAX_YEAR;

useEffect(
() => {
const isValidSelectedItem =
@@ -92,9 +106,9 @@ function ResultDetails() {

const matrixRes = usePromise<MatrixType | null>(
async () => {
if (output && selectedItem) {
if (output && selectedItem && !isSynthesis) {
const path = createPath({
output: { ...output, id: outputId as string },
output,
item: selectedItem,
dataType,
timestep,
@@ -116,7 +130,21 @@ function ResultDetails() {
{
resetDataOnReload: true,
resetErrorOnReload: true,
deps: [study.id, output, selectedItem],
deps: [study.id, output, selectedItem, dataType, timestep, year],
}
);

const { data: synthesis } = usePromise<string>(
async () => {
if (outputId && selectedItem && isSynthesis) {
const path = `output/${outputId}/economy/mc-all/grid/${selectedItem.id}`;
const res = await getStudyData(study.id, path);
return res;
}
return null;
},
{
deps: [study.id, outputId, selectedItem],
}
);

@@ -131,16 +159,6 @@ function ResultDetails() {
setItemType(value);
};

const handleSelection: SelectionDrawerProps["onSelection"] = ({
dataType,
timestep,
year,
}) => {
setDataType(dataType);
setTimeStep(timestep);
setYear(year);
};

const handleDownload = (matrixData: MatrixType, fileName: string): void => {
downloadMatrix(matrixData, fileName);
};
@@ -150,49 +168,72 @@ function ResultDetails() {
////////////////////////////////////////////////////////////////

return (
<>
<SplitLayoutView
left={
<PropertiesView
topContent={
<Box
sx={{
width: 1,
px: 1,
}}
<SplitLayoutView
left={
<PropertiesView
topContent={
<Box
sx={{
width: 1,
px: 1,
}}
>
<ButtonBack onClick={() => navigate("..")} />
</Box>
}
mainContent={
<>
<ToggleButtonGroup
sx={{ p: 1 }}
value={itemType}
exclusive
size="small"
orientation="vertical"
fullWidth
onChange={handleItemTypeChange}
>
<ButtonBack onClick={() => navigate("..")} />
</Box>
}
mainContent={
<>
<ToggleButtonGroup
sx={{ p: 1 }}
value={itemType}
exclusive
size="small"
fullWidth
onChange={handleItemTypeChange}
>
<ToggleButton value={OutputItemType.Areas}>
{t("study.areas")}
</ToggleButton>
<ToggleButton value={OutputItemType.Links}>
{t("study.links")}
</ToggleButton>
</ToggleButtonGroup>
<ListElement
list={filteredItems}
currentElement={selectedItemId}
currentElementKeyToTest="id"
setSelectedItem={(item) => setSelectedItemId(item.id)}
/>
</>
}
onSearchFilterChange={setSearchValue}
/>
}
right={
<ToggleButton value={OutputItemType.Areas}>
{t("study.areas")}
</ToggleButton>
<ToggleButton value={OutputItemType.Links}>
{t("study.links")}
</ToggleButton>
<ToggleButton value={OutputItemType.Synthesis}>
{t("study.synthesis")}
</ToggleButton>
</ToggleButtonGroup>
<ListElement
list={filteredItems}
currentElement={selectedItemId}
currentElementKeyToTest="id"
setSelectedItem={(item) => setSelectedItemId(item.id)}
/>
</>
}
onSearchFilterChange={setSearchValue}
/>
}
right={
isSynthesis ? (
<Box
sx={{
display: "flex",
flexDirection: "column",
height: 1,
width: 1,
overflow: "auto",
}}
>
<Paper
sx={{
p: 2,
overflow: "auto",
}}
>
<code style={{ whiteSpace: "pre" }}>{synthesis}</code>
</Paper>
</Box>
) : (
<Box
sx={{
display: "flex",
@@ -207,43 +248,110 @@ function ResultDetails() {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
gap: 4,
gap: 2,
flexWrap: "wrap",
}}
>
{[
{(
[
`${t("study.results.mc")}:`,
year > 0 ? `${t("study.results.mc.year")} ${year}` : "all",
],
[`${t("study.results.display")}:`, dataType],
[`${t("study.results.temporality")}:`, timestep],
].map(([label, value]) => (
<Box key={label}>
[
`${t("study.results.mc")}:`,
() => (
<>
<BooleanFE
value={year <= 0}
trueText="Synthesis"
falseText="Year by year"
size="small"
variant="outlined"
onChange={(event) => {
setYear(event?.target.value ? -1 : 1);
}}
/>
{year > 0 && (
<NumberFE
size="small"
variant="outlined"
value={year}
sx={{ m: 0, ml: 1, width: 80 }}
inputProps={{
min: 1,
max: maxYear,
}}
onChange={(event) => {
setYear(Number(event.target.value));
}}
/>
)}
</>
),
],
[
`${t("study.results.display")}:`,
() => (
<SelectFE
value={dataType}
options={[
{ value: DataType.General, label: "General values" },
{ value: DataType.Thermal, label: "Thermal plants" },
{ value: DataType.Renewable, label: "Ren. clusters" },
{ value: DataType.Record, label: "RecordYears" },
]}
size="small"
variant="outlined"
onChange={(event) => {
setDataType(event?.target.value as DataType);
}}
/>
),
],
[
`${t("study.results.temporality")}:`,
() => (
<SelectFE
value={timestep}
options={[
{ value: Timestep.Hourly, label: "Hourly" },
{ value: Timestep.Daily, label: "Daily" },
{ value: Timestep.Weekly, label: "Weekly" },
{ value: Timestep.Monthly, label: "Monthly" },
{ value: Timestep.Annual, label: "Annual" },
]}
size="small"
variant="outlined"
onChange={(event) => {
setTimeStep(event?.target.value as Timestep);
}}
/>
),
],
] as const
).map(([label, Field]) => (
<Box
key={label}
sx={{
display: "flex",
alignItems: "center",
}}
>
<Box component="span" sx={{ opacity: 0.7, mr: 1 }}>
{label}
</Box>
{value}
<Field />
</Box>
))}
<Button
variant="outlined"
onClick={() => setShowFilter(true)}
disabled={matrixRes.isLoading}
>
{t("global.change")}
</Button>

<Button
size="small"
title={t("global.download")}
variant="outlined"
color="primary"
startIcon={<DownloadOutlinedIcon />}
onClick={() =>
matrixRes.data &&
handleDownload(matrixRes.data, `matrix_${study.id}`)
}
disabled={matrixRes.isLoading}
>
{t("global.download")}
<DownloadOutlinedIcon />
</Button>
</Box>
<Box sx={{ flex: 1 }}>
@@ -286,16 +394,9 @@ function ResultDetails() {
/>
</Box>
</Box>
}
/>
<SelectionDrawer
open={showFilter}
onClose={() => setShowFilter(false)}
values={{ dataType, timestep, year }}
maxYear={output?.nbyears}
onSelection={handleSelection}
/>
</>
)
}
/>
);
}

Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import { Area, LinkElement, Simulation } from "../../../../../../common/types";
export enum OutputItemType {
Areas = "areas",
Links = "links",
Synthesis = "synthesis",
}

export enum DataType {
@@ -43,3 +44,26 @@ export function createPath(params: Params): string {

return `output/${id}/${mode}/${periodFolder}/${itemType}/${itemFolder}/${dataType}-${timestep}`;
}

export const SYNTHESIS_ITEMS = [
{
id: "areas",
name: "Areas",
label: "Areas synthesis",
},
{
id: "links",
name: "Links",
label: "Links synthesis",
},
{
id: "digest",
name: "Digest",
label: "Digest",
},
{
id: "thermal",
name: "Thermal",
label: "Thermal synthesis",
},
];
Original file line number Diff line number Diff line change
@@ -180,22 +180,22 @@ function SettingsForm(props: PropType) {
label={t("xpansion.solver")}
data={currentSettings.solver || ""}
handleChange={handleChange}
optional
sx={{
minWidth: "100%",
}}
optional
/>
<TextField
type="number"
label={t("xpansion.batchSize")}
variant="filled"
value={currentSettings.batch_size || ""}
onChange={(e) =>
handleChange("batch_size", parseInt(e.target.value, 10))
}
sx={{ mb: 1 }}
/>
</SelectFields>
<TextField
type="number"
label={t("xpansion.batchSize")}
variant="filled"
value={currentSettings.batch_size || ""}
onChange={(e) =>
handleChange("batch_size", parseInt(e.target.value, 10))
}
sx={{ mb: 1 }}
/>
<TextField
type="number"
label={t("xpansion.timeLimit")}

0 comments on commit 45ee182

Please sign in to comment.