diff --git a/antarest/__init__.py b/antarest/__init__.py
index bd53e7a23c..4023e2ca7b 100644
--- a/antarest/__init__.py
+++ b/antarest/__init__.py
@@ -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)"
diff --git a/antarest/study/web/raw_studies_blueprint.py b/antarest/study/web/raw_studies_blueprint.py
index a62a08b207..1105a5f74a 100644
--- a/antarest/study/web/raw_studies_blueprint.py
+++ b/antarest/study/web/raw_studies_blueprint.py
@@ -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",
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index cd2ae9ff87..e6d96180aa 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -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)
--------------------
diff --git a/setup.py b/setup.py
index 323d8deb7b..92e4e34f98 100644
--- a/setup.py
+++ b/setup.py
@@ -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",
diff --git a/sonar-project.properties b/sonar-project.properties
index 3fb85c7559..9bf66c1c14 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -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/**/*
\ No newline at end of file
diff --git a/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py b/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py
index 96366c6e44..7b5bc4e38e 100644
--- a/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py
+++ b/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py
@@ -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()
diff --git a/tests/integration/test_integration_xpansion.py b/tests/integration/test_integration_xpansion.py
index f4f4648605..0aa0579734 100644
--- a/tests/integration/test_integration_xpansion.py
+++ b/tests/integration/test_integration_xpansion.py
@@ -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",
diff --git a/webapp/package-lock.json b/webapp/package-lock.json
index af03a6bde3..9a4f7cd401 100644
--- a/webapp/package-lock.json
+++ b/webapp/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "antares-web",
- "version": "2.15.0",
+ "version": "2.15.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "antares-web",
- "version": "2.14.4",
+ "version": "2.15.1",
"dependencies": {
"@emotion/react": "11.10.6",
"@emotion/styled": "11.10.6",
diff --git a/webapp/package.json b/webapp/package.json
index 3a5251d7f8..74a8fd65f0 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -1,6 +1,6 @@
{
"name": "antares-web",
- "version": "2.15.0",
+ "version": "2.15.1",
"private": true,
"engines": {
"node": "18.16.1"
diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json
index f7651f99b1..e966640063 100644
--- a/webapp/public/locales/en/main.json
+++ b/webapp/public/locales/en/main.json
@@ -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",
diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json
index d60556d964..90a338597b 100644
--- a/webapp/public/locales/fr/main.json
+++ b/webapp/public/locales/fr/main.json
@@ -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",
diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/SelectionDrawer.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/SelectionDrawer.tsx
deleted file mode 100644
index da3aef3229..0000000000
--- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/SelectionDrawer.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import { Box, Button, Drawer, RadioGroup } from "@mui/material";
-import { useEffect, useState } from "react";
-import { useTranslation } from "react-i18next";
-import BooleanFE from "../../../../../common/fieldEditors/BooleanFE";
-import NumberFE from "../../../../../common/fieldEditors/NumberFE";
-import RadioFE from "../../../../../common/fieldEditors/RadioFE";
-import Fieldset from "../../../../../common/Fieldset";
-import { DataType, MAX_YEAR, Timestep } from "./utils";
-
-export interface SelectionDrawerProps {
- open: boolean;
- onClose: () => void;
- values: {
- dataType: DataType;
- timestep: Timestep;
- year: number;
- };
- maxYear?: number;
- onSelection: (values: SelectionDrawerProps["values"]) => void;
-}
-
-function SelectionDrawer(props: SelectionDrawerProps) {
- const { open, onClose, values, maxYear = MAX_YEAR, onSelection } = props;
- const [dataTypeTmp, setDataTypeTmp] = useState(values.dataType);
- const [timestepTmp, setTimestepTemp] = useState(values.timestep);
- const [yearTmp, setYearTmp] = useState(values.year);
- const { t } = useTranslation();
-
- useEffect(() => {
- setDataTypeTmp(values.dataType);
- setTimestepTemp(values.timestep);
- setYearTmp(values.year);
- }, [values.dataType, values.timestep, values.year]);
-
- ////////////////////////////////////////////////////////////////
- // Event Handlers
- ////////////////////////////////////////////////////////////////
-
- const handleSelection = () => {
- onSelection({
- dataType: dataTypeTmp,
- timestep: timestepTmp,
- year: yearTmp,
- });
- onClose();
- };
-
- const handleClose = () => {
- setDataTypeTmp(values.dataType);
- setTimestepTemp(values.timestep);
- setYearTmp(values.year);
- onClose();
- };
-
- ////////////////////////////////////////////////////////////////
- // JSX
- ////////////////////////////////////////////////////////////////
-
- return (
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default SelectionDrawer;
diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx
index abc29e9b4b..f89b584035 100644
--- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx
+++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx
@@ -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(
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(
+ 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 (
- <>
-
+ navigate("..")} />
+
+ }
+ mainContent={
+ <>
+
- navigate("..")} />
-
- }
- mainContent={
- <>
-
-
- {t("study.areas")}
-
-
- {t("study.links")}
-
-
- setSelectedItemId(item.id)}
- />
- >
- }
- onSearchFilterChange={setSearchValue}
- />
- }
- right={
+
+ {t("study.areas")}
+
+
+ {t("study.links")}
+
+
+ {t("study.synthesis")}
+
+
+ setSelectedItemId(item.id)}
+ />
+ >
+ }
+ onSearchFilterChange={setSearchValue}
+ />
+ }
+ right={
+ isSynthesis ? (
+
+
+ {synthesis}
+
+
+ ) : (
- {[
+ {(
[
- `${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]) => (
-
+ [
+ `${t("study.results.mc")}:`,
+ () => (
+ <>
+ {
+ setYear(event?.target.value ? -1 : 1);
+ }}
+ />
+ {year > 0 && (
+ {
+ setYear(Number(event.target.value));
+ }}
+ />
+ )}
+ >
+ ),
+ ],
+ [
+ `${t("study.results.display")}:`,
+ () => (
+ {
+ setDataType(event?.target.value as DataType);
+ }}
+ />
+ ),
+ ],
+ [
+ `${t("study.results.temporality")}:`,
+ () => (
+ {
+ setTimeStep(event?.target.value as Timestep);
+ }}
+ />
+ ),
+ ],
+ ] as const
+ ).map(([label, Field]) => (
+
{label}
- {value}
+
))}
-
- }
onClick={() =>
matrixRes.data &&
handleDownload(matrixRes.data, `matrix_${study.id}`)
}
disabled={matrixRes.isLoading}
>
- {t("global.download")}
+
@@ -286,16 +394,9 @@ function ResultDetails() {
/>
- }
- />
- setShowFilter(false)}
- values={{ dataType, timestep, year }}
- maxYear={output?.nbyears}
- onSelection={handleSelection}
- />
- >
+ )
+ }
+ />
);
}
diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts
index 6b2c53913b..a2bcdd4af9 100644
--- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts
+++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts
@@ -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",
+ },
+];
diff --git a/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/SettingsForm.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/SettingsForm.tsx
index b03440c7b4..b8fe5b9076 100644
--- a/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/SettingsForm.tsx
+++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/SettingsForm.tsx
@@ -180,22 +180,22 @@ function SettingsForm(props: PropType) {
label={t("xpansion.solver")}
data={currentSettings.solver || ""}
handleChange={handleChange}
+ optional
sx={{
minWidth: "100%",
}}
- optional
- />
-
- handleChange("batch_size", parseInt(e.target.value, 10))
- }
- sx={{ mb: 1 }}
/>
+
+ handleChange("batch_size", parseInt(e.target.value, 10))
+ }
+ sx={{ mb: 1 }}
+ />