diff --git a/api/src/common/interfaces/utilities.interface.ts b/api/src/common/interfaces/utilities.interface.ts index db02ef99e3f4c67f65f320f1d0bfe1a2a6b832f5..0a0978c5ab64ef2474bea31709697c3d002a5456 100644 --- a/api/src/common/interfaces/utilities.interface.ts +++ b/api/src/common/interfaces/utilities.interface.ts @@ -3,6 +3,7 @@ import { ResultUnion } from 'src/engine/models/result/common/result-union.model' export type Dictionary<T> = { [key: string]: T }; export type ExperimentResult = typeof ResultUnion; +export type AlgoResults = ExperimentResult[]; export enum MIME_TYPES { ERROR = 'text/plain+error', diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/area.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/area.handler.ts new file mode 100644 index 0000000000000000000000000000000000000000..198175a884dce0f40293a5cfec038f3de2920973 --- /dev/null +++ b/api/src/engine/connectors/exareme/handlers/algorithms/area.handler.ts @@ -0,0 +1,49 @@ +import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata' +import { AlgoResults } from 'src/common/interfaces/utilities.interface'; +import { ResultChartExperiment } from '../../interfaces/experiment/result-chart-experiment.interface'; +import BaseHandler from '../base.handler'; + +export default class AreaHandler extends BaseHandler { + static readonly transform = jsonata(` + ({ + "name": data.title.text, + "xAxis": { + "label": data.xAxis.title.text + }, + "yAxis": { + "label": data.yAxis.title.text + }, + "lines": [ + { + "label": "ROC curve", + "x": data.series.data.$[0], + "y": data.series.data.$[1], + "type": 0 + } + ] + }) + `); + + canHandle(input: ResultChartExperiment): boolean { + return ( + input.type === 'application/vnd.highcharts+json' && + input.data.chart.type === 'area' + ); + } + + handle(algorithm: string, data: unknown, res: AlgoResults): void { + let req = data; + const inputs = data as ResultChartExperiment[]; + + if (inputs) { + inputs + .filter(this.canHandle) + .map((input) => AreaHandler.transform.evaluate(input)) + .forEach((input) => res.push(input)); + + req = JSON.stringify(inputs.filter((input) => !this.canHandle(input))); + } + + this.next?.handle(algorithm, req, res); + } +} diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/descriptive.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/descriptive.handler.ts new file mode 100644 index 0000000000000000000000000000000000000000..a314a6bf6840e6b447a2ee5ddd881244b5a10793 --- /dev/null +++ b/api/src/engine/connectors/exareme/handlers/algorithms/descriptive.handler.ts @@ -0,0 +1,131 @@ +import { AlgoResults } from 'src/common/interfaces/utilities.interface'; +import { + GroupResult, + GroupsResult, +} from 'src/engine/models/result/groups-result.model'; +import { ResultExperiment } from '../../interfaces/experiment/result-experiment.interface'; +import BaseHandler from '../base.handler'; +import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata' + +export default class DescriptiveHandler extends BaseHandler { + private static readonly headerDescriptive = ` +$fnum := function($x) { $type($x) = 'number' ? $round($number($x),3) : $x }; + +$e := function($x, $r) {($x != null) ? $fnum($x) : ($r ? $r : '')}; + +$fn := function($o, $prefix) { + $type($o) = 'object' ? + $each($o, function($v, $k) {( + $type($v) = 'object' ? { $k: $v.count & ' (' & $v.percentage & '%)' } : { + $k: $v + } + )}) ~> $merge() + : {} +};`; + + static readonly descriptiveModelToTables = jsonata(` +( + ${this.headerDescriptive} + + $vars := $count($keys(data.model.*.data))-1; + $varNames := $keys(data.model.*.data); + $model := data.model; + + [[0..$vars].( + $i := $; + $varName := $varNames[$i]; + $ks := $keys($model.*.data.*[$i][$type($) = 'object']); + { + 'name': $varName, + 'headers': $append("", $keys($$.data.model)).{ + 'name': $, + 'type': 'string' + }, + 'data': [ + [$varName, $model.*.($e(num_total))], + ['Datapoints', $model.*.($e(num_datapoints))], + ['Nulls', $model.*.($e(num_nulls))], + ($lookup($model.*.data, $varName).($fn($)) ~> $reduce(function($a, $b) { + $map($ks, function($k) {( + { + $k : [$e($lookup($a,$k), "No data"), $e($lookup($b,$k), "No data")] + } + )}) ~> $merge() + })).$each(function($v, $k) {$append($k,$v)})[] + ] + } + )] +)`); + + static readonly descriptiveSingleToTables = jsonata(` +( + ${this.headerDescriptive} + + data.[ + $.single.*@$p#$i.( + $ks := $keys($p.*.data[$type($) = 'object']); + { + 'name': $keys(%)[$i], + 'headers': $append("", $keys(*)).{ + 'name': $, + 'type': 'string' + }, + 'data' : [ + [$keys(%)[$i], $p.*.($e(num_total))], + ['Datapoints', $p.*.($e(num_datapoints))], + ['Nulls', $p.*.($e(num_nulls))], + ($p.*.data.($fn($)) ~> $reduce(function($a, $b) { + $map($ks, function($k) {( + { + $k : [$e($lookup($a,$k), "No data"), $e($lookup($b,$k), "No data")] + } + )}) ~> $merge() + })).$each(function($v, $k) {$append($k,$v)})[] + ] + }) + ] +) +`); + + descriptiveDataToTableResult(data: ResultExperiment): GroupsResult { + const result = new GroupsResult(); + + result.groups = [ + new GroupResult({ + name: 'Variables', + description: 'Descriptive statistics for the variables of interest.', + results: DescriptiveHandler.descriptiveSingleToTables.evaluate(data), + }), + ]; + + result.groups.push( + new GroupResult({ + name: 'Model', + description: + 'Intersection table for the variables of interest as it appears in the experiment.', + results: DescriptiveHandler.descriptiveModelToTables.evaluate(data), + }), + ); + + return result; + } + + handle(algorithm: string, data: unknown, res: AlgoResults): void { + let req = data; + + if (algorithm.toLowerCase() === 'descriptive_stats') { + const inputs = data as ResultExperiment[]; + + inputs + .filter((input) => input.type === 'application/json') + .map((input) => this.descriptiveDataToTableResult(input)) + .forEach((input) => res.push(input)); + + req = JSON.stringify( + inputs.filter((input) => input.type !== 'application/json'), + ); + } + + this.next?.handle(algorithm, req, res); + } +} diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/heat-map.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/heat-map.handler.ts new file mode 100644 index 0000000000000000000000000000000000000000..20591ad7589b9e075d2266a22ae5ccd7f0acd7a8 --- /dev/null +++ b/api/src/engine/connectors/exareme/handlers/algorithms/heat-map.handler.ts @@ -0,0 +1,46 @@ +import { AlgoResults } from 'src/common/interfaces/utilities.interface'; +import { ResultChartExperiment } from '../../interfaces/experiment/result-chart-experiment.interface'; +import BaseHandler from '../base.handler'; +import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata' + +export default class HeatMapHandler extends BaseHandler { + static readonly transform = jsonata(` + ( + { + "name": data.title.text, + "xAxis": { + "categories": data.xAxis.categories, + "label": data.xAxis.label + }, + "yAxis": { + "categories": data.yAxis.categories, + "label": data.yAxis.label + }, + "matrix": $toMat(data.series.data) + } + ) + `); + + canHandle(input: ResultChartExperiment): boolean { + return ( + input.type.toLowerCase() === 'application/vnd.highcharts+json' && + input.data.chart.type.toLowerCase() === 'heatmap' + ); + } + + handle(algorithm: string, data: unknown, res: AlgoResults): void { + let req = data; + const inputs = data as ResultChartExperiment[]; + + if (inputs) { + inputs + .filter(this.canHandle) + .map((input) => HeatMapHandler.transform.evaluate(input)) + .forEach((input) => res.push(input)); + + req = JSON.stringify(inputs.filter((input) => !this.canHandle(input))); + } + + this.next?.handle(algorithm, req, res); + } +} diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts new file mode 100644 index 0000000000000000000000000000000000000000..16702657df654d7c282efe54094ce46e4fb5e497 --- /dev/null +++ b/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts @@ -0,0 +1,47 @@ +import { Expression } from 'jsonata'; +import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata' +import { AlgoResults } from 'src/common/interfaces/utilities.interface'; +import { HeatMapResult } from 'src/engine/models/result/heat-map-result.model'; +import BaseHandler from '../base.handler'; + +export default class PearsonHandler extends BaseHandler { + readonly transform: Expression = jsonata(` + ( + $params := ['correlations', 'p-values', 'low_confidence_intervals', 'high_confidence_intervals']; + + $.$sift(function($v, $k) {$k in $params}).$each(function($v, $k) { + { + 'name': $k, + 'xAxis': { + 'categories': $v.variables + }, + 'yAxis': { + 'categories': $keys($v.$sift(function($val, $key) {$key ~> /^(?!variables$)/})) + }, + 'matrix': $v.$sift(function($val, $key) {$key ~> /^(?!variables$)/}).$each(function($val, $key) {$val})[] + } + }) + )`); + + canHandle(algorithm: string): boolean { + return algorithm.toLocaleLowerCase() === 'pearson'; + } + + handle(algorithm: string, data: unknown, res: AlgoResults): void { + if (this.canHandle(algorithm)) { + try { + const results = this.transform.evaluate(data) as HeatMapResult[]; + results + .filter((heatMap) => heatMap.matrix.length > 0 && heatMap.name) + .forEach((heatMap) => res.push(heatMap)); + } catch (e) { + PearsonHandler.logger.warn( + 'An error occur when converting result from Pearson', + ); + PearsonHandler.logger.verbose(JSON.stringify(data)); + } + } + + this.next?.handle(algorithm, data, res); + } +} diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/raw.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/raw.handler.ts new file mode 100644 index 0000000000000000000000000000000000000000..810be82cecd06bb193adaefb88fc50ccbdaa2fad --- /dev/null +++ b/api/src/engine/connectors/exareme/handlers/algorithms/raw.handler.ts @@ -0,0 +1,29 @@ +import { + AlgoResults, + MIME_TYPES, +} from 'src/common/interfaces/utilities.interface'; +import { RawResult } from 'src/engine/models/result/raw-result.model'; +import { ResultExperiment } from '../../interfaces/experiment/result-experiment.interface'; +import BaseHandler from '../base.handler'; + +export default class RawHandler extends BaseHandler { + dataToRaw = (algo: string, result: ResultExperiment): RawResult => { + let data = result; + + if (algo === 'CART') { + data = { ...data, type: MIME_TYPES.JSONBTREE }; + } + + return { rawdata: data }; + }; + + handle(algorithm: string, data: unknown, res: AlgoResults): void { + const inputs = data as ResultExperiment[]; + + inputs + .map((input) => this.dataToRaw(algorithm, input)) + .forEach((input) => res.push(input)); + + this.next?.handle(algorithm, data, res); + } +} diff --git a/api/src/engine/connectors/exareme/handlers/base.handler.ts b/api/src/engine/connectors/exareme/handlers/base.handler.ts new file mode 100644 index 0000000000000000000000000000000000000000..2f72f190258e37f1bd689cdd6163b32699586b52 --- /dev/null +++ b/api/src/engine/connectors/exareme/handlers/base.handler.ts @@ -0,0 +1,18 @@ +import { Logger } from '@nestjs/common'; +import { AlgoResults } from 'src/common/interfaces/utilities.interface'; +import ResultHandler from './result-handler.interface'; + +export default abstract class BaseHandler implements ResultHandler { + protected static readonly logger = new Logger(this.name); + + next: ResultHandler = null; + + setNext(h: ResultHandler): ResultHandler { + this.next = h; + return h; + } + + handle(algorithm: string, data: unknown, res: AlgoResults): void { + this.next?.handle(algorithm, data, res); + } +} diff --git a/api/src/engine/connectors/exareme/handlers/index.ts b/api/src/engine/connectors/exareme/handlers/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e153ece232bc26a5a2de18d65265bdc1b34f0eb --- /dev/null +++ b/api/src/engine/connectors/exareme/handlers/index.ts @@ -0,0 +1,19 @@ +import { AlgoResults } from 'src/common/interfaces/utilities.interface'; +import AreaHandler from './algorithms/area.handler'; +import DescriptiveHandler from './algorithms/descriptive.handler'; +import HeatMapHandler from './algorithms/heat-map.handler'; +import { + default as PearsonHandler, + default as RawHandler, +} from './algorithms/raw.handler'; + +const last = new RawHandler(); // should be last handler as it works as a fallback (if other handlers could not process the results) +const start = new PearsonHandler() + .setNext(new AreaHandler()) + .setNext(new DescriptiveHandler()) + .setNext(new HeatMapHandler()) + .setNext(last); + +export default (algo: string, data: unknown, res: AlgoResults) => { + start.handle(algo, data, res); +}; diff --git a/api/src/engine/connectors/exareme/handlers/result-handler.interface.ts b/api/src/engine/connectors/exareme/handlers/result-handler.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..93f98a9e3043a793a08b233ae66965ebc1e93495 --- /dev/null +++ b/api/src/engine/connectors/exareme/handlers/result-handler.interface.ts @@ -0,0 +1,7 @@ +import { AlgoResults } from 'src/common/interfaces/utilities.interface'; + +// produce algo handler +export default interface ResultHandler { + setNext(h: ResultHandler): ResultHandler; + handle(algorithm: string, data: unknown, res: AlgoResults): void; +} diff --git a/api/src/engine/models/result/common/chart-axis.model.ts b/api/src/engine/models/result/common/chart-axis.model.ts index 16fc1f68a10e379b0bf4e45225c7912710d1d34d..1b9bc68960fdbbb89241794afb4f827957d23c96 100644 --- a/api/src/engine/models/result/common/chart-axis.model.ts +++ b/api/src/engine/models/result/common/chart-axis.model.ts @@ -2,9 +2,13 @@ import { ObjectType, Field } from '@nestjs/graphql'; @ObjectType() export class ChartAxis { - @Field({ nullable: true, defaultValue: '' }) + @Field({ nullable: true, defaultValue: '', description: 'label of the Axis' }) label?: string; - @Field(() => [String], { nullable: true, defaultValue: [] }) + @Field(() => [String], { + nullable: true, + defaultValue: [], + description: 'label of each element on this Axis', + }) categories?: string[]; } diff --git a/api/src/schema.gql b/api/src/schema.gql index 4d248dc43d05871c60002fd581ba9a9a41f0ff07..b940e0ec06d23a0e87f41a791a669163fb88fe47 100644 --- a/api/src/schema.gql +++ b/api/src/schema.gql @@ -133,7 +133,10 @@ type LineChartResult { } type ChartAxis { + """label of the Axis""" label: String + + """label of each element on this Axis""" categories: [String!] }