diff --git a/manifests/bugs/azure-importer-ignoring-defaults.yml b/manifests/bugs/azure-importer-ignoring-defaults.yml index b2b82c512..424be5e84 100644 --- a/manifests/bugs/azure-importer-ignoring-defaults.yml +++ b/manifests/bugs/azure-importer-ignoring-defaults.yml @@ -15,14 +15,14 @@ initialize: method: AzureImporter path: "@grnsft/if-unofficial-plugins" "try-defaults-1": - path: "@grnsft/if-plugins" + path: "builtin" method: Coefficient global-config: input-parameter: grid/carbon-intensity coefficient: 0.1 output-parameter: grid/carbon-intensity "try-defaults-2": - path: "@grnsft/if-plugins" + path: "builtin" method: Coefficient global-config: input-parameter: network/energy diff --git a/manifests/bugs/azure-importer-incorrect-calculation.yml b/manifests/bugs/azure-importer-incorrect-calculation.yml index 0489a5e93..fa4cc17d0 100644 --- a/manifests/bugs/azure-importer-incorrect-calculation.yml +++ b/manifests/bugs/azure-importer-incorrect-calculation.yml @@ -7,14 +7,14 @@ initialize: path: '@grnsft/if-unofficial-plugins' method: AzureImporter try-defaults-1: - path: '@grnsft/if-plugins' + path: 'builtin' method: Coefficient global-config: input-parameter: grid/carbon-intensity coefficient: 0.1 output-parameter: grid/carbon-intensity try-defaults-2: - path: '@grnsft/if-plugins' + path: 'builtin' method: Coefficient global-config: input-parameter: network/energy diff --git a/manifests/examples/generics.yml b/manifests/examples/generics.yml index 3720d6692..f2167581b 100644 --- a/manifests/examples/generics.yml +++ b/manifests/examples/generics.yml @@ -17,7 +17,7 @@ initialize: - network/energy output-parameter: energy-sum "coefficient": - path: "@grnsft/if-plugins" + path: "builtin" method: Coefficient global-config: input-parameter: energy diff --git a/manifests/plugins/coefficient/failure-invalid-config-input-param.yml b/manifests/plugins/coefficient/failure-invalid-config-input-param.yml index 912b8af22..9c0416c5e 100644 --- a/manifests/plugins/coefficient/failure-invalid-config-input-param.yml +++ b/manifests/plugins/coefficient/failure-invalid-config-input-param.yml @@ -6,7 +6,7 @@ initialize: plugins: coefficient: method: Coefficient - path: "@grnsft/if-plugins" + path: "builtin" global-config: input-parameter: 4 coefficient: 3 diff --git a/manifests/plugins/coefficient/failure-output-param-is-null.yaml b/manifests/plugins/coefficient/failure-output-param-is-null.yaml index 3c4940d87..795c04492 100644 --- a/manifests/plugins/coefficient/failure-output-param-is-null.yaml +++ b/manifests/plugins/coefficient/failure-output-param-is-null.yaml @@ -6,7 +6,7 @@ initialize: plugins: coefficient: method: Coefficient - path: "@grnsft/if-plugins" + path: "builtin" global-config: input-parameter: "carbon" coefficient: 3 diff --git a/manifests/plugins/coefficient/success.yml b/manifests/plugins/coefficient/success.yml index dc6969d59..1ce61af1b 100644 --- a/manifests/plugins/coefficient/success.yml +++ b/manifests/plugins/coefficient/success.yml @@ -6,7 +6,7 @@ initialize: plugins: coefficient: method: Coefficient - path: "@grnsft/if-plugins" + path: "builtin" global-config: input-parameter: "carbon" coefficient: 3 diff --git a/src/__tests__/unit/builtins/coefficient.test.ts b/src/__tests__/unit/builtins/coefficient.test.ts new file mode 100644 index 000000000..bf7455679 --- /dev/null +++ b/src/__tests__/unit/builtins/coefficient.test.ts @@ -0,0 +1,123 @@ +import {Coefficient} from '../../../builtins/coefficient'; + +import {ERRORS} from '../../../util/errors'; + +const {InputValidationError, ConfigNotFoundError} = ERRORS; + +describe('builtins/coefficient: ', () => { + describe('Coefficient: ', () => { + const globalConfig = { + 'input-parameter': 'carbon', + coefficient: 3, + 'output-parameter': 'carbon-product', + }; + const coefficient = Coefficient(globalConfig); + + describe('init: ', () => { + it('successfully initalized.', () => { + expect(coefficient).toHaveProperty('metadata'); + expect(coefficient).toHaveProperty('execute'); + }); + }); + + describe('execute(): ', () => { + it('successfully applies coefficient strategy to given input.', () => { + expect.assertions(1); + + const expectedResult = [ + { + duration: 3600, + carbon: 3, + 'carbon-product': 9, + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + const result = coefficient.execute([ + { + duration: 3600, + carbon: 3, + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + + expect.assertions(1); + + expect(result).toStrictEqual(expectedResult); + }); + + it('throws an error when global config is not provided.', () => { + const config = undefined; + const coefficient = Coefficient(config!); + const expectedMessage = 'Global config is not provided.'; + + expect.assertions(1); + + try { + coefficient.execute([ + { + duration: 3600, + timestamp: '2021-01-01T00:00:00Z', + carbon: 3, + }, + ]); + } catch (error) { + expect(error).toStrictEqual(new ConfigNotFoundError(expectedMessage)); + } + }); + + it('throws an error on missing `input-parameter` param in input.', () => { + const invalidConfig = { + 'input-parameter': '', + coefficient: 3, + 'output-parameter': 'carbon-product', + }; + const coefficient = Coefficient(invalidConfig); + const expectedMessage = + '"input-parameter" parameter is string must contain at least 1 character(s). Error code: too_small.'; + + expect.assertions(1); + + try { + coefficient.execute([ + { + duration: 3600, + timestamp: '2021-01-01T00:00:00Z', + carbon: 3, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError(expectedMessage) + ); + } + }); + + it('throws an error on missing `output-parameter` param in input.', () => { + const invalidConfig = { + 'input-parameter': 'carbon', + coefficient: 10, + 'output-parameter': '', + }; + const coefficient = Coefficient(invalidConfig); + const expectedMessage = + '"output-parameter" parameter is string must contain at least 1 character(s). Error code: too_small.'; + + expect.assertions(1); + try { + coefficient.execute([ + { + duration: 3600, + timestamp: '2021-01-01T00:00:00Z', + carbon: 3, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError(expectedMessage) + ); + } + }); + }); + }); +}); diff --git a/src/__tests__/unit/builtins/exponent.test.ts b/src/__tests__/unit/builtins/exponent.test.ts new file mode 100644 index 000000000..54c69ffcb --- /dev/null +++ b/src/__tests__/unit/builtins/exponent.test.ts @@ -0,0 +1,118 @@ +import {Exponent} from '../../../builtins/exponent'; + +import {ERRORS} from '../../../util/errors'; + +const {InputValidationError} = ERRORS; + +describe('lib/exponent: ', () => { + describe('Exponent: ', () => { + const globalConfig = { + 'input-parameter': 'energy/base', + exponent: 3, + 'output-parameter': 'energy', + }; + const exponent = Exponent(globalConfig); + + describe('init: ', () => { + it('successfully initalized.', () => { + expect(exponent).toHaveProperty('metadata'); + expect(exponent).toHaveProperty('execute'); + }); + }); + + describe('execute(): ', () => { + it('successfully applies Exponent strategy to given input.', async () => { + expect.assertions(1); + + const expectedResult = [ + { + duration: 3600, + 'energy/base': 2, + energy: 8, + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + const result = await exponent.execute([ + { + duration: 3600, + 'energy/base': 2, + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + + it('throws an error on missing params in input.', async () => { + const expectedMessage = + 'Exponent: energy/base is missing from the input array.'; + + expect.assertions(1); + + try { + await exponent.execute([ + { + duration: 3600, + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError(expectedMessage) + ); + } + }); + + it('throws an error on input param value not numeric.', async () => { + const expectedMessage = 'Exponent: i-am-not-a-number is not numeric.'; + + expect.assertions(1); + + try { + await exponent.execute([ + { + duration: 3600, + 'energy/base': 'i-am-not-a-number', + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError(expectedMessage) + ); + } + }); + + it('returns a result with input params not related to energy.', async () => { + expect.assertions(1); + const newConfig = { + 'input-parameter': 'carbon/base', + exponent: 4, + 'output-parameter': 'carbon', + }; + const exponent = Exponent(newConfig); + + const data = [ + { + duration: 3600, + timestamp: '2021-01-01T00:00:00Z', + 'carbon/base': 2, + }, + ]; + const response = await exponent.execute(data); + + const expectedResult = [ + { + duration: 3600, + 'carbon/base': 2, + carbon: 16, + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + expect(response).toEqual(expectedResult); + }); + }); + }); +}); diff --git a/src/builtins/coefficient/README.md b/src/builtins/coefficient/README.md new file mode 100644 index 000000000..096d20ab5 --- /dev/null +++ b/src/builtins/coefficient/README.md @@ -0,0 +1,92 @@ +# Coefficient + +`Coefficient` is a generic plugin for multiplying a value in an `input` array by a given coefficient. + +You provide the name of the value you want to multiply, a coefficient value, and a name to use to append the product to the output array. + +For example, you could multiply `cpu/energy` by 10 and name the result `energy-product`. `energy-product` would then be added to every observation in your input array as the product of `cpu/energy` and 10. + +## Parameters + +### Plugin global config + +Three parameters are required in global config: `input-parameter`, `coefficient` and `output-parameter`. + +- `input-parameter`: a string matching an existing key in the `inputs` array +- `coefficient`: the value to multiply `input-parameter` by. +- `output-parameter`: a string defining the name to use to add the product of the input parameters to the output array. + +### Inputs + +All of `input-parameters` must be available in the input array. + +## Returns + +- `output-parameter`: the product of all `input-parameters` with the parameter name defined by `output-parameter` in global config. + +## Calculation + +```pseudocode +output = input * coefficient +``` + +## Implementation + +To run the plugin from a Typescript app, you must first create an instance of `Coefficient`. Then, you can call `execute()`. + +```typescript +const config = { + 'input-parameter': 'carbon', + coefficient: 10, + 'output-parameter': 'carbon-product', +}; + +const coeff = Coefficient(config); +const result = coeff.execute([ + { + duration: 3600, + timestamp: '2021-01-01T00:00:00Z', + carbon: 3, + }, +]); +``` + +## Example manifest + +IF users will typically call the plugin as part of a pipeline defined in a manifest file. In this case, instantiating the plugin is handled by `ie` and does not have to be done explicitly by the user. The following is an example manifest that calls `coefficient`: + +```yaml +name: coefficient-demo +description: +tags: +initialize: + outputs: + - yaml + plugins: + coefficient: + method: Coefficient + path: 'builtin' + global-config: + input-parameter: 'carbon' + coefficient: 3 + output-parameter: 'carbon-product' +tree: + children: + child: + pipeline: + - coefficient + config: + coefficient: + inputs: + - timestamp: 2023-08-06T00:00 + duration: 3600 + carbon: 30 +``` + +You can run this example by saving it as `./examples/manifests/coefficient.yml` and executing the following command from the project root: + +```sh +ie --manifest ./examples/manifests/coefficient.yml --output ./examples/outputs/coefficient.yml +``` + +The results will be saved to a new `yaml` file in `./examples/outputs` diff --git a/src/builtins/coefficient/index.ts b/src/builtins/coefficient/index.ts new file mode 100644 index 000000000..892dac02d --- /dev/null +++ b/src/builtins/coefficient/index.ts @@ -0,0 +1,66 @@ +import {z} from 'zod'; + +import {ExecutePlugin, PluginParams} from '../../types/interface'; + +import {validate} from '../../util/validations'; +import {ERRORS} from '../../util/errors'; + +import {CoefficientConfig} from './types'; + +const {ConfigNotFoundError} = ERRORS; + +export const Coefficient = (globalConfig: CoefficientConfig): ExecutePlugin => { + const metadata = { + kind: 'execute', + }; + + /** + * Calculate the product of each input parameter. + */ + const execute = (inputs: PluginParams[]) => { + const safeGlobalConfig = validateGlobalConfig(); + const inputParameter = safeGlobalConfig['input-parameter']; + const outputParameter = safeGlobalConfig['output-parameter']; + const coefficient = safeGlobalConfig['coefficient']; + + return inputs.map(input => { + return { + ...input, + [outputParameter]: calculateProduct(input, inputParameter, coefficient), + }; + }); + }; + + /** + * Calculates the product of the energy components. + */ + const calculateProduct = ( + input: PluginParams, + inputParameter: string, + coefficient: number + ) => input[inputParameter] * coefficient; + + /** + * Checks global config value are valid. + */ + const validateGlobalConfig = () => { + if (!globalConfig) { + throw new ConfigNotFoundError('Global config is not provided.'); + } + + const globalConfigSchema = z.object({ + coefficient: z.number(), + 'input-parameter': z.string().min(1), + 'output-parameter': z.string().min(1), + }); + + return validate>( + globalConfigSchema, + globalConfig + ); + }; + return { + metadata, + execute, + }; +}; diff --git a/src/builtins/coefficient/types.ts b/src/builtins/coefficient/types.ts new file mode 100644 index 000000000..9cbb78b7d --- /dev/null +++ b/src/builtins/coefficient/types.ts @@ -0,0 +1,5 @@ +export type CoefficientConfig = { + 'input-parameter': string; + coefficient: number; + 'output-parameter': string; +}; diff --git a/src/builtins/exponent/README.md b/src/builtins/exponent/README.md new file mode 100644 index 000000000..48a67965a --- /dev/null +++ b/src/builtins/exponent/README.md @@ -0,0 +1,97 @@ +# Exponent + +`exponent` is a generic plugin for calculating exponent of an input param (as base) and another (as the exponent) in an `input` array. + +You provide the names of the values you want to use for the exponent calculation, and a name to use to add the exponent result to the output array. + +For example, you use `cpu/energy` as base and `network/energy` as and name the result `energy`. `energy` would then be added to every observation in your input array as `cpu/energy` raised by the exponent `network/energy`. + +## Parameters + +### Plugin config + +Three parameters are required in global config: `input-parameter`, `exponent` and `output-parameter`. + +`input-parameter`: a string defining the base. Must match an existing key in the `inputs` array +`exponent`: a number defining the exponent. +`output-parameter`: a string defining the name to use to add the result of the exponent to the output array. + +### Inputs + +`input-parameter` and `exponent` must be available in the input array. + +## Returns + +- `output-parameter`: `input-parameter` raised by `exponent` with the parameter name defined by `output-parameter` in global config. + +## Calculation + +```pseudocode +output = input ^ exponent +``` + +## Implementation + +To run the plugin, you must first create an instance of `Exponent`. Then, you can call `execute()`. + +```typescript +import {Exponent} from 'builtins'; + +const config = { + inputParameter: ['cpu/energy'], + exponent: 2 + outputParameter: 'energy', +}; + +const exponent = Exponent(config); +const result = await exponent.execute([ + { + duration: 3600, + timestamp: '2021-01-01T00:00:00Z', + 'cpu/energy': 0.1, + 'energy': 0.01, + }, +]); +``` + +## Example manifest + +IF users will typically call the plugin as part of a pipeline defined in a manifest file. In this case, instantiating the plugin is handled by and does not have to be done explicitly by the user. The following is an example manifest that calls `exponent`: + +```yaml +name: exponent demo +description: +tags: +initialize: + outputs: + - yaml + plugins: + exponent: + method: Exponent + path: 'builtin' + global-config: + input-parameter: 'cpu/energy' + exponent: 2 + output-parameter: 'energy' +tree: + children: + child: + pipeline: + - exponent + config: + exponent: + inputs: + - timestamp: 2023-08-06T00:00 + duration: 3600 + cpu/energy: 0.001 + network/energy: 0.001 +``` + +You can run this example by saving it as `manifests/examples/test/exponent.yml` and executing the following command from the project root: + +```sh +npm i -g @grnsft/if +ie --manifest manifests/examples/test/exponent.yml --output manifests/outputs/exponent.yml +``` + +The results will be saved to a new `yaml` file in `manifests/outputs`. diff --git a/src/builtins/exponent/index.ts b/src/builtins/exponent/index.ts new file mode 100644 index 000000000..404161e63 --- /dev/null +++ b/src/builtins/exponent/index.ts @@ -0,0 +1,97 @@ +import {z} from 'zod'; + +import {ERRORS} from '../../util/errors'; +import {buildErrorMessage} from '../../util/helpers'; +import {validate} from '../../util/validations'; + +import {ExecutePlugin, PluginParams} from '../../types/interface'; +import {ExponentConfig} from './types'; + +const {InputValidationError} = ERRORS; + +export const Exponent = (globalConfig: ExponentConfig): ExecutePlugin => { + const errorBuilder = buildErrorMessage(Exponent.name); + const metadata = { + kind: 'execute', + }; + + /** + * Checks global config value are valid. + */ + const validateGlobalConfig = () => { + const globalConfigSchema = z.object({ + 'input-parameter': z.string().min(1), + exponent: z.number().min(1), + 'output-parameter': z.string().min(1), + }); + + return validate>( + globalConfigSchema, + globalConfig + ); + }; + + /** + * Checks for required fields in input. + */ + const validateSingleInput = (input: PluginParams, inputParameter: string) => { + validateParamExists(input, inputParameter); + validateNumericString(input[inputParameter]); + }; + + const validateParamExists = (input: PluginParams, param: string) => { + if (input[param] === undefined) { + throw new InputValidationError( + errorBuilder({ + message: `${param} is missing from the input array`, + }) + ); + } + }; + + const validateNumericString = (str: string) => { + if (isNaN(+Number(str))) { + throw new InputValidationError( + errorBuilder({ + message: `${str} is not numeric`, + }) + ); + } + }; + + /** + * Calculate the input param raised by to the power of the given exponent. + */ + const execute = (inputs: PluginParams[]): PluginParams[] => { + const { + 'input-parameter': inputParameter, + exponent: exponent, + 'output-parameter': outputParameter, + } = validateGlobalConfig(); + return inputs.map(input => { + validateSingleInput(input, inputParameter); + + return { + ...input, + [outputParameter]: calculateExponent(input, inputParameter, exponent), + }; + }); + }; + + /** + * Calculates the input param raised by the power of a given exponent. + */ + const calculateExponent = ( + input: PluginParams, + inputParameter: string, + exponent: number + ) => { + const base = input[inputParameter]; + return Math.pow(base, exponent); + }; + + return { + metadata, + execute, + }; +}; diff --git a/src/builtins/exponent/types.ts b/src/builtins/exponent/types.ts new file mode 100644 index 000000000..54e8b2efc --- /dev/null +++ b/src/builtins/exponent/types.ts @@ -0,0 +1,5 @@ +export type ExponentConfig = { + 'input-parameter': string; + exponent: number; + 'output-parameter': string; +}; diff --git a/src/builtins/index.ts b/src/builtins/index.ts index 0b737841f..5efcded90 100644 --- a/src/builtins/index.ts +++ b/src/builtins/index.ts @@ -1,7 +1,9 @@ export {GroupBy} from './group-by'; export {TimeSync} from './time-sync'; export {Subtract} from './subtract'; +export {Coefficient} from './coefficient'; export {Multiply} from './multiply'; export {Sum} from './sum'; export {SciEmbodied} from './sci-embodied'; export {Sci} from './sci'; +export {Exponent} from './exponent';