diff --git a/package.json b/package.json index a4145ec..b05090d 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@azure/arm-monitor": "^7.0.0", "@azure/identity": "^3.4.1", "@cloud-carbon-footprint/aws": "^0.15.0", + "@tgwf/co2": "^0.13.9", "axios": "^1.6.0", "dayjs": "^1.11.10", "dotenv": "16.3.1", diff --git a/src/__tests__/unit/lib/co2js/index.test.ts b/src/__tests__/unit/lib/co2js/index.test.ts new file mode 100644 index 0000000..083c9cb --- /dev/null +++ b/src/__tests__/unit/lib/co2js/index.test.ts @@ -0,0 +1,121 @@ +import {describe, expect, jest, test} from '@jest/globals'; +import {Co2jsModel} from '../../../../lib/co2js'; + +jest.setTimeout(30000); + +describe('lib/co2js', () => { + describe('initialization tests', () => { + test('init', async () => { + const outputModel = new Co2jsModel(); + await expect( + outputModel.configure({ + type: '1byte', + }) + ).resolves.toBeInstanceOf(Co2jsModel); + await expect( + outputModel.configure({ + type: 'swd', + }) + ).resolves.toBeInstanceOf(Co2jsModel); + await expect( + outputModel.configure({ + type: '1byt', + }) + ).rejects.toThrow(); + }); + }); + describe('configure()', () => { + test('initialize and configure 1byte', async () => { + const model = await new Co2jsModel().configure({ + type: '1byte', + }); + expect(model).toBeInstanceOf(Co2jsModel); + }); + test('initialize and configure swd', async () => { + const model = await new Co2jsModel().configure({ + type: 'swd', + }); + expect(model).toBeInstanceOf(Co2jsModel); + }); + test('initialize and test without bytes input', async () => { + const model = await new Co2jsModel().configure({ + type: '1byte', + }); + expect(model).toBeInstanceOf(Co2jsModel); + await expect( + model.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'green-web-host': true, + }, + ]) + ).rejects.toThrow(); + }); + }); + describe('execute()', () => { + test('initialize and execute 1byte', async () => { + const model = await new Co2jsModel().configure({ + type: '1byte', + }); + expect(model).toBeInstanceOf(Co2jsModel); + await expect( + model.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + bytes: 100000, + 'green-web-host': true, + }, + ]) + ).resolves.toStrictEqual([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + bytes: 100000, + 'green-web-host': true, + 'operational-carbon': 0.023195833333333332, + }, + ]); + }); + test('initialize and execute swd', async () => { + const model = await new Co2jsModel().configure({ + type: 'swd', + }); + expect(model).toBeInstanceOf(Co2jsModel); + await expect( + model.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + bytes: 100000, + 'green-web-host': true, + }, + ]) + ).resolves.toStrictEqual([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + bytes: 100000, + 'green-web-host': true, + 'operational-carbon': 0.023209515022500005, + }, + ]); + }); + test('initialize and execute without bytes input', async () => { + const model = await new Co2jsModel().configure({ + type: '1byte', + }); + expect(model).toBeInstanceOf(Co2jsModel); + await expect( + model.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'green-web-host': true, + }, + ]) + ).rejects.toThrow(); + }); + }); +}); diff --git a/src/lib/co2js/README.md b/src/lib/co2js/README.md new file mode 100644 index 0000000..87b7e7b --- /dev/null +++ b/src/lib/co2js/README.md @@ -0,0 +1,146 @@ +# CO2.JS + +> [!NOTE] +> `CO2.JS` 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! + + +# Parameters + +## model config + +- `type`: supported models by the library, `swd` or `1byte` + +## observations + +- `bytes`: the number of bytes transferred +- `green-web-host`: true if the website is hosted on a green web host, false otherwise +- `duration`: the amount of time the observation covers, in seconds +- `timestamp`: a timestamp for the observation + +## Returns + +- `operational-carbon`: carbon emissions from the operation of the website, in grams of CO2e + + +# IF Implementation + +IF utilizes the CO2JS Framework to calculate the carbon emissions of a website. The CO2JS Framework is a collection of models that calculate the carbon emissions of a website based on different parameters. IF installs the CO2js npm package from `@tgwf/co2js` and invokes its functions from a model plugin. + +The CO2JS Framework 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! + + +## Usage + +In IEF the model is called from an `impl`. An `impl` is a `.yaml` file that contains configuration metadata and usage inputs. This is interpreted by the command line tool, `impact-engine`. There, the model's `configure` method is called first. + +The model config should define a `type` supported by the CO2.JS library (either `swd` or `1byte`). These are different ways to calculate the operational carbon associated with a web application; `swd` is shorthand for 'sustainable web design' model and `1byte` refers to the OneByte mdoel. You can read about the details of these models and how they differ at the [Green Web Foundation website](https://developers.thegreenwebfoundation.org/co2js/explainer/methodologies-for-calculating-website-carbon/). + +Each input is expected to contain `bytes`, `green-web-host`, `duration` and `timestamp` fields. + +## IMPL + +The following is an example of how CO2.JS can be invoked using an `impl`. +```yaml +name: co2js-demo +description: example impl invoking CO2.JS model +initialize: + models: + - name: co2js + model: Co2JsModel + path: '@grnsft/if-unofficial-models' +graph: + children: + child: + pipeline: + - co2js + config: + co2js: + type: swd + inputs: + - timestamp: 2023-07-06T00:00 # [KEYWORD] [NO-SUBFIELDS] time when measurement occurred + duration: 1 + bytes: 1000000 + green-web-host: true +``` + +This impl is run using `impact-engine` using the following command, run from the project root: + +```sh +npm i -g @grnsft/if +npm i -g @grnsft/if-unofficial-models +impact-engine --impl ./examples/impls/co2js-test.yml --ompl ./examples/ompls/co2js-test.yml +``` + +This yields a result that looks like the following (saved to `/ompls/co2js-test.yml`): + +```yaml +name: co2js-demo +description: example impl invoking CO2.JS model +initialize: + models: + - name: co2js + model: Co2JsModel + path: '@grnsft/if-unofficial-models' +graph: + children: + child: + pipeline: + - co2js + config: + co2js: + type: swd + inputs: + - timestamp: 2023-07-06T00:00 + duration: 1 + bytes: 1000000 + green-web-host: true + outputs: + - timestamp: 2023-07-06T00:00 + operational-carbon: 0.02 + duration: 1 + bytes: 1000000 + green-web-host: true +``` + + +## TypeScript + +You can see example Typescript invocations for each model below. + +### SWD + +```typescript +import {Co2jsModel} from '@grnsft/if-unofficial-models'; + +const co2js = await (new Co2jsModel()).configure({ + type: 'swd' +}) +const results = co2js.execute([ + { + duration: 3600, // duration institute + timestamp: '2021-01-01T00:00:00Z', // ISO8601 / RFC3339 timestamp + bytes: 1000000, // bytes transferred + 'green-web-host': true // true if the website is hosted on a green web host, false otherwise + } +]) +``` + + +### 1byte + +```typescript +import {Co2jsModel} from '@grnsft/if-unofficial-models'; + +const co2js = await (new Co2jsModel()).configure({ + type: '1byte' +}) +const results = co2js.execute([ + { + duration: 3600, // duration institute + timestamp: '2021-01-01T00:00:00Z', // ISO8601 / RFC3339 timestamp + bytes: 1000000, // bytes transferred + 'green-web-host': true // true if the website is hosted on a green web host, false otherwise + } +]) +``` + diff --git a/src/lib/co2js/index.ts b/src/lib/co2js/index.ts new file mode 100644 index 0000000..f241e3e --- /dev/null +++ b/src/lib/co2js/index.ts @@ -0,0 +1,48 @@ +import {co2} from '@tgwf/co2'; +import {ModelPluginInterface} from '../../interfaces'; +import {KeyValuePair, ModelParams} from '../../types'; + +export class Co2jsModel implements ModelPluginInterface { + staticParams: KeyValuePair = {}; + model: any | undefined; + + async execute(observations: ModelParams[]): Promise { + return observations.map((observation: any) => { + this.configure(observation); + if (observation['bytes'] === undefined) { + throw new Error('bytes not provided'); + } + const greenhosting = + observation['green-web-host'] !== undefined && + observation['green-web-host'] === true; + let result; + switch (this.staticParams.type) { + case 'swd': { + result = this.model.perVisit(observation['bytes'], greenhosting); + break; + } + case '1byte': { + result = this.model.perByte(observation['bytes'], greenhosting); + break; + } + } + if (result !== undefined) { + observation['operational-carbon'] = result; + } + return observation; + }); + } + + async configure(staticParams: object): Promise { + if (staticParams !== undefined && 'type' in staticParams) { + if (!['1byte', 'swd'].includes(staticParams.type as string)) { + throw new Error( + `Invalid co2js model: ${staticParams.type}. Must be one of 1byte or swd.` + ); + } + this.staticParams['type'] = staticParams.type; + this.model = new co2({model: staticParams.type}); + } + return this; + } +} diff --git a/yarn.lock b/yarn.lock index c286dac..c78cfbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1136,6 +1136,11 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918" integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== +"@tgwf/co2@^0.13.9": + version "0.13.9" + resolved "https://registry.yarnpkg.com/@tgwf/co2/-/co2-0.13.9.tgz#6d59fabb080f3c2ef73ade4d15c83fe056521598" + integrity sha512-7PuJkzfLFJgKauoz5u5GM21rniOMQCqOcPVcebZURCk5nhvn9R1LdVc9nCWAc9ybUClDn8QtNlkSyXcX5f8z+Q== + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"