From 22f359e4b848ff3f96eedba0f0fd5f64adb9e040 Mon Sep 17 00:00:00 2001 From: FadyCoding Date: Wed, 5 Jun 2024 10:11:48 +0200 Subject: [PATCH 1/3] added metadata for selections and models --- data-provider-API.yaml | 8 ++++++ src/controllers/debiai/models.js | 38 +++++++++++++++++++++++-- src/controllers/debiai/selections.js | 42 ++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/data-provider-API.yaml b/data-provider-API.yaml index d231ba7..e70a5fa 100644 --- a/data-provider-API.yaml +++ b/data-provider-API.yaml @@ -426,6 +426,10 @@ paths: description: Timestamp of the model creation example: 1697814247211 nullable: true + metadata: + type: object + description: Metadata of the model + nullable: true /debiai/projects/{projectId}/models/{modelId}/evaluated-data-id-list: parameters: - schema: @@ -591,6 +595,10 @@ paths: description: Timestamp of the selection creation example: 1697814247211 nullable: true + metadata: + type: object + description: Metadata of the selection + nullable: true post: x-eov-operation-id: createSelection x-eov-operation-handler: controllers/debiai/selections.js diff --git a/src/controllers/debiai/models.js b/src/controllers/debiai/models.js index ddb0f6e..617d76c 100644 --- a/src/controllers/debiai/models.js +++ b/src/controllers/debiai/models.js @@ -32,9 +32,41 @@ exports.modelList = (req, res) => { name: "Model 1", nbResults: 2, metadata: { - name: "Author's name", - input_features: ["feature1", "feature2"], - notes: "Test metadata", + author_name: ["Alice", "Bob", "Charlie"], + model_names: ["Linear Regression", "Random Forest", "Neural Network"], + input_features: [ + ["feature1", "feature2"], + ["feature1", "feature2", "feature3"], + ["feature1", "feature2", "feature4"], + ], + notes: [ + "Baseline model", + "Tuned with grid search", + "Deep learning model", + ], + models_data: [ + { + ModelID: "M1", + ModelName: "Linear Regression", + Author: "Alice", + InputFeatures: ["feature1", "feature2"], + Notes: "Baseline model", + }, + { + ModelID: "M2", + ModelName: "Random Forest", + Author: "Bob", + InputFeatures: ["feature1", "feature2", "feature3"], + Notes: "Tuned with grid search", + }, + { + ModelID: "M3", + ModelName: "Neural Network", + Author: "Charlie", + InputFeatures: ["feature1", "feature2", "feature4"], + Notes: "Deep learning model", + }, + ], }, }, { diff --git a/src/controllers/debiai/selections.js b/src/controllers/debiai/selections.js index da28963..dd67869 100644 --- a/src/controllers/debiai/selections.js +++ b/src/controllers/debiai/selections.js @@ -20,12 +20,52 @@ const selections = [ id: "first-selection", name: "First selection", nbSamples: 2, + metadata: { + metadata: { + samples: ["S1", "S2"], + cols: ["SampleID", "Species", "Weight", "Height"], + data: [ + { + SampleID: "S1", + Species: "Homo sapiens", + Weight: 70.5, + Height: 175, + }, + { + SampleID: "S2", + Species: "Mus musculus", + Weight: 25.3, + Height: 150, + }, + ], + }, + }, dataIds: [1, 2], }, { id: "second-selection", name: "second selection", + metadata: { + metadata: { + samples: ["S1", "S2"], + cols: ["SampleID", "Species", "Weight", "Height"], + data: [ + { + SampleID: "S1", + Species: "Homo sapiens", + Weight: 70.5, + Height: 175, + }, + { + SampleID: "S2", + Species: "Mus musculus", + Weight: 25.3, + Height: 150, + }, + ], + }, + }, dataIds: [2, 3], }, @@ -45,6 +85,7 @@ exports.selectionList = (req, res) => { "id": "string", "name": "string", // Optional "nbSamples": "number" // Optional + "metadata": "dict" // Optional }] */ try { @@ -59,6 +100,7 @@ exports.selectionList = (req, res) => { id: selection.id, name: selection.name, nbSamples: selection.nbSamples, + metadata: selection.metadata, }; }); From e0049dcf5e636370cf66a95b7db5c2eaefbd7b89 Mon Sep 17 00:00:00 2001 From: FadyCoding Date: Fri, 6 Sep 2024 11:09:33 +0200 Subject: [PATCH 2/3] random data provider --- src/controllers/debiai/info.js | 4 +- src/controllers/debiai/project_random.js | 307 +++++++++++++++++++++++ src/controllers/debiai/projects.js | 106 +++++--- 3 files changed, 379 insertions(+), 38 deletions(-) create mode 100644 src/controllers/debiai/project_random.js diff --git a/src/controllers/debiai/info.js b/src/controllers/debiai/info.js index b1fe73b..8a0fc82 100644 --- a/src/controllers/debiai/info.js +++ b/src/controllers/debiai/info.js @@ -24,8 +24,8 @@ exports.info = (req, res) => { */ const infoResponse = { version: version, - maxSampleIdByRequest: 10000, - maxSampleDataByRequest: 2000, + maxSampleIdByRequest: 100000, + maxSampleDataByRequest: 20000, maxResultByRequest: 5000, canDelete: { diff --git a/src/controllers/debiai/project_random.js b/src/controllers/debiai/project_random.js new file mode 100644 index 0000000..746c931 --- /dev/null +++ b/src/controllers/debiai/project_random.js @@ -0,0 +1,307 @@ +// This data provider will provide a number of project with a certain number of random data + +const sampleNumberToCreate = [ + // 10, + // 100, + 1000, + // 10000, + 50000, + // 100000, + // 1000000, + // 2000000, + 3000000, + // 4000000, + // 5000000, + // 6000000, + // 7000000, + // 8000000, + // 9000000, + // 10000000, +]; + +const projectToCreate = sampleNumberToCreate.map((sampleNumber) => { + return { name: `P with ${sampleNumber} samples`, sampleNumber }; +}); + +const randomContext = () => { + const context = ["A", "B", "C", "D", "E", "F", "G"]; + return context[Math.floor(Math.random() * context.length)]; +}; + +exports.info = (req, res) => { + // Return the list of projects with their columns and results + // 'project_1' is the project id, it will be used as a path parameter in the API + // 'name' is the name of the project + // 'columns' is the list of columns of the project data + // 'expectedResults' is the list of columns of the model results, it can be empty + // 'nbSamples' [optional] is the number of samples in the project data + /* projects = { + project_1: { + name: "Project 1", + columns: [ + { name: "Context 1", type: "text" }, + { name: "Ground thruth 1", type: "number" }, + { name: "Input 1", type: "number" }, + ], + expectedResults: [ + { name: "Model prediction", type: "number" }, + { name: "Model error", type: "number" }, + ], + nbSamples: 3, + } + } */ + try { + const projects = {}; + for (const project of projectToCreate) { + projects[project.name] = { + name: project.name, + columns: [ + { name: "Random Context", type: "text" }, + { name: "Random GDT", type: "number" }, + { name: "Input same as ID", type: "number" }, + ], + expectedResults: [ + { name: "Model prediction same as ID", type: "number" }, + { name: "Model error", type: "number" }, + ], + nbSamples: project.sampleNumber, + nbModels: 2, + }; + } + + res.status(200).send(projects); + } catch (error) { + console.log(error); + res.status(500).send(error); + } +}; + +exports.dataIdList = async (req, res) => { + // Return the list of the project data ids + try { + const requestedProjectId = req.openapi.pathParams.view; + const start = new Date(); + const requestedSampleNumber = projectToCreate.find( + (project) => project.name == requestedProjectId + ).sampleNumber; + + // The data ids are 1, 2, 3, they will be requested by DebiAI + // they can be in any format, but please avoid caracters like : / ( ) < > . ; or , + + // In case of a nulber of sample > 10000, we will ask for a sequensed amount of sample ID + // Set variables only if from & to in query parameters* + const from = req.query.from; + const to = req.query.to; + + // // Wait for 1 second to simulate a long request + // const msToWait = from === undefined ? 1000 : from / 20 + // console.log("\nWaiting " + (msToWait / 1000) + " s"); + // await new Promise(resolve => setTimeout(resolve, msToWait)); + + console.log( + `Requested ${requestedSampleNumber} samples from`, + from, + "to", + to + ); + + if (from !== undefined && to !== undefined) { + // Fetch data with from and to filter; + const projectDataIds = Array(to - from + 1) + .fill() + .map((_, i) => i + 1 + from); + console.log("ArrayCreation time: %dms", new Date() - start); + console.log("Sending data ids"); + // Add + 1 because slice function excluded last value + res.status(200).send(projectDataIds); + } else { + const projectDataIds = Array(requestedSampleNumber) + .fill() + .map((_, i) => i + 1); + console.log("ArrayCreation time: %dms", new Date() - start); + console.log("Sending data ids"); + res.status(200).send(projectDataIds); + } + console.log("Execution time: %dms", new Date() - start); + } catch (error) { + console.log(error); + res.status(500).send(error); + } +}; + +exports.data = (req, res) => { + // Return the data for the given data ids + try { + const requestedDataIds = req.body; // List of data ids requested by DebiAI + + projectData = {}; + for (const dataId of requestedDataIds) + projectData[dataId] = [randomContext(), Math.random() * 100, dataId]; + + res.status(200).send(projectData); + } catch (error) { + console.log(error); + res.status(500).send(error); + } +}; + +exports.modelList = (req, res) => { + // Return the list of the project models + try { + const requestedProjectId = req.openapi.pathParams.view; + const requestedSampleNumber = projectToCreate.find( + (project) => project.name == requestedProjectId + ).sampleNumber; + + const projectModels = [ + // 1 sample 1 result + { + id: "model_1", + name: "Model 1", + nbResults: requestedSampleNumber, + }, + // 2 samples 1 result + { + id: "model_2", + name: "Model 2", + nbResults: Math.floor(requestedSampleNumber / 2), + }, + ]; + + // The model ids are 'model_1' and 'model_2', they will be requested by DebiAI + // The name is optional, it will be replaced by the model id if not provided + // The nbResults is optional, it will be replaced by 0 if not provided + + res.status(200).send(projectModels); + } catch (error) { + console.log(error); + res.status(500).send(error); + } +}; + +exports.modelEvaluatedDataIdList = (req, res) => { + // Return a list of data ids that the model have been evaluated on + try { + const requestedProjectId = req.openapi.pathParams.view; + const requestedModelId = req.openapi.pathParams.modelId; + const requestedSampleNumber = projectToCreate.find( + (project) => project.name == requestedProjectId + ).sampleNumber; + + if (requestedModelId == "model_1") { + // 1 sample 1 result + const projectDataIds = Array(requestedSampleNumber) + .fill() + .map((_, i) => i + 1); + res.status(200).send(projectDataIds); + } else if (requestedModelId == "model_2") { + // 2 samples 1 result + const projectDataIds = Array(Math.floor(requestedSampleNumber / 2)) + .fill() + .map((_, i) => i * 2 + 1); + res.status(200).send(projectDataIds); + } else res.status(404).send("Model not found"); + } catch (error) { + console.log(error); + res.status(500).send(error); + } +}; + +exports.modelResults = (req, res) => { + // Return the model results for the given data ids + try { + const requestedProjectId = req.openapi.pathParams.view; + const requestedModelId = req.openapi.pathParams.modelId; + const requestedDataIds = req.body; + + const results = {}; + for (const dataId of requestedDataIds) + results[dataId] = [dataId, Math.random() * 100]; + res.status(200).send(results); + } catch (error) { + console.log(error); + res.status(500).send(error); + } +}; + +const selections = {}; +// Selections +exports.selectionList = (req, res) => { + // Return the project selections + /* + Response body : [{ + "id": "string", + "name": "string", + "nbSamples": "number" + }] + */ + try { + const requestedProjectId = req.openapi.pathParams.view; + if (selections[requestedProjectId] == undefined) + selections[requestedProjectId] = {}; + res.status(200).send(Object.values(selections[requestedProjectId])); + } catch (error) { + console.log(error); + res.status(500).send(error); + } +}; +exports.selectionDataIdList = (req, res) => { + // Return the list of a selection samples ids + /* + Response body : + ["id 1", 10, "id 3", "id 4", "39"] + */ + try { + const requestedProjectId = req.openapi.pathParams.view; + const requestedSelectionId = req.openapi.pathParams.selectionId; + + const idList = selections[requestedProjectId][requestedSelectionId].idList; + res.status(200).send(idList); + } catch (error) { + console.log(error); + res.status(500).send(error); + } +}; +exports.createSelection = (req, res) => { + // Return no content http response (204) + /* Create a selection from the idList ids given in request body + The route is called by DebiAI user Interface + Optionnal route + If the data provider is not designed to support creation, throw an error + + RequestBody: + { + "name": "my selection", + "idList": [ + "sample-1", + "sample-2", + "sample-3" + ] + } + */ + try { + const requestedProjectId = req.openapi.pathParams.view; + const requestedData = req.body; + console.log(requestedData); + const selectionId = requestedData.name; + + if (selections[requestedProjectId] == undefined) + selections[requestedProjectId] = {}; + + // The samples ids sent by DebiAI are in a str format + // We need to convert them to int + + const idList = requestedData.idList.map((id) => parseInt(id)); + + selections[requestedProjectId][selectionId] = { + id: selectionId, + name: requestedData.name, + idList: idList, + nbSamples: idList.length, + }; + res.status(204).end(); + } catch (error) { + console.log(error); + res.status(500).send(error); + } +}; diff --git a/src/controllers/debiai/projects.js b/src/controllers/debiai/projects.js index 31175f4..fac5617 100644 --- a/src/controllers/debiai/projects.js +++ b/src/controllers/debiai/projects.js @@ -15,18 +15,45 @@ // - Get the project data from a list of data ids // - Delete a project (optional) +// This data provider will provide a number of project with a certain number of random data + +const sampleNumberToCreate = [ + 10, + // 100, + 1000, 10000, + // 50000, + 100000, 1000000, 10000000, + // 3000000, + // 4000000, + // 5000000, + // 6000000, + // 7000000, + // 8000000, + // 9000000, + // 100000000000000000, +]; + +const projectToCreate = sampleNumberToCreate.map((sampleNumber) => { + return { name: `P with ${sampleNumber} samples`, sampleNumber }; +}); + +const randomContext = () => { + const context = ["A", "B", "C", "D", "E", "F", "G"]; + return context[Math.floor(Math.random() * context.length)]; +}; + exports.getProjectsOverview = async (req, res) => { // Return the list of projects with their names and values their numbers of samples, selections and models try { - const projects = { - project_1: { - name: "Project 1", - nbSamples: 10, - nbSelections: 2, - nbModels: 1, - }, - }; - + const projects = {}; + for (const project of projectToCreate) { + projects[project.name] = { + name: project.name, + nbSamples: project.sampleNumber, + nbSelections: 0, + nbModels: 0, + }; + } res.status(200).send(projects); } catch (error) { console.log(error); @@ -40,7 +67,11 @@ exports.getProject = async (req, res) => { Return a single project with his columns and results */ const projectId = req.openapi.pathParams.projectId; - + // Check if projectId exists + if (!projectToCreate.find((p) => p.name === projectId)) { + res.status(404).send("Can't find project " + projectId); + return; + } // Project value will be the column and expected results for the project const projectValue = { name: "Project 1", @@ -49,7 +80,6 @@ exports.getProject = async (req, res) => { { name: "Context 1", category: "context", type: "text" }, { name: "Ground truth 1", category: "groundtruth", type: "number" }, { name: "Input 1", category: "input" }, // type is not required, it will be detected automatically - { name: "Other col", group: "My group" }, // Category is not required, it will be set to "other" by default // You can also add a group to your columns, it will be used to group the columns in the interface // category can be : context, groundtruth, input, other. Default category is other @@ -62,15 +92,12 @@ exports.getProject = async (req, res) => { // For example, you can add the model predictions and some metrics like the model errors // You can also group the expected results in the interface with the group property ], - nbSamples: 10, + nbSamples: projectToCreate.find((p) => p.name === projectId).sampleNumber, }; // To set name, columns and expected results to the variable we send to Debiai - if (projectId == "project_1") { - res.status(200).send(projectValue); - } else { - res.status(404).send("Can't find project " + projectId); - } + res.status(200).send(projectValue); + console.log(projectId); } catch (error) { console.log(error); res.status(500).send(error); @@ -81,9 +108,13 @@ exports.dataIdList = async (req, res) => { // Return the list of the project data ids try { const requestedProjectId = req.openapi.pathParams.projectId; - - if (requestedProjectId !== "project_1") + const project = projectToCreate.find((p) => p.name === requestedProjectId); + if (project === undefined) { res.status(404).send("Can't find project " + requestedProjectId); + return; + } + + const requestedSampleNumber = project.sampleNumber; // DebiAI provide information about the analysis to help manage the data requests const analysis = { @@ -98,7 +129,6 @@ exports.dataIdList = async (req, res) => { // - end : A boolean to know if it's the last request for this analysis // We gather the data ids from the database - const projectDataIds = [1, 2, 3]; // The id of the data are 1, 2, 3, they will used by DebiAI to request the data // They can be in any format, but please avoid characters like : / ( ) < > . ; or , @@ -107,11 +137,24 @@ exports.dataIdList = async (req, res) => { const from = req.query.from; const to = req.query.to; + console.log( + `Requested ${requestedSampleNumber} samples from`, + from, + "to", + to + ); + if (from !== undefined && to !== undefined) { // Fetch data with from and to filter; + const projectDataIds = Array(to - from + 1) + .fill() + .map((_, i) => String(i + 1 + from)); // Add + 1 because slice function excluded last value - res.status(200).send(projectDataIds.slice(from, to + 1)); + res.status(200).send(projectDataIds); } else { + const projectDataIds = Array(requestedSampleNumber) + .fill() + .map((_, i) => String(i + 1)); res.status(200).send(projectDataIds); } } catch (error) { @@ -126,8 +169,11 @@ exports.data = (req, res) => { const requestedProjectId = req.openapi.pathParams.projectId; const requestedDataIds = req.body; // List of data ids requested by DebiAI - if (requestedProjectId !== "project_1") + const project = projectToCreate.find((p) => p.name === requestedProjectId); + if (project === undefined) { res.status(404).send("Can't find project " + requestedProjectId); + return; + } // DebiAI provide information about the analysis to help manage the data requests const analysis = { @@ -136,23 +182,11 @@ exports.data = (req, res) => { end: req.query.analysisEnd, }; - // If the requested ids are [1, 2, 3], the following data will be returned: - const projectData = { - 1: ["Context a", 11, 4, false], - 2: ["Context b", 23, 2, true], - 3: ["Context c", -2, 0, true], - }; - - // The object keys are the data ids and the object values are the data - // The data array MUST follow the columns order defined in the project info - // Data containing '', null or undefined aren't supported by DebiAI at the moment - // Data in a format other than string or number (array, objects, ...) aren't supported by DebiAI - const dataToReturn = {}; requestedDataIds.forEach((id) => { - dataToReturn[id] = projectData[id]; + // dataToReturn[id] = [randomContext(), Math.random() * 100, id]; + dataToReturn[id] = [0, 0, id]; }); - res.status(200).send(dataToReturn); } catch (error) { console.log(error); From 130236705975c8cd326423ab96b53887a7d81c6d Mon Sep 17 00:00:00 2001 From: "tom.mansion" Date: Thu, 12 Dec 2024 17:10:48 +0100 Subject: [PATCH 3/3] Improved name --- src/controllers/debiai/projects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/debiai/projects.js b/src/controllers/debiai/projects.js index fac5617..9fda0f9 100644 --- a/src/controllers/debiai/projects.js +++ b/src/controllers/debiai/projects.js @@ -34,7 +34,7 @@ const sampleNumberToCreate = [ ]; const projectToCreate = sampleNumberToCreate.map((sampleNumber) => { - return { name: `P with ${sampleNumber} samples`, sampleNumber }; + return { name: `${sampleNumber} samples project`, sampleNumber }; }); const randomContext = () => {