@tgwf/co2 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/readme.md ADDED
@@ -0,0 +1,66 @@
1
+ # How to use the different models in CO2.js
2
+
3
+ CO2js offers two models for understanding the environmental impact of compute - the 1byte model (`1byte.js`), and the Sustainable Web Design model (`swd.js`)
4
+
5
+ ### The 1byte model
6
+
7
+ The default model in use is the 1byte model as used by the Shift Project, as introduced in their report on CO2 emissions from digital infrastructure, [Lean ICT: for a sober digital][soberDigital].
8
+
9
+ This returns a number for the estimated CO2 emissions for the corresponding number of bytes sent over the wire, and has been used for video streaming, file downloads and websites.
10
+
11
+ ```js
12
+ // assume you have imported or required the CO2 class from 1byte.js
13
+ // for your runtime - either a browser, nodejs, etc.
14
+
15
+ const bytesSent = (1024 * 1024 * 1024)
16
+ const co2Emission = new CO2();
17
+ const estimatedCO2 = co2Emission.perByte(bytesSent)
18
+ ```
19
+
20
+
21
+ ### The Sustainable Web Design model
22
+
23
+ As of version 0.9, CO2.js also provides the [Sustainable Web Design model][swd] for calculating emissions from digital services. As the name suggests, this has been designed for helping understand the environmental impact of websites. Further details are available on the [Sustainable Web Design website explaining the model](https://sustainablewebdesign.org/calculating-digital-emissions/), but for convenience, a short summary is below.
24
+
25
+ #### How the SWD works
26
+
27
+ This model uses data transfer as an proxy indicator for total resource usage, and uses this number to extrapolate energy usage numbers for your application as a fraction of the energy used by the total system comprised of:
28
+
29
+ 1. the use-phase energy of datacentres serving content
30
+ 2. the use-phase energy network transfering the data
31
+ 3. the use-phase energy of user device an user is accessing content on
32
+ 4. the total embodied energy used to create all of the above
33
+
34
+ ![swd model](../images/swd-energy-usage.png)
35
+
36
+ It then converts these energy figures to carbon emissions, based on the carbon intensity of electricity from the [Ember annual global electricity review][Ember-annual-global-electricity-review].
37
+
38
+ The carbon intensity of electricity figures for the swd model include the the full lifecycle emissions including upstream methane, supply-chain and manufacturing emissions, and include all gases, converted into CO2 equivalent over a 100 year timescale.
39
+
40
+ This follows the approach used by the IPCC 5th Assessment Report Annex 3 (2014), for the carbon intensity of electricity.
41
+
42
+ [Ember's methodology notes][ember-methodology] detail where the rest of this data comes from in more detail, as well as any further assumptions made.
43
+
44
+
45
+ [ember-methodology]: https://ember-climate.org/app/uploads/2022/03/GER22-Methodology.pdf
46
+
47
+ [Ember-annual-global-electricity-review]: https://ember-climate.org/insights/research/european-electricity-review-2022/
48
+
49
+
50
+
51
+
52
+ ### Sample usage
53
+
54
+ ```js
55
+ // assume you have imported or required the CO2 class from swd.js
56
+ // for your runtime - either a browser, nodejs, etc.
57
+
58
+ // assume a 1 megabyte webpage
59
+ const bytesSent = (1024 * 1024 * 1024)
60
+
61
+ const co2Emission = new CO2();
62
+ const estimatedCO2ForTransfer = co2Emission.emissionsPerVisitInGrams(bytesSent)
63
+ ```
64
+
65
+ [soberDigital]: https://theshiftproject.org/en/lean-ict-2/
66
+ [swd]: https://sustainablewebdesign.org/calculating-digital-emissions
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Sustainable Web Design
5
+ *
6
+ * Updated calculations and figures from
7
+ * https://sustainablewebdesign.org/calculating-digital-emissions/
8
+ *
9
+ *
10
+ */
11
+ const { fileSize } = require("./constants");
12
+ const { formatNumber } = require("./helpers");
13
+
14
+ // this refers to the estimated total energy use for the internet around 2000 TWh,
15
+ // divided by the total transfer it enables around 2500 exabytes
16
+ const KWH_PER_GB = 0.81;
17
+
18
+ // these constants outline how the energy is attributed to
19
+ // different parts of the system in the SWD model
20
+ const END_USER_DEVICE_ENERGY = 0.52;
21
+ const NETWORK_ENERGY = 0.14;
22
+ const DATACENTER_ENERGY = 0.15;
23
+ const PRODUCTION_ENERGY = 0.19;
24
+
25
+ // These carbon intensity figures https://ember-climate.org/data/data-explorer
26
+ // - Global carbon intensity for 2021
27
+ const GLOBAL_INTENSITY = 442;
28
+ const RENEWABLES_INTENSITY = 50;
29
+
30
+ // Taken from: https://gitlab.com/wholegrain/carbon-api-2-0/-/blob/master/includes/carbonapi.php
31
+
32
+ const FIRST_TIME_VIEWING_PERCENTAGE = 0.25;
33
+ const RETURNING_VISITOR_PERCENTAGE = 0.75;
34
+ const PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD = 0.02;
35
+
36
+ class SustainableWebDesign {
37
+ constructor(options) {
38
+ this.options = options;
39
+ }
40
+
41
+ /**
42
+ * Accept a figure for bytes transferred and return an object representing
43
+ * the share of the total enrgy use of the entire system, broken down
44
+ * by each corresponding system component
45
+ *
46
+ * @param {number} bytes - the data transferred in bytes
47
+ * @return {object} Object containing the energy in kilowatt hours, keyed by system component
48
+ */
49
+ energyPerByteByComponent(bytes) {
50
+ const transferedBytesToGb = bytes / fileSize.GIGABYTE;
51
+ const energyUsage = transferedBytesToGb * KWH_PER_GB;
52
+
53
+ // return the total energy, with breakdown by component
54
+ return {
55
+ consumerDeviceEnergy: energyUsage * END_USER_DEVICE_ENERGY,
56
+ networkEnergy: energyUsage * NETWORK_ENERGY,
57
+ productionEnergy: energyUsage * PRODUCTION_ENERGY,
58
+ dataCenterEnergy: energyUsage * DATACENTER_ENERGY,
59
+ };
60
+ }
61
+ /**
62
+ * Accept an object keys by the different system components, and
63
+ * return an object with the co2 figures key by the each component
64
+ *
65
+ * @param {object} energyBycomponent - energy grouped by the four system components
66
+ * @param {number} [carbonIntensity] - carbon intensity to apply to the datacentre values
67
+ * @return {number} the total number in grams of CO2 equivalent emissions
68
+ */
69
+ co2byComponent(energyBycomponent, carbonIntensity = GLOBAL_INTENSITY) {
70
+ const returnCO2ByComponent = {};
71
+ for (const [key, value] of Object.entries(energyBycomponent)) {
72
+ // we update the datacentre, as that's what we have information
73
+ // about.
74
+ if (key === "dataCenterEnergy") {
75
+ returnCO2ByComponent[key] = value * carbonIntensity;
76
+ } else {
77
+ // We don't have info about the device location,
78
+ // nor the network path used, nor the production emissions
79
+ // so we revert to global figures
80
+ returnCO2ByComponent[key] = value * GLOBAL_INTENSITY;
81
+ }
82
+ }
83
+ return returnCO2ByComponent;
84
+ }
85
+
86
+ /**
87
+ * Accept a figure for bytes transferred and return a single figure for CO2
88
+ * emissions. Where information exists about the origin data is being
89
+ * fetched from, a different carbon intensity figure
90
+ * is applied for the datacentre share of the carbon intensity.
91
+ *
92
+ * @param {number} bytes - the data transferred in bytes
93
+ * @param {number} `carbonIntensity` the carbon intensity for datacentre (average figures, not marginal ones)
94
+ * @return {number} the total number in grams of CO2 equivalent emissions
95
+ */
96
+ perByte(bytes, carbonIntensity = GLOBAL_INTENSITY) {
97
+ const energyBycomponent = this.energyPerByteByComponent(bytes);
98
+
99
+ // when faced with falsy values, fallback to global intensity
100
+ if (Boolean(carbonIntensity) === false) {
101
+ carbonIntensity = GLOBAL_INTENSITY;
102
+ }
103
+ // if we have a boolean, we have a green result from the green web checker
104
+ // use the renewables intensity
105
+ if (carbonIntensity === true) {
106
+ carbonIntensity = RENEWABLES_INTENSITY;
107
+ }
108
+
109
+ // otherwise when faced with non numeric values throw an error
110
+ if (typeof carbonIntensity !== "number") {
111
+ throw new Error(
112
+ `perByte expects a numeric value or boolean for the carbon intensity value. Received: ${carbonIntensity}`
113
+ );
114
+ }
115
+
116
+ const co2ValuesbyComponent = this.co2byComponent(
117
+ energyBycomponent,
118
+ carbonIntensity
119
+ );
120
+
121
+ // pull out our values…
122
+ const co2Values = Object.values(co2ValuesbyComponent);
123
+
124
+ // so we can return their sum
125
+ return co2Values.reduce(
126
+ (prevValue, currentValue) => prevValue + currentValue
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Accept a figure for bytes transferred and return the number of kilowatt hours used
132
+ * by the total system for this data transfer
133
+ *
134
+ * @param {number} bytes
135
+ * @return {number} the number of kilowatt hours used
136
+ */
137
+ energyPerByte(bytes) {
138
+ const energyByComponent = this.energyPerByteByComponent(bytes);
139
+
140
+ // pull out our values…
141
+ const energyValues = Object.values(energyByComponent);
142
+
143
+ // so we can return their sum
144
+ return energyValues.reduce(
145
+ (prevValue, currentValue) => prevValue + currentValue
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Accept a figure for bytes transferred, and return an object containing figures
151
+ * per system component, with the caching assumptions applied. This tries to account
152
+ * for webpages being loaded from a cache by browsers, so if you had a thousand page views,
153
+ * and tried to work out the energy per visit, the numbers would reflect the reduced amounts
154
+ * of transfer.
155
+ *
156
+ * @param {number} bytes - the data transferred in bytes for loading a webpage
157
+ * @param {number} firstView - what percentage of visits are loading this page for the first time
158
+ * @param {number} returnView - what percentage of visits are loading this page for subsequent times
159
+ * @param {number} dataReloadRatio - what percentage of a page is reloaded on each subsequent page view
160
+ *
161
+ * @return {object} Object containing the energy in kilowatt hours, keyed by system component
162
+ */
163
+ energyPerVisitByComponent(
164
+ bytes,
165
+ firstView = FIRST_TIME_VIEWING_PERCENTAGE,
166
+ returnView = RETURNING_VISITOR_PERCENTAGE,
167
+ dataReloadRatio = PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD
168
+ ) {
169
+ const energyBycomponent = this.energyPerByteByComponent(bytes);
170
+ const cacheAdjustedSegmentEnergy = {};
171
+
172
+ for (const [key, value] of Object.entries(energyBycomponent)) {
173
+ // represent the first load
174
+ cacheAdjustedSegmentEnergy[key] = value * firstView;
175
+
176
+ // then represent the subsequent load
177
+ cacheAdjustedSegmentEnergy[key] += value * returnView * dataReloadRatio;
178
+ }
179
+
180
+ return cacheAdjustedSegmentEnergy;
181
+ }
182
+
183
+ /**
184
+ * Accept a figure for bytes, and return the total figure for energy per visit
185
+ * using the default caching assumptions for loading a single website
186
+ *
187
+ * @param {number} bytes
188
+ * @return {number} the total energy use for the visit, after applying the caching assumptions
189
+ */
190
+ energyPerVisit(bytes) {
191
+ // fetch the values using the default caching assumptions
192
+ const energyValues = Object.values(this.energyPerVisitByComponent(bytes));
193
+
194
+ // return the summed of the values to return our single number
195
+ return energyValues.reduce(
196
+ (prevValue, currentValue) => prevValue + currentValue
197
+ );
198
+ }
199
+
200
+ // TODO: this method looks like it applies the carbon intensity
201
+ // change to the *entire* system, not just the datacenter.
202
+ emissionsPerVisitInGrams(energyPerVisit, carbonintensity = GLOBAL_INTENSITY) {
203
+ return formatNumber(energyPerVisit * carbonintensity);
204
+ }
205
+
206
+ annualEnergyInKwh(energyPerVisit, monthlyVisitors = 1000) {
207
+ return energyPerVisit * monthlyVisitors * 12;
208
+ }
209
+
210
+ annualEmissionsInGrams(co2grams, monthlyVisitors = 1000) {
211
+ return co2grams * monthlyVisitors * 12;
212
+ }
213
+
214
+ annualSegmentEnergy(annualEnergy) {
215
+ return {
216
+ consumerDeviceEnergy: formatNumber(annualEnergy * END_USER_DEVICE_ENERGY),
217
+ networkEnergy: formatNumber(annualEnergy * NETWORK_ENERGY),
218
+ dataCenterEnergy: formatNumber(annualEnergy * DATACENTER_ENERGY),
219
+ productionEnergy: formatNumber(annualEnergy * PRODUCTION_ENERGY),
220
+ };
221
+ }
222
+ }
223
+
224
+ module.exports = SustainableWebDesign;
@@ -0,0 +1,81 @@
1
+ const SustainableWebDesign = require("./sustainable-web-design");
2
+
3
+ describe("sustainable web design model", () => {
4
+ const swd = new SustainableWebDesign();
5
+ const averageWebsiteInBytes = 2257715.2;
6
+
7
+ describe("energyPerByteByComponent", () => {
8
+ it("should return a object with numbers for each system component", () => {
9
+ const groupedEnergy = swd.energyPerByteByComponent(averageWebsiteInBytes);
10
+
11
+ expect(groupedEnergy.consumerDeviceEnergy).toBeCloseTo(0.00088564, 8);
12
+ expect(groupedEnergy.networkEnergy).toBeCloseTo(0.00023844, 8);
13
+ expect(groupedEnergy.productionEnergy).toBeCloseTo(0.0003236, 8);
14
+ expect(groupedEnergy.dataCenterEnergy).toBeCloseTo(0.00025547, 8);
15
+ });
16
+ });
17
+
18
+ describe("energyPerByte", () => {
19
+ it("should return a number in kilowatt hours for the given data transfer in bytes", () => {
20
+ const energyForTransfer = swd.energyPerByte(averageWebsiteInBytes);
21
+ expect(energyForTransfer).toBeCloseTo(0.00170316, 7);
22
+ });
23
+ });
24
+
25
+ describe("perByte", () => {
26
+ it("should return a single number for CO2 emissions", () => {
27
+ expect(typeof swd.perByte(2257715.2)).toBe("number");
28
+ });
29
+ });
30
+
31
+ describe("energyPerVisit", function () {
32
+ it("should return a number", () => {
33
+ expect(typeof swd.energyPerVisit(averageWebsiteInBytes)).toBe("number");
34
+ });
35
+
36
+ it("should calculate the correct energy", () => {
37
+ expect(swd.energyPerVisit(averageWebsiteInBytes)).toBe(
38
+ 0.0004513362121582032
39
+ );
40
+ });
41
+ });
42
+
43
+ describe("emissionsPerVisitInGrams", function () {
44
+ it("should calculate the correct co2 per visit", () => {
45
+ const averageWebsiteInBytes = 2257715.2;
46
+ const energy = swd.energyPerVisit(averageWebsiteInBytes);
47
+ expect(swd.emissionsPerVisitInGrams(energy)).toEqual(0.2);
48
+ });
49
+
50
+ it("should accept a dynamic KwH value", () => {
51
+ const averageWebsiteInBytes = 2257715.2;
52
+ const energy = swd.energyPerVisit(averageWebsiteInBytes);
53
+ expect(swd.emissionsPerVisitInGrams(energy, 245)).toEqual(0.11);
54
+ });
55
+ });
56
+
57
+ describe("annualEnergyInKwh", function () {
58
+ it("should calculate the correct energy in kWh", () => {
59
+ expect(swd.annualEnergyInKwh(averageWebsiteInBytes)).toBe(27092582400);
60
+ });
61
+ });
62
+
63
+ describe("annualEmissionsInGrams", function () {
64
+ it("should calculate the corrent energy in grams", () => {
65
+ expect(swd.annualEmissionsInGrams(averageWebsiteInBytes)).toBe(
66
+ 27092582400
67
+ );
68
+ });
69
+ });
70
+
71
+ describe("annualSegmentEnergy", function () {
72
+ it("should return the correct values", () => {
73
+ expect(swd.annualSegmentEnergy(averageWebsiteInBytes)).toEqual({
74
+ consumerDeviceEnergy: 1174011.9,
75
+ dataCenterEnergy: 338657.28,
76
+ networkEnergy: 316080.13,
77
+ productionEnergy: 428965.89,
78
+ });
79
+ });
80
+ });
81
+ });