Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CO2JS Model Implementation #10

Merged
merged 10 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
88 changes: 88 additions & 0 deletions src/__tests__/unit/lib/co2js/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {describe, expect, jest, test} from '@jest/globals';
import {Co2jsModel} from '../../../../lib/co2js';

jest.setTimeout(30000);

describe('co2js-test', () => {
test('initialization tests', 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();
});
test('initialize and test 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 test 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 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();
});
});
146 changes: 146 additions & 0 deletions src/lib/co2js/README.md
Original file line number Diff line number Diff line change
@@ -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
}
])
```

53 changes: 53 additions & 0 deletions src/lib/co2js/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {co2} from '@tgwf/co2';
import {ModelPluginInterface} from '../../interfaces';
import {KeyValuePair, ModelParams} from '../../types';

export class Co2jsModel implements ModelPluginInterface {
authParams: object | undefined = undefined;
gnanakeethan marked this conversation as resolved.
Show resolved Hide resolved
staticParams: KeyValuePair = {};
name: string | undefined;
time: string | unknown;
model: any | undefined;

async execute(observations: ModelParams[]): Promise<ModelParams[]> {
return observations.map((observation: any) => {
this.configure(observation);
if (observation['bytes'] === undefined) {
throw new Error('bytes not provided');
}
let greenhosting = false;
if (observation['green-web-host'] !== undefined) {
greenhosting = observation['green-web-host'];
gnanakeethan marked this conversation as resolved.
Show resolved Hide resolved
}
let result;
console.log('TYPE', this.staticParams.type);
switch (this.staticParams.type) {
gnanakeethan marked this conversation as resolved.
Show resolved Hide resolved
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<ModelPluginInterface> {
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;
}
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down