diff --git a/src/lib/index.ts b/src/lib/index.ts index feea642..e6f0637 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -4,3 +4,4 @@ export * from './ccf'; export * from './teads-aws'; export * from './teads-curve'; export * from './watt-time'; +export * from './mock-observations'; diff --git a/src/lib/mock-observations/README.md b/src/lib/mock-observations/README.md new file mode 100644 index 0000000..8f170f6 --- /dev/null +++ b/src/lib/mock-observations/README.md @@ -0,0 +1,81 @@ +# Mock Observations Model + +> [!NOTE] +> `Mock-observations` is a community model, not part of the IF standard library. This means the IF core team are not closely monitoring these models to keep them up to date. You should do your own research before implementing them! + +## Introduction + +A model for mocking observations (inputs) for testing and demo purposes + +## Scope + +The mode currently mocks 2 types of observation data: +- Common key-value pairs, that are generated statically and are the same for each generated observation/input (see 'helpers/CommonGenerator.ts') +- Randomly generated integer values for predefined keys (see 'helpers/RandIntGenerator.ts') + + +## Implementation + +The model's 'config' section in the impl file determines its behaviour. +('inputs' section is ignored). +Config section will have the the following fields (see example below): +```yaml +timestamp-from +timestamp-to +duration +components +generators +``` +- **timestamp-from**, **timestamp-to** and **duration** define time buckets for which to generate observations. +- **generators** define which fields to generate for each observations +- **components** define the components for which to generate observations for. The observations generated according to **timestamp-from**, **timestamp-to**, **duration** and **generators** will be duplicated for each component. +All generators implement the Generator interface, see 'interfaces/index.ts' +For more information on the models spec please refer to https://github.com/Green-Software-Foundation/if/issues/332 + + +### Authentication + +N/A + +### inputs + +The model's 'config' section in the impl file determines its behaviour. +'inputs' section is ignored. + +### Typescript Usage +```typescript +const mock_obs_model = await new MockObservations().configure('mock-observations', { + timestamp-from: 2023-07-06T00:00 + timestamp-to: 2023-07-06T00:10 + duration: 60 + components: + - instance-type: A1 + generators: + common: + region: uk-west +}); +const inputs = [{}]; +const results = mock_obs_model.execute(inputs); +``` + +### IMPL Example +```yaml + mock-observations: + timestamp-from: 2023-07-06T00:00 + timestamp-to: 2023-07-06T00:10 + duration: 60 + components: + - instance-type: A1 + - instance-type: B1 + generators: + common: + region: uk-west + common-key: common-val + randint: + cpu-util: + min: 1 + max: 99 + mem-util: + min: 1 + max: 99 +``` diff --git a/src/lib/mock-observations/helpers/CommonGenerator.ts b/src/lib/mock-observations/helpers/CommonGenerator.ts new file mode 100644 index 0000000..23a8513 --- /dev/null +++ b/src/lib/mock-observations/helpers/CommonGenerator.ts @@ -0,0 +1,52 @@ +import {Generator} from '../interfaces'; +import * as yaml from 'js-yaml'; +import {ERRORS} from '../../../util/errors'; +import {buildErrorMessage} from '../../../util/helpers'; +const {InputValidationError} = ERRORS; + +class CommonGenerator implements Generator { + private name = ''; + private generateObject: {} = {}; + errorBuilder = buildErrorMessage(CommonGenerator); + + initialise(name: string, config: {[key: string]: any}): void { + this.name = this.validateName(name); + this.generateObject = this.validateConfig(config); + } + + next(_historical: Object[]): Object { + return Object.assign({}, this.generateObject); + } + + public getName(): String { + return this.name; + } + + private validateName(name: string | null): string { + if (name === null || name.trim() === '') { + throw new InputValidationError( + this.errorBuilder({message: 'name is null'}) + ); + } + return name; + } + + private validateConfig(config: {[key: string]: any}): {[key: string]: any} { + if (!config || Object.keys(config).length === 0) { + throw new InputValidationError( + this.errorBuilder({message: 'Config must not be null or empty'}) + ); + } + let ret: {[key: string]: any} = {}; + try { + ret = yaml.load(JSON.stringify({...config})) as {[key: string]: any}; + } catch (error) { + throw new InputValidationError( + this.errorBuilder({message: 'Invalid YML structure'}) + ); + } + return ret; + } +} + +export default CommonGenerator; diff --git a/src/lib/mock-observations/helpers/RandIntGenerator.ts b/src/lib/mock-observations/helpers/RandIntGenerator.ts new file mode 100644 index 0000000..190f572 --- /dev/null +++ b/src/lib/mock-observations/helpers/RandIntGenerator.ts @@ -0,0 +1,62 @@ +import {Generator} from '../interfaces'; +import {ERRORS} from '../../../util/errors'; +import {buildErrorMessage} from '../../../util/helpers'; +const {InputValidationError} = ERRORS; + +class RandIntGenerator implements Generator { + private static readonly MIN: string = 'min'; + private static readonly MAX: string = 'max'; + private fieldToPopulate = ''; + private min = 0; + private max = 0; + errorBuilder = buildErrorMessage(RandIntGenerator); + + initialise(fieldToPopulate: string, config: {[key: string]: any}): void { + this.fieldToPopulate = this.validateName(fieldToPopulate); + this.validateConfig(config); + this.min = config[RandIntGenerator.MIN]; + this.max = config[RandIntGenerator.MAX]; + } + + next(_historical: Object[]): Object { + const retObject = { + [this.fieldToPopulate]: this.generateRandInt(), + }; + return retObject; + } + + private validateName(name: string | null): string { + if (name === null || name.trim() === '') { + throw new InputValidationError( + this.errorBuilder({message: 'name is empty or null'}) + ); + } + return name; + } + + private validateConfig(config: {[key: string]: any}): void { + if (!Object.prototype.hasOwnProperty.call(config, RandIntGenerator.MIN)) { + throw new InputValidationError( + this.errorBuilder({ + message: 'config is missing ' + RandIntGenerator.MIN, + }) + ); + } + if (!Object.prototype.hasOwnProperty.call(config, RandIntGenerator.MAX)) { + throw new InputValidationError( + this.errorBuilder({ + message: 'config is missing ' + RandIntGenerator.MAX, + }) + ); + } + } + + private generateRandInt(): number { + const randomNumber = Math.random(); + const scaledNumber = randomNumber * (this.max - this.min) + this.min; + const truncatedNumber = Math.trunc(scaledNumber); + return truncatedNumber; + } +} + +export default RandIntGenerator; diff --git a/src/lib/mock-observations/index.ts b/src/lib/mock-observations/index.ts new file mode 100644 index 0000000..47a40e3 --- /dev/null +++ b/src/lib/mock-observations/index.ts @@ -0,0 +1,146 @@ +import {ERRORS} from '../../util/errors'; +import {buildErrorMessage} from '../../util/helpers'; +import {ModelParams} from '../../types/common'; +import {ModelPluginInterface} from '../../interfaces'; +import * as dayjs from 'dayjs'; +import CommonGenerator from './helpers/CommonGenerator'; +import RandIntGenerator from './helpers/RandIntGenerator'; +import Generator from './interfaces/index'; +const {InputValidationError} = ERRORS; + +export class MockObservations implements ModelPluginInterface { + private errorBuilder = buildErrorMessage(MockObservations); + private duration = 0; + private timeBuckets: dayjs.Dayjs[] = []; + private components: Record> = {}; + private generators: Generator[] = []; + + async execute(_inputs: ModelParams[]): Promise { + const observations: ModelParams[] = []; + const generatorToHistory = new Map(); + this.generators.forEach(generator => { + generatorToHistory.set(generator, []); + }); + for (const componentKey in this.components) { + if (Object.prototype.hasOwnProperty.call(this.components, componentKey)) { + const component = this.components[componentKey]; + for (const timeBucket of this.timeBuckets) { + const observation = this.createObservation( + component, + timeBucket, + generatorToHistory + ); + observations.push(observation); + } + } + } + return observations; + } + + async configure( + staticParams: object | undefined + ): Promise { + if (staticParams === undefined) { + throw new InputValidationError( + this.errorBuilder({message: 'Input data is missing'}) + ); + } + const timestampFrom: dayjs.Dayjs = dayjs( + this.getValidatedParam( + 'timestamp-from', + staticParams + ) as unknown as string + ); + const timestampTo: dayjs.Dayjs = dayjs( + this.getValidatedParam('timestamp-to', staticParams) as unknown as string + ); + const duration = this.getValidatedParam( + 'duration', + staticParams + ) as unknown as number; + this.timeBuckets = this.createTimeBuckets( + timestampFrom, + timestampTo, + duration + ); + this.components = this.getValidatedParam( + 'components', + staticParams + ) as Record>; + this.generators = this.createGenerators( + this.getValidatedParam('generators', staticParams) + ); + return this; + } + + private getValidatedParam( + attributeName: string, + params: {[key: string]: any} + ): object { + if (attributeName in params) { + return params[attributeName]; + } else { + throw new InputValidationError( + this.errorBuilder({message: attributeName + ' missing from params'}) + ); + } + } + + private createTimeBuckets( + timestampFrom: dayjs.Dayjs, + timestampTo: dayjs.Dayjs, + duration: number + ): dayjs.Dayjs[] { + const timeBuckets: dayjs.Dayjs[] = []; + let currTimestamp: dayjs.Dayjs = timestampFrom; + while ( + currTimestamp.isBefore(timestampTo) || + currTimestamp.isSame(timestampTo, 'second') + ) { + timeBuckets.push(currTimestamp); + currTimestamp = currTimestamp.add(duration, 'second'); + } + return timeBuckets; + } + + private createGenerators(generatorsConfig: object): Generator[] { + const generators: Generator[] = []; + Object.entries(generatorsConfig).forEach(([key, value]) => { + if ('common' === key) { + const commonGenerator = new CommonGenerator(); + commonGenerator.initialise('common-generator', value); + generators.push(commonGenerator); + } + if ('randint' === key) { + for (const fieldToPopulate in value) { + const randIntGenerator = new RandIntGenerator(); + randIntGenerator.initialise(fieldToPopulate, value[fieldToPopulate]); + generators.push(randIntGenerator); + } + } + }); + return generators; + } + + private createObservation( + component: Record, + timeBucket: dayjs.Dayjs, + generatorToHistory: Map + ): ModelParams { + const observation: ModelParams = { + timestamp: timeBucket.format('YYYY-MM-DD HH:mm:ss'), + duration: this.duration, + }; + Object.assign(observation, component); + for (const generator of this.generators) { + const generated: Record = generator.next( + generatorToHistory.get(generator) + ); + generatorToHistory.get(generator)?.push(generated.value); + Object.assign(observation, generated); + } + return observation; + } +} + +export default MockObservations; diff --git a/src/lib/mock-observations/interfaces/index.ts b/src/lib/mock-observations/interfaces/index.ts new file mode 100644 index 0000000..2ba7696 --- /dev/null +++ b/src/lib/mock-observations/interfaces/index.ts @@ -0,0 +1,11 @@ +export interface Generator { + /** + * initialise the generator with the given name and config. + */ + initialise(name: String, config: {[key: string]: any}): void; + /** + * generate the next value, optionally based on historical values + */ + next(historical: Object[] | undefined): Object; +} +export default Generator;