Skip to content

Commit

Permalink
Extended functionality in EnsembleParameters
Browse files Browse the repository at this point in the history
  • Loading branch information
sigurdp committed Sep 8, 2023
1 parent e0419b3 commit b3ddec9
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 20 deletions.
108 changes: 99 additions & 9 deletions frontend/src/framework/EnsembleParameters.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,113 @@
export type Parameter = {
import { MinMax } from "@lib/utils/MinMax";

export enum ParameterType {
CONTINUOUS,
DISCRETE,
}

export type ContinuousParameter = {
readonly type: ParameterType.CONTINUOUS;
readonly name: string;
readonly groupName: string | null;
readonly description?: string;
readonly isConstant: boolean;
readonly isLogarithmic: boolean;
readonly realizations: number[]; // The two arrays, realizations and values, must always be same length
readonly values: number[]; // Array items will be floating point.
};

export type DiscreteParameter = {
readonly type: ParameterType.DISCRETE;
readonly name: string;
readonly isLogarithmic: boolean; // Only applicable for numerical/float arrays?
readonly isNumerical: boolean;
readonly groupName: string | null;
readonly description?: string;
readonly isConstant: boolean;
readonly groupName?: string;
readonly descriptiveName?: string;
readonly realizations: number[]; // The two arrays, realizations and values, must always be same length
readonly values: number[] | string[]; // Array items can be string, int or float. Should probably be Float32Array, Int32Array or string[] instead.
readonly realizations: number[]; // The two arrays, realizations and values, must always be same length
readonly values: number[] | string[]; // Array items can be string or int. Should maybe utilize Int32Array or string[] instead?
};

export type Parameter = ContinuousParameter | DiscreteParameter;

export class ParameterIdent {
readonly name: string;
readonly groupName: string | null;

constructor(name: string, groupName: string | null) {
this.name = name;
this.groupName = groupName;
}

static fromNameAndGroup(name: string, groupName: string | null): ParameterIdent {
return new ParameterIdent(name, groupName);
}

static fromString(paramIdentString: string): ParameterIdent {
const parts = paramIdentString.split("~@@~");
if (parts.length === 1) {
return new ParameterIdent(parts[0], null);
} else if (parts.length === 2) {
return new ParameterIdent(parts[0], parts[1]);
} else {
throw new Error(`Invalid parameter ident string: ${paramIdentString}`);
}
}

toString(): string {
if (this.groupName) {
return `${this.name}~@@~${this.groupName}`;
} else {
return this.name;
}
}
}

export class EnsembleParameters {
private _parameterArr: Parameter[];

constructor(parameterArr: Parameter[]) {
this._parameterArr = parameterArr;
}

getParameterNames(): string[] {
return this._parameterArr.map((par) => par.name);
getParameterIdents(requiredParamType: ParameterType | null): ParameterIdent[] {
const identArr: ParameterIdent[] = [];
for (const par of this._parameterArr) {
if (requiredParamType == null || par.type === requiredParamType) {
identArr.push({ name: par.name, groupName: par.groupName });
}
}

return identArr;
}

hasParameter(paramIdent: ParameterIdent): boolean {
return this.findParameter(paramIdent) != null;
}

getParameter(paramIdent: ParameterIdent): Parameter {
const par = this.findParameter(paramIdent);
if (!par) {
throw new Error(`Parameter ${paramIdent.name} (group=${paramIdent.groupName}) not found`);
}
return par;
}

getContinuousParameterMinMax(paramIdent: ParameterIdent): MinMax {
const par = this.getParameter(paramIdent);
if (par.type !== ParameterType.CONTINUOUS) {
throw new Error(`Parameter ${paramIdent.name} (group=${paramIdent.groupName}) is not of type continuous`);
}

return MinMax.fromNumericValues(par.values);
}

findParameter(paramIdent: ParameterIdent): Parameter | null {
for (const par of this._parameterArr) {
if (par.name === paramIdent.name && par.groupName === paramIdent.groupName) {
return par;
}
}

return null;
}

getParameterArr(): readonly Parameter[] {
Expand Down
37 changes: 26 additions & 11 deletions frontend/src/framework/internal/EnsembleSetLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { QueryClient } from "@tanstack/react-query";

import { Ensemble } from "../Ensemble";
import { EnsembleIdent } from "../EnsembleIdent";
import { Parameter } from "../EnsembleParameters";
import { Parameter, ParameterType, ContinuousParameter, DiscreteParameter } from "../EnsembleParameters";
import { Sensitivity, SensitivityCase } from "../EnsembleSensitivities";
import { EnsembleSet } from "../EnsembleSet";

Expand Down Expand Up @@ -129,16 +129,31 @@ function buildParameterArrFromApiResponse(apiParameterArr: EnsembleParameter_api
const retParameterArr: Parameter[] = [];

for (const apiPar of apiParameterArr) {
retParameterArr.push({
name: apiPar.name,
isLogarithmic: apiPar.is_logarithmic,
isNumerical: apiPar.is_numerical,
isConstant: apiPar.is_constant,
groupName: apiPar.group_name,
descriptiveName: apiPar.descriptive_name,
realizations: apiPar.realizations,
values: apiPar.values,
});
if (apiPar.is_numerical) {
const retPar: ContinuousParameter = {
type: ParameterType.CONTINUOUS,
name: apiPar.name,
groupName: apiPar.group_name ?? null,
description: apiPar.descriptive_name,
isConstant: apiPar.is_constant,
isLogarithmic: apiPar.is_logarithmic,
realizations: apiPar.realizations,
values: apiPar.values as number[],
};
retParameterArr.push(retPar);
}
else {
const retPar: DiscreteParameter = {
type: ParameterType.DISCRETE,
name: apiPar.name,
groupName: apiPar.group_name ?? null,
description: apiPar.descriptive_name,
isConstant: apiPar.is_constant,
realizations: apiPar.realizations,
values: apiPar.values,
};
retParameterArr.push(retPar);
}
}

return retParameterArr;
Expand Down
64 changes: 64 additions & 0 deletions frontend/src/lib/utils/MinMax.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Immutable class for storing a min/max scalar range
*/

export class MinMax {
readonly min: number;
readonly max: number;

constructor(min: number, max: number) {
this.min = min;
this.max = max;
}

static createInvalid(): MinMax {
return new MinMax(Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY);
}

static fromNumericValues(values: Iterable<number>): MinMax {
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
for (const v of values) {
if (v < min) {
min = v;
}
if (v > max) {
max = v;
}
}

if (min <= max) {
return new MinMax(min, max);
} else {
return MinMax.createInvalid();
}
}

/**
* Returns a new MinMax object that is extend to include values from other min/max range
* Will handle invalid ranges (min > max) in both this and the other object.
*/
extendedBy(otherMinMax: MinMax): MinMax {
if (!otherMinMax.isValid()) {
return new MinMax(this.min, this.max);
}
if (!this.isValid()) {
return new MinMax(otherMinMax.min, otherMinMax.max);
}

const newMin = Math.min(this.min, otherMinMax.min);
const newMax = Math.max(this.max, otherMinMax.max);
return new MinMax(newMin, newMax);
}

/**
* Returns true if the range is valid, i.e. if minimum <= maximum
*/
isValid(): boolean {
if (this.min <= this.max) {
return true;
} else {
return false;
}
}
}
99 changes: 99 additions & 0 deletions frontend/tests/unit-tests/EnsembleParameters.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { EnsembleParameters, Parameter, ParameterIdent, ParameterType } from "@framework/EnsembleParameters";
import { MinMax } from "@lib/utils/MinMax";

// prettier-ignore
const PARAM_ARR: Parameter[] = [
{type: ParameterType.CONTINUOUS, name: "cparam_10", groupName: null, description: "desc10", isConstant: false, isLogarithmic: false, realizations: [1,2,3], values: [11, 12, 19]},
{type: ParameterType.CONTINUOUS, name: "cparam_20", groupName: null, description: "desc20", isConstant: false, isLogarithmic: false, realizations: [1,2,3], values: [21, 22, 29]},
{type: ParameterType.CONTINUOUS, name: "cparam_50", groupName: "grp1", description: "desc50g1", isConstant: false, isLogarithmic: false, realizations: [1,2,3], values: [51, 52, 54]},
{type: ParameterType.CONTINUOUS, name: "cparam_50", groupName: "grp2", description: "desc50g2", isConstant: false, isLogarithmic: false, realizations: [1,2,3], values: [55, 56, 59]},

{type: ParameterType.DISCRETE, name: "dparam_A", groupName: null, description: "descA", isConstant: false, realizations: [1,2,3], values: [1, 2, 3]},
{type: ParameterType.DISCRETE, name: "dparam_B", groupName: null, description: "descB", isConstant: false, realizations: [1,2,3], values: ["A", "B", "C"]},
];


describe("EnsembleParameters tests", () => {
test("Get list of parameter idents", () => {
const ensParams = new EnsembleParameters(PARAM_ARR);
{
const allIdents = ensParams.getParameterIdents(null);
expect(allIdents.length).toEqual(6);
expect(allIdents[0]).toEqual(ParameterIdent.fromNameAndGroup("cparam_10", null));
expect(allIdents[1]).toEqual(ParameterIdent.fromNameAndGroup("cparam_20", null));
expect(allIdents[2]).toEqual(ParameterIdent.fromNameAndGroup("cparam_50", "grp1"));
expect(allIdents[3]).toEqual(ParameterIdent.fromNameAndGroup("cparam_50", "grp2"));
expect(allIdents[4]).toEqual(ParameterIdent.fromNameAndGroup("dparam_A", null));
expect(allIdents[5]).toEqual(ParameterIdent.fromNameAndGroup("dparam_B", null));
}
{
const contIdents = ensParams.getParameterIdents(ParameterType.CONTINUOUS);
expect(contIdents.length).toEqual(4);
expect(contIdents[0]).toEqual(ParameterIdent.fromNameAndGroup("cparam_10", null));
expect(contIdents[1]).toEqual(ParameterIdent.fromNameAndGroup("cparam_20", null));
expect(contIdents[2]).toEqual(ParameterIdent.fromNameAndGroup("cparam_50", "grp1"));
expect(contIdents[3]).toEqual(ParameterIdent.fromNameAndGroup("cparam_50", "grp2"));
}
{
const discIdents = ensParams.getParameterIdents(ParameterType.DISCRETE);
expect(discIdents.length).toEqual(2);
expect(discIdents[0]).toEqual(ParameterIdent.fromNameAndGroup("dparam_A", null));
expect(discIdents[1]).toEqual(ParameterIdent.fromNameAndGroup("dparam_B", null));
}
});

test("Check for parameter existence", () => {
const ensParams = new EnsembleParameters(PARAM_ARR);

expect(ensParams.hasParameter(ParameterIdent.fromNameAndGroup("cparam_10", null))).toBe(true);
expect(ensParams.hasParameter(ParameterIdent.fromNameAndGroup("cparam_50", "grp1"))).toBe(true);
expect(ensParams.hasParameter(ParameterIdent.fromNameAndGroup("cparam_50", "grp2"))).toBe(true);

expect(ensParams.hasParameter(ParameterIdent.fromNameAndGroup("aName", "aGroup"))).toBe(false);
expect(ensParams.hasParameter(ParameterIdent.fromNameAndGroup("", ""))).toBe(false);
expect(ensParams.hasParameter(ParameterIdent.fromNameAndGroup("cparam_10", ""))).toBe(false);
expect(ensParams.hasParameter(ParameterIdent.fromNameAndGroup("cparam_50", null))).toBe(false);
});

test("Get parameters", () => {
const ensParams = new EnsembleParameters(PARAM_ARR);
{
const par = ensParams.getParameter(ParameterIdent.fromNameAndGroup("cparam_10", null));
expect(par.type).toEqual(ParameterType.CONTINUOUS);
expect(par.name).toEqual("cparam_10");
expect(par.groupName).toEqual(null);
expect(par.values).toEqual([11, 12, 19]);
}
{
const par = ensParams.getParameter(ParameterIdent.fromNameAndGroup("cparam_50", "grp2"));
expect(par.type).toEqual(ParameterType.CONTINUOUS);
expect(par.name).toEqual("cparam_50");
expect(par.groupName).toEqual("grp2");
expect(par.values).toEqual([55, 56, 59]);
}
{
const par = ensParams.getParameter(ParameterIdent.fromNameAndGroup("dparam_B", null));
expect(par.type).toEqual(ParameterType.DISCRETE);
expect(par.name).toEqual("dparam_B");
expect(par.groupName).toEqual(null);
expect(par.values).toEqual(["A", "B", "C"]);
}
});

test("Check that getting non-existing parameter throws", () => {
const ensParams = new EnsembleParameters(PARAM_ARR);
expect(() => ensParams.getParameter(ParameterIdent.fromNameAndGroup("someBogusName", null))).toThrow();
});

test("Test getting min/max values for continuous parameter", () => {
const ensParams = new EnsembleParameters(PARAM_ARR);
{
const minMax = ensParams.getContinuousParameterMinMax(ParameterIdent.fromNameAndGroup("cparam_10", null));
expect(minMax).toEqual(new MinMax(11, 19));
}
{
const minMax = ensParams.getContinuousParameterMinMax(ParameterIdent.fromNameAndGroup("cparam_50", "grp1"));
expect(minMax).toEqual(new MinMax(51, 54));
}
});
});
42 changes: 42 additions & 0 deletions frontend/tests/unit-tests/MinMax.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { MinMax } from "@lib/utils/MinMax";

describe("MinMax tests", () => {
test("Check validity of MinMax instances", () => {
expect(new MinMax(0, 1).isValid()).toBe(true);
expect(new MinMax(-1, -1).isValid()).toBe(true);
expect(new MinMax(0, Number.POSITIVE_INFINITY).isValid()).toBe(true);
expect(new MinMax(Number.NEGATIVE_INFINITY, 0).isValid()).toBe(true);

expect(MinMax.createInvalid().isValid()).toBe(false);
expect(new MinMax(1, 0).isValid()).toBe(false);
expect(new MinMax(0, Number.NaN).isValid()).toBe(false);
expect(new MinMax(Number.NaN, 0).isValid()).toBe(false);
expect(new MinMax(Number.NaN, Number.NaN).isValid()).toBe(false);
});

test("Check construction from numeric values", () => {
expect(MinMax.fromNumericValues([]).isValid()).toBe(false);
expect(MinMax.fromNumericValues([1])).toEqual(new MinMax(1, 1));
expect(MinMax.fromNumericValues([0, 1, 2, 3, 4])).toEqual(new MinMax(0, 4));

expect(MinMax.fromNumericValues(new Float32Array([0, 1, 2, 3, 4]))).toEqual(new MinMax(0, 4));
expect(MinMax.fromNumericValues(new Set<number>([0, 1, 2, 3, 4]))).toEqual(new MinMax(0, 4));

const bogusArray = [1, undefined, 2, Number.NaN, 3, 4];
expect(MinMax.fromNumericValues(bogusArray as number[])).toEqual(new MinMax(1, 4));
});

test("Check extending by another MinMax object", () => {
const validMinMaxA = new MinMax(0, 1);
const validMinMaxB = new MinMax(10, 11);
const invalidMinMax = MinMax.createInvalid();

expect(validMinMaxA.extendedBy(validMinMaxA)).toEqual(validMinMaxA);
expect(invalidMinMax.extendedBy(validMinMaxA)).toEqual(validMinMaxA);
expect(validMinMaxA.extendedBy(invalidMinMax)).toEqual(validMinMaxA);

expect(validMinMaxA.extendedBy(validMinMaxB)).toEqual(new MinMax(0, 11));

expect(invalidMinMax.extendedBy(invalidMinMax).isValid()).toBe(false);
});
});

0 comments on commit b3ddec9

Please sign in to comment.