diff --git a/backend/controller/models.py b/backend/controller/models.py index 040cb2a26..4b4802bcb 100644 --- a/backend/controller/models.py +++ b/backend/controller/models.py @@ -10,6 +10,17 @@ ############################################################################# +def get_model_id_list(dataProviderId, projectId, modelId): + """ + Get the list of models for a project + """ + try: + data_provider = data_provider_manager.get_single_data_provider(dataProviderId) + return list(data_provider.get_model_results_id_list(projectId, modelId)), 200 + except DataProviderException as e: + return e.message, e.status_code + + def get_results(dataProviderId, projectId, modelId, data): """ Get the model results from a sample list diff --git a/backend/controller/projects.py b/backend/controller/projects.py index e7e354026..56d2a85b6 100644 --- a/backend/controller/projects.py +++ b/backend/controller/projects.py @@ -55,27 +55,6 @@ def get_projects(): return projectOverviews, 200 -def get_projects(): - # return a list of project overviews - data_providers_list = data_provider_manager.get_data_provider_list() - projectOverviews = [] - for data_provider in data_providers_list: - try: - projects = data_provider.get_projects() - - if projects is not None: - # Adding data provider id to projects - for project in projects: - project["dataProviderId"] = data_provider.name - - projectOverviews.extend(projects) - - except DataProviderException as e: - print("Warning get DP projects : " + e.message) - - return projectOverviews, 200 - - def get_project(dataProviderId, projectId): # return the info about datasets, models, selections & tags try: @@ -91,6 +70,23 @@ def get_project(dataProviderId, projectId): return e.message, e.status_code +def get_data_id_list(dataProviderId, projectId, requestParameters): + # return the list of data ids + try: + data_provider = data_provider_manager.get_single_data_provider(dataProviderId) + + data_id_list = data_provider.get_id_list( + projectId, + requestParameters["analysis"], + requestParameters["from"], + requestParameters["to"], + ) + + return data_id_list, 200 + except DataProviderException as e: + return e.message, e.status_code + + def delete_project(dataProviderId, projectId): # Delete a project try: diff --git a/backend/controller/samples.py b/backend/controller/samples.py deleted file mode 100644 index 2223e7559..000000000 --- a/backend/controller/samples.py +++ /dev/null @@ -1,28 +0,0 @@ -############################################################################# -# Imports -############################################################################# -from modules.dataProviders.DataProviderException import DataProviderException -import modules.dataProviders.dataProviderManager as data_provider_manager -import utils.samples.get_id_list as get_id_list - -############################################################################# -# SAMPLES Management -############################################################################# - - -# Get the list of samples ID of the project -def get_list(dataProviderId, projectId, data): - # Option 1 : get samples id list - # Option 2 : get samples id list from selections (intersection or union) - # Option 3 : get samples id list from model results (common or not) - # Option 4 : mix of 2 and 3 - # Return option : from and to for streaming purpose - try: - data_provider = data_provider_manager.get_single_data_provider(dataProviderId) - - # Call our utility function - data_id_list = get_id_list.get_list(data_provider, projectId, data) - - return data_id_list, 200 - except DataProviderException as e: - return e.message, e.status_code diff --git a/backend/controller/selection.py b/backend/controller/selection.py index 35f7c67c9..f99a0506c 100644 --- a/backend/controller/selection.py +++ b/backend/controller/selection.py @@ -18,6 +18,14 @@ def get_selections(dataProviderId, projectId): return e.message, e.status_code +def get_selection_id_list(dataProviderId, projectId, selectionId): + try: + data_provider = data_provider_manager.get_single_data_provider(dataProviderId) + return data_provider.get_selection_id_list(projectId, selectionId), 200 + except DataProviderException as e: + return e.message, e.status_code + + def post_selection(dataProviderId, projectId, data): try: data_provider = data_provider_manager.get_single_data_provider(dataProviderId) diff --git a/backend/modules/dataProviders/webDataProvider/cache/cache.py b/backend/modules/dataProviders/webDataProvider/cache/cache.py index b0dc060ee..f5d733ad6 100644 --- a/backend/modules/dataProviders/webDataProvider/cache/cache.py +++ b/backend/modules/dataProviders/webDataProvider/cache/cache.py @@ -3,42 +3,13 @@ # It will mainly save the id list of samples, selections and models results # The ability to cache and the time to live are configurable in the config file -import time from config.init_config import get_config +from cacheout import Cache as CacheoutCache class Cache: def __init__(self): - self.cache = { - "project_id_list": { - # : { - # _: { - # id_list: [...], - # timestamp: - # }, - # total: { - # id_list: [...], - # timestamp: - # } - # } - }, - "selection_id_list": { - # : { - # : { - # id_list: [...], - # timestamp: - # } - # } - }, - "model_result_id_list": { - # : { - # : { - # id_list: [...], - # timestamp: - # } - # } - }, - } + # Get config self.config = get_config() self.cache_enabled = self.config["DATA_PROVIDERS_CONFIG"][ @@ -48,137 +19,76 @@ def __init__(self): "web_data_provider_cache_duration" ] - # Project id list - def get_id_list(self, id_project, _from=None, _to=None): - if not self.cache_enabled: - return None + # Init cache + self.project_id_list_cache = CacheoutCache(maxsize=256, ttl=self.cache_ttl) + # __: [...] + # _total: [...] - if id_project not in self.cache["project_id_list"]: - return None + self.selection_id_list_cache = CacheoutCache(maxsize=256, ttl=self.cache_ttl) + # _: [...] - project_cache = self.cache["project_id_list"][id_project] + self.model_result_id_list_cache = CacheoutCache(maxsize=256, ttl=self.cache_ttl) + # _: [...] + # Project id list + def get_key(self, id_project, _from=None, _to=None): if _from is None or _to is None: - if "total" not in project_cache: - return None - - # Check if cache is still valid - timestamp = project_cache["total"]["timestamp"] - if time.time() - timestamp > self.cache_ttl: - # Cache is not valid anymore - # Delete cache - del project_cache["total"] - return None - - return project_cache["total"]["id_list"] - + return "{}_total".format(id_project) else: - key = "{}_{}".format(_from, _to) - if key not in project_cache: - return None + return "{}_{}_{}".format(id_project, _from, _to) - # Check if cache is still valid - timestamp = project_cache[key]["timestamp"] - if time.time() - timestamp > self.cache_ttl: - # Cache is not valid anymore - # Delete cache - del project_cache[key] - return None + def get_id_list(self, id_project, _from=None, _to=None): + if not self.cache_enabled: + return None + + key = self.get_key(id_project, _from, _to) - return project_cache[key]["id_list"] + return self.project_id_list_cache.get(key) def set_id_list(self, id_project, id_list, _from=None, _to=None): if not self.cache_enabled: return - if id_project not in self.cache["project_id_list"]: - self.cache["project_id_list"][id_project] = {} + key = self.get_key(id_project, _from, _to) - project_cache = self.cache["project_id_list"][id_project] - - if _from is None or _to is None: - project_cache["total"] = { - "id_list": id_list, - "timestamp": time.time(), - } - else: - key = "{}_{}".format(_from, _to) - project_cache[key] = { - "id_list": id_list, - "timestamp": time.time(), - } + self.project_id_list_cache.set(key, id_list) # Selection id list + def get_selection_key(self, id_project, id_selection): + return "{}_{}".format(id_project, id_selection) + def get_selection_id_list(self, id_project, id_selection): if not self.cache_enabled: return None - if id_project not in self.cache["selection_id_list"]: - return None - - project_cache = self.cache["selection_id_list"][id_project] - - if id_selection not in project_cache: - return None + key = self.get_selection_key(id_project, id_selection) - # Check if cache is still valid - timestamp = project_cache[id_selection]["timestamp"] - - if time.time() - timestamp > self.cache_ttl: - # Cache is not valid anymore - # Delete cache - del project_cache[id_selection] - return None - - return project_cache[id_selection]["id_list"] + return self.selection_id_list_cache.get(key) def set_selection_id_list(self, id_project, id_selection, id_list): if not self.cache_enabled: return - if id_project not in self.cache["selection_id_list"]: - self.cache["selection_id_list"][id_project] = {} - - project_cache = self.cache["selection_id_list"][id_project] + key = self.get_selection_key(id_project, id_selection) - project_cache[id_selection] = { - "id_list": id_list, - "timestamp": time.time(), - } + self.selection_id_list_cache.set(key, id_list) # Model result id list + def get_model_result_key(self, id_project, id_model): + return "{}_{}".format(id_project, id_model) + def get_model_result_id_list(self, id_project, id_model): if not self.cache_enabled: return None - if id_project not in self.cache["model_result_id_list"]: - return None - - project_cache = self.cache["model_result_id_list"][id_project] - - if id_model not in project_cache: - return None - - # Check if cache is still valid - timestamp = project_cache[id_model]["timestamp"] - if time.time() - timestamp > self.cache_ttl: - # Cache is not valid anymore - # Delete cache - del project_cache[id_model] - return None + key = self.get_model_result_key(id_project, id_model) - return project_cache[id_model]["id_list"] + return self.model_result_id_list_cache.get(key) def set_model_result_id_list(self, id_project, id_model, id_list): if not self.cache_enabled: return - if id_project not in self.cache["model_result_id_list"]: - self.cache["model_result_id_list"][id_project] = {} - - project_cache = self.cache["model_result_id_list"][id_project] + key = self.get_model_result_key(id_project, id_model) - project_cache[id_model] = { - "id_list": id_list, - "timestamp": time.time(), - } + self.model_result_id_list_cache.set(key, id_list) diff --git a/backend/requirements.txt b/backend/requirements.txt index aed590a0d..dc8b3ec35 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,4 +9,5 @@ ujson == 4.0.2 sklearn == 0.0 kafka-python == 2.0.2 openapi_spec_validator == 0.2.8 -PyYAML == 6.0 \ No newline at end of file +PyYAML == 6.0 +cacheout == 0.14.1 \ No newline at end of file diff --git a/backend/swagger.yaml b/backend/swagger.yaml index 3e8645366..ad8d847f1 100644 --- a/backend/swagger.yaml +++ b/backend/swagger.yaml @@ -1,6 +1,6 @@ swagger: "2.0" info: - version: 0.24.7 + version: 0.24.8 title: DebiAI_BACKEND_API description: DebiAI backend api contact: @@ -232,6 +232,59 @@ paths: 404: description: project doesn't exist + /data-providers/{dataProviderId}/projects/{projectId}/dataIdList: + post: + summary: Get the project data id list + tags: [Project] + operationId: controller.projects.get_data_id_list + parameters: + - name: dataProviderId + in: path + type: string + required: true + - name: projectId + in: path + type: string + required: true + - name: requestParameters + in: body + required: true + schema: + type: object + required: + - analysis + - from + - to + properties: + from: + type: integer + description: The index of the first data to return + x-nullable: true + to: + type: integer + description: The index of the last data to return + x-nullable: true + analysis: + type: object + required: + - id + properties: + id: + type: string + description: Id of the analysis + start: + type: boolean + description: If true, this is the first request of the analysis + end: + type: boolean + description: If true, this is the last request of the analysis + + responses: + 200: + description: project + schema: + $ref: "#/definitions/project" + # BlockLevels /data-providers/{dataProviderId}/projects/{projectId}/blocklevels: post: @@ -380,6 +433,33 @@ paths: description: Project not found /data-providers/{dataProviderId}/projects/{projectId}/models/{modelId}: + get: + summary: Get a model results id list + tags: [Model] + operationId: controller.models.get_model_id_list + parameters: + - name: dataProviderId + in: path + type: string + required: true + - name: projectId + in: path + type: string + required: true + - name: modelId + in: path + type: string + required: true + responses: + 200: + description: model id list + schema: + type: array + items: + type: string + 404: + description: model or project doesn't exist + delete: summary: remove a model tags: [Model] @@ -485,76 +565,6 @@ paths: 404: description: model or project doesn't exist - # Samples - /data-providers/{dataProviderId}/projects/{projectId}/samples: - post: - summary: get the project sampleId list, can be filtered by selections, models, or both - tags: [Samples] - operationId: controller.samples.get_list - parameters: - - name: dataProviderId - in: path - type: string - required: true - - name: projectId - in: path - type: string - description: project ID - required: true - - - name: data - in: body - schema: - type: object - properties: - selectionIds: - type: array - items: - type: string - description: To get the samples that are parts of those given selection - selectionIntersection: - type: boolean - description: To get only the the samples that are in all of the selections - - modelIds: - type: array - items: - type: string - description: To get the samples that those models have been evaluated on - commonResults: - type: boolean - description: To get only the the samples that have been evaluated on a common sample - - from: - type: integer - description: Where we start the selection in case of over 10 000 samples - minimum: 0 - to: - type: integer - description: Where we stop the selection in case of over 10 000 samples - minimum: 0 - - analysis: - description: Informations about the analysis to help data-providers with data management - $ref: "#/definitions/analysis" - - responses: - 200: - description: List of the project sample id - schema: - type: object - properties: - samples: - type: array - items: - type: string - description: List of samples - nbFromSelection: - type: integer - description: The number of samples that are from the selection (and not the models) - 404: - description: project doesn't exist - # Blocks /data-providers/{dataProviderId}/projects/{projectId}/blocks: post: @@ -843,6 +853,33 @@ paths: type: object /data-providers/{dataProviderId}/projects/{projectId}/selections/{selectionId}: + get: + summary: Get a project selection id list + tags: [Selection] + operationId: controller.selection.get_selection_id_list + parameters: + - name: dataProviderId + in: path + type: string + required: true + - name: projectId + in: path + type: string + required: true + - name: selectionId + in: path + type: string + required: true + responses: + 200: + description: Project selection id list + schema: + type: array + items: + type: string + 404: + description: Selection, project or data provider not found + delete: summary: delete a selection tags: [Selection] @@ -1637,7 +1674,7 @@ definitions: type: string description: block level name - # Artefacts + # Artefact artefact: type: object required: diff --git a/backend/utils/samples/get_id_list.py b/backend/utils/samples/get_id_list.py deleted file mode 100644 index 143347a41..000000000 --- a/backend/utils/samples/get_id_list.py +++ /dev/null @@ -1,89 +0,0 @@ -# Get the list of samples ID of the project -def get_list(data_provider, project_id, data): - # Option 1 : get samples id list - # Option 2 : get samples id list from selections (intersection or union) - # Option 3 : get samples id list from model results (common or not) - # Option 4 : Option 2 + 3 - # Return option : from and to for streaming purpose - id_list = [] - nb_from_selection = 0 - nb_from_models = 0 - - # Option 1 : No selections or models, get samples id list - if ("selectionIds" not in data or len(data["selectionIds"]) == 0) and ( - "modelIds" not in data or len(data["modelIds"]) == 0 - ): - if "from" in data and "to" in data: - id_list = data_provider.get_id_list( - project_id, data["analysis"], data["from"], data["to"] - ) - else: - id_list = data_provider.get_id_list(project_id, data["analysis"]) - - nb_from_selection = len(id_list) - - # Option 2 : get samples id list from selections (intersection or union) - if "selectionIds" in data and len(data["selectionIds"]) > 0: - selection_intersection = data["selectionIntersection"] - - for selection_id in data["selectionIds"]: - # Get the selection id list - selection_sample_ids = data_provider.get_selection_id_list( - project_id, selection_id - ) - if len(id_list) == 0: - # Set the first selection id list - id_list = selection_sample_ids - else: - # Get the intersection or union between - # the current selection and the previous one - if selection_intersection: - id_list = list(set(id_list) & set(selection_sample_ids)) - if len(id_list) == 0: - break - else: - id_list = list(set(id_list) | set(selection_sample_ids)) - nb_from_selection = len(id_list) - - # Option 3 : get id list from model results samples ID (common or not) - if "modelIds" in data and len(data["modelIds"]) > 0: - common_results = data["commonResults"] - model_result_ids = [] - for model_id in data["modelIds"]: - # First get the model results id list - model_sample_ids = data_provider.get_model_results_id_list( - project_id, model_id - ) - - # Then get the common or not common samples id list - if len(model_result_ids) == 0: - model_result_ids = model_sample_ids - else: - if common_results: - model_result_ids = list( - set(model_result_ids) & set(model_sample_ids) - ) - if len(model_result_ids) == 0: - break - else: - model_result_ids = list( - set(model_result_ids) | set(model_sample_ids) - ) - - nb_from_models = len(model_result_ids) - - if "selectionIds" in data and len(data["selectionIds"]) > 0: - # Option 4 : Option 2 + 3, merge the two lists - id_list = list(set(id_list) & set(model_result_ids)) - - else: - # Option 3 : only model results - id_list = model_result_ids - nb_from_selection = len(model_result_ids) - - return { - "samples": id_list, - "nbSamples": len(id_list), - "nbFromSelection": nb_from_selection, - "nbFromModels": nb_from_models, - } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a7458f57a..3cb7d4abe 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "debiai_frontend", - "version": "0.24.7", + "version": "0.24.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "debiai_frontend", - "version": "0.24.7", + "version": "0.24.8", "license": "Apache-2.0", "dependencies": { "axios": "^0.21.4", diff --git a/frontend/package.json b/frontend/package.json index 9970b1e9f..cef7c82fd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "debiai_frontend", - "version": "0.24.7", + "version": "0.24.8", "description": "Frontend for Debiai, made with Vuejs", "license": "Apache-2.0", "scripts": { diff --git a/frontend/src/components/debiai/project/Models.vue b/frontend/src/components/debiai/project/Models.vue index 7cf9cf61a..be7853fb0 100644 --- a/frontend/src/components/debiai/project/Models.vue +++ b/frontend/src/components/debiai/project/Models.vue @@ -72,7 +72,7 @@ :src="require('../../../assets/svg/data.svg')" width="20" height="20" - />{{ "nbResults" in model ? (model.nbResults === null ? "?" : model.nbResults) : "?" }} + />{{ "nbResults" in model && model.nbResults !== null ? model.nbResults : "?" }} @@ -132,29 +132,6 @@
- Evaluated samples : -
- - - {{ nbEvaluatedSamples }} - - - - ({{ Math.ceil((nbEvaluatedSamples * 100) / nbSelectedSamples) }}%) - -
Number of results :
{{ nbResults === null ? "?" : nbResults }} @@ -299,7 +276,7 @@ export default { padding: 0 5px 0 5px; display: flex; align-items: center; - justify-content: center; + justify-content: flex-end; font-size: 0.9em; } @@ -315,7 +292,7 @@ export default { #modelsControls #commonModelResults { display: flex; align-items: center; - justify-content: center; + justify-content: flex-end; flex: 1; } @@ -323,7 +300,8 @@ export default { display: flex; align-items: center; justify-content: flex-end; - flex: 1; + height: 40px; + padding-left: 20px; } #modelsControls #commonModelResults label { diff --git a/frontend/src/components/debiai/project/Project.vue b/frontend/src/components/debiai/project/Project.vue index 4d1d605f0..abf201334 100644 --- a/frontend/src/components/debiai/project/Project.vue +++ b/frontend/src/components/debiai/project/Project.vue @@ -86,7 +86,8 @@ import Selections from "./selections/Selections.vue"; import CachePanel from "./cache/CachePanel.vue"; // Services -import dataLoader from "../../../services/dataLoader"; +import dataLoader from "@/services/dataLoader"; +import samplesIdListRequester from "@/services/statistics/samplesIdListRequester"; import swal from "sweetalert"; export default { @@ -225,28 +226,37 @@ export default { this.updateNbSamples(); }, updateNbSamples() { - // Let the backend told us the common or grouped selections and evaluations + // Get the number of samples that will be analyzed + // Don't send request if there is no selection and model + if (this.selectedSelections.length === 0 && this.selectedModels.length === 0) { + this.nbSelectedSamples = this.project.nbSamples; + this.nbEvaluatedSamples = 0; + this.nbResults = 0; + return; + } + + // Send request + const parameters = { + analysis: { id: this.$services.uuid(), start: true, end: true }, + selectionIds: this.selectedSelections.map((s) => s.id), + selectionIntersection: this.selectionIntersection, + modelIds: this.selectedModels.map((m) => m.id), + commonResults: this.commonModelResults, + }; this.loading = true; - this.$backendDialog - .getProjectSamples({ - analysis: { id: this.$services.uuid(), start: true, end: true }, - selectionIds: this.selectedSelections.map((s) => s.id), - selectionIntersection: this.selectionIntersection, - modelIds: this.selectedModels.map((m) => m.id), - commonResults: this.commonModelResults, - }) + samplesIdListRequester + .getIdList(parameters) .finally(() => (this.loading = false)) .then((res) => { this.nbSelectedSamples = res.nbFromSelection; this.nbEvaluatedSamples = res.nbSamples; - if (this.commonModelResults) this.nbResults = res.nbSamples * this.selectedModels.length; - else this.nbResults = null; // Will draw a "?" on the dashboard + this.nbResults = res.nbFromModels; }) .catch((e) => { console.log(e); this.$store.commit("sendMessage", { title: "error", - msg: "Something went wrong while counting ", + msg: "Something went wrong while getting the samples list", }); this.loadProject(); }); @@ -286,7 +296,7 @@ export default { modelIds = [], commonModelResults = false, }) { - console.time("LOAD TREE"); + console.time("Loading data"); this.loading = true; dataLoader @@ -318,9 +328,9 @@ export default { modelIds = modelIds.reduce((mId, total) => total + "." + mId); // Perf Log - console.timeEnd("LOAD TREE"); + console.timeEnd("Loading data"); - // start analysis immediatly + // start analysis immediately this.$router.push({ name: "dataAnalysis", query: { diff --git a/frontend/src/components/debiai/project/selections/Selections.vue b/frontend/src/components/debiai/project/selections/Selections.vue index 579b1fc2c..995ec3062 100644 --- a/frontend/src/components/debiai/project/selections/Selections.vue +++ b/frontend/src/components/debiai/project/selections/Selections.vue @@ -348,6 +348,7 @@ export default { flex: 1; align-items: center; justify-content: flex-end; + height: 40px; } #analysisControls #selectionIntersection { display: flex; diff --git a/frontend/src/services/backendDialog.js b/frontend/src/services/backendDialog.js index a6a2d67b4..e42547ff5 100644 --- a/frontend/src/services/backendDialog.js +++ b/frontend/src/services/backendDialog.js @@ -101,36 +101,39 @@ export default { }); }, - // Samples - getProjectSamples({ - analysis, - selectionIds = [], - selectionIntersection = false, - modelIds = [], - commonResults = false, - from = null, - to = null, - }) { - let code; - if (from === null) code = startRequest("Loading the project samples list"); + // Samples ID + getProjectIdList(analysis, from = null, to = null) { let request = - apiURL + "data-providers/" + dataProviderId() + "/projects/" + projectId() + "/samples"; + apiURL + "data-providers/" + dataProviderId() + "/projects/" + projectId() + "/dataIdList"; - const requestBody = { - analysis, - selectionIds, - selectionIntersection, - modelIds, - commonResults, - }; - if (from !== null && to !== null) { - requestBody.from = from; - requestBody.to = to; - } + const requestBody = { analysis, from, to }; + return axios.post(request, requestBody).then((response) => response.data); + }, + getSelectionIdList(selection_id) { return axios - .post(request, requestBody) - .finally(() => endRequest(code)) + .get( + apiURL + + "data-providers/" + + dataProviderId() + + "/projects/" + + projectId() + + "/selections/" + + selection_id + ) + .then((response) => response.data); + }, + getModelResultsIdList(model_id) { + return axios + .get( + apiURL + + "data-providers/" + + dataProviderId() + + "/projects/" + + projectId() + + "/models/" + + model_id + ) .then((response) => response.data); }, @@ -165,7 +168,6 @@ export default { ) .then((response) => response.data); }, - delModel(modelId) { let code = startRequest("Deleting selection"); return axios diff --git a/frontend/src/services/dataLoader.js b/frontend/src/services/dataLoader.js index 28db642c2..c95ecaa6b 100644 --- a/frontend/src/services/dataLoader.js +++ b/frontend/src/services/dataLoader.js @@ -3,6 +3,7 @@ import cacheService from "./cacheService"; import services from "./services"; const backendDialog = require("./backendDialog"); +const samplesIdListRequester = require("./statistics/samplesIdListRequester").default; let currentAnalysis = {}; @@ -18,9 +19,7 @@ function resetCurrentAnalysis() { }; } -// // Need to take position on which columns stay available or not -// const CATEGORIES = [ { blName: "inputs", singleName: "input" }, { blName: "groundTruth", singleName: "ground truth" }, @@ -30,26 +29,6 @@ const CATEGORIES = [ { blName: "annotations", singleName: "annotations" }, ]; -// Requests functions -function startRequest(name, cancelCallback = null) { - let requestCode = services.uuid(); - store.commit("startRequest", { name, code: requestCode, cancelCallback }); - return requestCode; -} -function startProgressRequest(name) { - let requestCode = services.uuid(); - store.commit("startRequest", { name, code: requestCode, progress: 0 }); - return requestCode; -} -function updateRequestProgress(code, progress) { - store.commit("updateRequestProgress", { code, progress }); -} -function updateRequestQuantity(code, quantity) { - store.commit("updateRequestQuantity", { code, quantity }); -} -function endRequest(code) { - store.commit("endRequest", code); -} async function getDataProviderLimit() { try { let dataProviderInfo = store.state.ProjectPage.dataProviderInfo; @@ -105,7 +84,7 @@ async function getProjectSamplesIdList( // At the moment, we gather all ID when we deal with selections and models // If we have a small project, we gather all ID // Also, if we don't have the number of samples, we gather all ID - const res = await backendDialog.default.getProjectSamples({ + const res = await samplesIdListRequester.getIdList({ analysis: { id: currentAnalysis.id, start: true, end: true }, selectionIds, selectionIntersection, @@ -119,7 +98,7 @@ async function getProjectSamplesIdList( let samplesIdList = []; let i = 0; console.warn("Project samples number is not known, loading samples ID list by chunks"); - let requestCode = startRequest("Step 1/2: Loading the data ID list"); + let requestCode = services.startRequest("Step 1/2: Loading the data ID list"); currentAnalysis.requestCodes.projectSamplesIdList = requestCode; try { @@ -133,7 +112,7 @@ async function getProjectSamplesIdList( if (i === 0) analysis.start = true; // Send the request - const res = await backendDialog.default.getProjectSamples({ analysis, from, to }); + const res = await samplesIdListRequester.getIdList({ analysis, from, to }); if (res.samples.length === 0) { console.log("No samples found while loading project samples ID list"); @@ -159,15 +138,15 @@ async function getProjectSamplesIdList( if (currentAnalysis.canceled) break; // Update a new progress bar counter - updateRequestQuantity(requestCode, samplesIdList.length); + services.updateRequestQuantity(requestCode, samplesIdList.length); i++; } console.timeEnd("getProjectSamplesIdList"); - endRequest(requestCode); + services.endRequest(requestCode); } catch (error) { console.timeEnd("getProjectSamplesIdList"); - endRequest(requestCode); + services.endRequest(requestCode); throw error; } @@ -180,7 +159,7 @@ async function getProjectSamplesIdList( let samplesIdList = []; console.log("Splitting ID list request in ", nbRequest, " requests"); - let requestCode = startProgressRequest("Step 1/2: Loading the data ID list"); + let requestCode = services.startProgressRequest("Step 1/2: Loading the data ID list"); currentAnalysis.requestCodes.projectSamplesIdList = requestCode; try { @@ -195,34 +174,34 @@ async function getProjectSamplesIdList( if (i === nbRequest - 1) analysis.end = true; // Send the request - const res = await backendDialog.default.getProjectSamples({ analysis, from, to }); + const idList = await backendDialog.default.getProjectIdList(analysis, from, to); - if (res.samples.length === 0) + if (idList.length === 0) throw "No samples found while loading project samples ID list from " + from + " to " + to; - if (res.samples.length !== to - from + 1) + if (idList.length !== to - from + 1) throw ( "Wrong number of samples while loading project samples ID list from " + from + " to " + to + ", got " + - res.samples.length + + idList.length + " instead of " + (to - from + 1) ); // Add the samples to the list - samplesIdList = samplesIdList.concat(res.samples); - updateRequestProgress(requestCode, (i + 1) / nbRequest); + samplesIdList = samplesIdList.concat(idList); + services.updateRequestProgress(requestCode, (i + 1) / nbRequest); // Check if the request has been canceled if (currentAnalysis.canceled) break; } console.timeEnd("getProjectSamplesIdList"); - endRequest(requestCode); + services.endRequest(requestCode); } catch (error) { console.timeEnd("getProjectSamplesIdList"); - endRequest(requestCode); + services.endRequest(requestCode); throw error; } @@ -301,7 +280,7 @@ async function downloadSamplesData(projectMetadata, sampleIds) { let nbSamples = sampleIds.length; // Create a request - let requestCode = startProgressRequest("Loading the project data"); + let requestCode = services.startProgressRequest("Loading the project data"); console.time("Loading the project data"); // Pull the tree let retArray = []; @@ -363,13 +342,13 @@ async function downloadSamplesData(projectMetadata, sampleIds) { // Update the progress pulledData += CHUNK_SIZE; - updateRequestProgress(requestCode, pulledData / nbSamples); + services.updateRequestProgress(requestCode, pulledData / nbSamples); // Check if the request has been canceled if (currentAnalysis.canceled) break; } } finally { - endRequest(requestCode); + services.endRequest(requestCode); console.timeEnd("Loading the project data"); } return { dataArray: retArray, sampleIdList: retDataIdList }; @@ -381,7 +360,7 @@ async function downloadResults(projectMetadata, modelId, sampleIds) { let pulledData = 0; // Create a request - let requestCode = startProgressRequest(modelId); + let requestCode = services.startProgressRequest(modelId); currentAnalysis.requestCodes.modelResults = requestCode; let modelResultsRet = {}; @@ -413,14 +392,14 @@ async function downloadResults(projectMetadata, modelId, sampleIds) { // cacheService.saveResults(timestamp, modelId, resultsToSave) pulledData += CHUNK_SIZE; - updateRequestProgress(requestCode, pulledData / nbSamples); + services.updateRequestProgress(requestCode, pulledData / nbSamples); // Check if the request has been canceled if (currentAnalysis.canceled) break; } - updateRequestProgress(requestCode, 1); + services.updateRequestProgress(requestCode, 1); } finally { - endRequest(requestCode); + services.endRequest(requestCode); } return modelResultsRet; @@ -480,7 +459,7 @@ async function loadDataAndModelResults( // =========== Then add the model results // Create a request - let requestCode = startProgressRequest("Loading the model results"); + let requestCode = services.startProgressRequest("Loading the model results"); try { let dataArrayFull = []; @@ -510,9 +489,8 @@ async function loadDataAndModelResults( dataArrayFull = [...dataArrayFull, ...dataAndResultsArray]; samplesToPullFull = [...samplesToPullFull, ...modelsSamplesToPull]; - updateRequestProgress(requestCode, (i + 1) / modelIds.length); + services.updateRequestProgress(requestCode, (i + 1) / modelIds.length); } - store.commit("endRequest", requestCode); return { metaData: projectMetadata, @@ -520,8 +498,9 @@ async function loadDataAndModelResults( sampleIdList: samplesToPullFull, }; } catch (error) { - store.commit("endRequest", requestCode); throw error; + } finally { + services.endRequest(requestCode); } } @@ -595,7 +574,7 @@ function createColumn(label, values, category, type = null, group = null) { } async function arrayToJson(array, metaData) { - let requestCode = startProgressRequest("Preparing the analysis"); + let requestCode = services.startProgressRequest("Preparing the analysis"); console.time("Preparing the analysis"); let ret = { @@ -633,14 +612,14 @@ async function arrayToJson(array, metaData) { col.index = i; ret.columns[i] = col; - updateRequestProgress(requestCode, (i + 1) / ret.nbColumns); + services.updateRequestProgress(requestCode, (i + 1) / ret.nbColumns); console.timeLog( "Preparing the analysis", "Column " + label + " " + (i + 1) + " / " + ret.nbColumns ); } - endRequest(requestCode); + services.endRequest(requestCode); console.timeEnd("Preparing the analysis"); return ret; @@ -657,7 +636,7 @@ async function loadProjectSamples({ // Setups the analysis resetCurrentAnalysis(); - let requestCode = startRequest("The analysis is starting", cancelCallback); + let requestCode = services.startRequest("The analysis is starting", cancelCallback); currentAnalysis.requestCodes.analysisStarting = requestCode; currentAnalysis.id = services.uuid(); @@ -689,7 +668,7 @@ async function loadProjectSamples({ resetCurrentAnalysis(); throw e; } finally { - endRequest(requestCode); + services.endRequest(requestCode); } if (currentAnalysis.canceled) { @@ -708,9 +687,9 @@ function cancelCallback() { currentAnalysis.canceled = true; // Stop all the requests - endRequest(currentAnalysis.requestCodes.analysisStarting); - endRequest(currentAnalysis.requestCodes.projectSamplesIdList); - endRequest(currentAnalysis.requestCodes.modelResults); + services.endRequest(currentAnalysis.requestCodes.analysisStarting); + services.endRequest(currentAnalysis.requestCodes.projectSamplesIdList); + services.endRequest(currentAnalysis.requestCodes.modelResults); } function isAnalysisLoading() { if (currentAnalysis.id == null || currentAnalysis.canceled) return false; diff --git a/frontend/src/services/services.js b/frontend/src/services/services.js index a1dce750d..5b0e85c8d 100644 --- a/frontend/src/services/services.js +++ b/frontend/src/services/services.js @@ -2,6 +2,7 @@ const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/ navigator.userAgent ); import { v4 as uuidv4 } from "uuid"; +import store from "../store"; export default { isMobile, @@ -141,4 +142,25 @@ export default { getDate() { return this.timeStampToDate(this.getTimestamp()); }, + + // Requests animations + startRequest(name, cancelCallback = null) { + let requestCode = uuidv4(); + store.commit("startRequest", { name, code: requestCode, cancelCallback }); + return requestCode; + }, + startProgressRequest(name) { + let requestCode = uuidv4(); + store.commit("startRequest", { name, code: requestCode, progress: 0 }); + return requestCode; + }, + updateRequestProgress(code, progress) { + store.commit("updateRequestProgress", { code, progress }); + }, + updateRequestQuantity(code, quantity) { + store.commit("updateRequestQuantity", { code, quantity }); + }, + endRequest(code) { + store.commit("endRequest", code); + }, }; diff --git a/frontend/src/services/statistics/samplesIdListRequester.js b/frontend/src/services/statistics/samplesIdListRequester.js new file mode 100644 index 000000000..5fbe2576b --- /dev/null +++ b/frontend/src/services/statistics/samplesIdListRequester.js @@ -0,0 +1,164 @@ +import backendDialog from "../backendDialog"; +import services from "../services"; + +async function getIdList(data) { + // Get the list of samples ID of the project according to the chosen selection(s) and/or model(s) + // Option 1 : get samples id list + // Option 2 : get samples id list from selections (intersection or union) + // Option 3 : get samples id list from model results (common or not) + // Option 4 : Option 2 + 3 + // Return option : from and to for streaming purpose + + const code = services.startRequest("Getting samples ID list"); + + let id_list = []; + let id_list_set = new Set(); + let nb_from_selection = 0; + let nb_from_models = 0; // Number of samples from models, used to calculate + // How many predictions will result from the models union + + // Option 1 : No selections or models, get samples id list + if ( + (!data.selectionIds || data.selectionIds.length === 0) && + (!data.modelIds || data.modelIds.length === 0) + ) { + try { + if (data.from && data.to) + id_list = await backendDialog.getProjectIdList(data.analysis, data.from, data.to); + else id_list = await backendDialog.getProjectIdList(data.analysis); + + nb_from_selection = id_list.length; + } catch (error) { + services.endRequest(code); + throw error; + } + } + + // Option 2 : get samples id list from selections (intersection or union) + if (data.selectionIds && data.selectionIds.length > 0) { + const selectionsIdCode = services.startProgressRequest("Getting selections ID list"); + const selection_intersection = data.selectionIntersection; + + try { + let i = 0; + for (const selection_id of data.selectionIds) { + i++; + services.updateRequestProgress(selectionsIdCode, i / data.selectionIds.length); + + // Get the selection id list + const selection_sample_ids = await backendDialog.getSelectionIdList(selection_id); + if (id_list.length === 0) { + // Set the first selection id list + id_list = selection_sample_ids; + } else { + // Get the intersection or union between + // the current selection and the previous one + if (selection_intersection) { + const selection_sample_ids_set = new Set(selection_sample_ids); + id_list = id_list.filter((sampleId) => selection_sample_ids_set.has(sampleId)); + if (id_list.length === 0) { + nb_from_selection = 0; + break; + } + } else { + id_list = [...new Set([...id_list, ...selection_sample_ids])]; + } + } + } + } catch (e) { + services.endRequest(code); + throw e; + } finally { + services.endRequest(selectionsIdCode); + } + + id_list_set = new Set(id_list); + id_list = [...id_list_set]; + nb_from_selection = id_list.length; + } + + // Option 3 : get id list from model results samples ID (common or not) + if (data.modelIds && data.modelIds.length > 0) { + if (data.selectionIds && data.selectionIds.length > 0) { + // Option 4 : Option 2 + 3, intersection of selections and model results + // If the selection id list is empty, the result will be empty + if (id_list.length === 0) { + nb_from_models = null; + services.endRequest(code); + return { + samples: [], + nbSamples: 0, + nbFromSelection: 0, + nbFromModels: 0, + }; + } + } + + const modelsIdCode = services.startProgressRequest("Getting models ID list"); + + const common_results = data.commonResults; + let model_result_ids = []; + + try { + let i = 0; + for (const model_id of data.modelIds) { + i++; + services.updateRequestProgress(modelsIdCode, i / data.modelIds.length); + + // First get the model results id list + let model_sample_ids = await backendDialog.getModelResultsIdList(model_id); + + // Only select the samples that are in the selection (option 4) + if (data.selectionIds && data.selectionIds.length > 0) { + model_sample_ids = model_sample_ids.filter((sampleId) => id_list_set.has(sampleId)); + } else { + nb_from_selection += model_sample_ids.length; + } + + // Then get the common or not common samples id list + if (model_result_ids.length === 0) { + model_result_ids = model_sample_ids; + + if (!common_results) nb_from_models += model_sample_ids.length; + } else { + if (common_results) { + // Get the intersection between the current model results and the previous one + const model_sample_ids_set = new Set(model_sample_ids); + model_result_ids = model_result_ids.filter((sampleId) => + model_sample_ids_set.has(sampleId) + ); + + if (model_result_ids.length === 0) { + nb_from_models = 0; + break; + } + } else { + nb_from_models += model_sample_ids.length; + model_result_ids = [...new Set([...model_result_ids, ...model_sample_ids])]; + } + } + } + + if (common_results) nb_from_models = model_result_ids.length * data.modelIds.length; + id_list = model_result_ids; + } catch (e) { + services.endRequest(code); + throw e; + } finally { + services.endRequest(modelsIdCode); + } + } + + services.endRequest(code); + + return { + samples: id_list, + nbSamples: id_list.length, + nbFromSelection: nb_from_selection, + nbFromModels: nb_from_models, + }; +} + +export default { + getIdList, +};