From 2ab9773d085c2e4e97b976bc067620c88e0197e1 Mon Sep 17 00:00:00 2001 From: Matthew Griffin Date: Thu, 2 May 2024 08:50:00 +0100 Subject: [PATCH 1/7] Use CO2.js for Carbon Intensity values --- docs/estimation.md | 20 --------- docs/types.md | 16 +++---- .../assumptions-and-limitation.component.html | 8 ++-- .../assumptions-and-limitation.component.ts | 20 ++++----- .../carbon-estimator-form.component.ts | 20 ++++----- .../carbon-estimator-form.constants.ts | 8 ++-- src/app/estimation/device-usage.spec.ts | 16 +++---- .../estimate-downstream-emissions.spec.ts | 28 ++++++------- .../estimate-downstream-emissions.ts | 6 +-- .../estimate-energy-emissions.spec.ts | 8 ++-- .../estimation/estimate-energy-emissions.ts | 21 ++-------- .../estimate-indirect-emissions.spec.ts | 6 +-- .../carbon-estimation.service.spec.ts | 42 +++++++++---------- src/app/types/carbon-estimator.ts | 16 +++---- src/app/types/units.ts | 2 - 15 files changed, 100 insertions(+), 137 deletions(-) diff --git a/docs/estimation.md b/docs/estimation.md index c030a417..f0e8db3b 100644 --- a/docs/estimation.md +++ b/docs/estimation.md @@ -51,9 +51,7 @@ classDiagram class estimate-energy-emissions { <> - +locationIntensityMap: Record~WorldLocation, KgCo2ePerKwh~ +estimateEnergyEmissions(...) KgCo2e - +getCarbonIntensity(...) gCo2ePerKwh } } @@ -314,21 +312,3 @@ Estimate emissions from energy used in a location. ##### Returns [`KgCo2e`](types.md#units) - Kg of CO2e emitted via energy use. - -#### `getCarbonIntensity()` - -Exported to get Carbon Intensity in unit that [CO2.js](https://www.thegreenwebfoundation.org/co2-js/) library requires. - -##### Parameters - -`location:`[`WorldLocation`](types.md#estimatorvalues) - The World Location for Carbon Intensity. - -##### Returns - -[`gCo2ePerKwh`](types.md#units) - g of CO2e emitted per kWh of energy used. - -### Exported variables - -#### `locationIntensityMap: Record` - -Exported to allow use in assumptions component. diff --git a/docs/types.md b/docs/types.md index 3048d862..08505d00 100644 --- a/docs/types.md +++ b/docs/types.md @@ -52,14 +52,14 @@ classDiagram class WorldLocation{ <> - 'global' - 'uk' - 'europe' - 'northAmerica' - 'asia' - 'africa' - 'oceania' - 'latinAmerica' + 'WORLD' + 'GBR' + 'EUROPE' + 'NORTH AMERICA' + 'ASIA' + 'AFRICA' + 'OCEANIA' + 'LATIN AMERICA AND CARIBBEAN' } class CostRange { diff --git a/src/app/assumptions-and-limitation/assumptions-and-limitation.component.html b/src/app/assumptions-and-limitation/assumptions-and-limitation.component.html index 8ba4b1cb..5932f877 100644 --- a/src/app/assumptions-and-limitation/assumptions-and-limitation.component.html +++ b/src/app/assumptions-and-limitation/assumptions-and-limitation.component.html @@ -92,21 +92,21 @@

Power consumption

Carbon Intensity

We make use of the latest available Carbon Intensity figures from the - Ember Data Explorer for the - regions we make available. At present these are: + Ember Data Explorer via the + CO2.js library. We limit the range of regions that can be selected, which are currently:

- + @for (item of locationCarbonInfo; track $index) { - + } diff --git a/src/app/assumptions-and-limitation/assumptions-and-limitation.component.ts b/src/app/assumptions-and-limitation/assumptions-and-limitation.component.ts index bcad5e25..acde5421 100644 --- a/src/app/assumptions-and-limitation/assumptions-and-limitation.component.ts +++ b/src/app/assumptions-and-limitation/assumptions-and-limitation.component.ts @@ -3,7 +3,7 @@ import { CLOUD_AVERAGE_PUE, ON_PREMISE_AVERAGE_PUE } from '../estimation/constan import { siteTypeInfo } from '../estimation/estimate-downstream-emissions'; import { PurposeOfSite, WorldLocation, locationArray, purposeOfSiteArray } from '../types/carbon-estimator'; import { DecimalPipe } from '@angular/common'; -import { locationIntensityMap } from '../estimation/estimate-energy-emissions'; +import { averageIntensity } from '@tgwf/co2'; const purposeDescriptions: Record = { information: 'Information', @@ -14,14 +14,14 @@ const purposeDescriptions: Record = { }; const locationDescriptions: Record = { - global: 'Global', - northAmerica: 'North America', - europe: 'Europe', - uk: 'United Kingdom', - asia: 'Asia', - africa: 'Africa', - oceania: 'Oceania', - latinAmerica: 'Latin America and Caribbean', + WORLD: 'Global', + 'NORTH AMERICA': 'North America', + EUROPE: 'Europe', + GBR: 'United Kingdom', + ASIA: 'Asia', + AFRICA: 'Africa', + OCEANIA: 'Oceania', + 'LATIN AMERICA AND CARIBBEAN': 'Latin America and Caribbean', }; @Component({ @@ -41,7 +41,7 @@ export class AssumptionsAndLimitationComponent implements AfterContentInit { })); readonly locationCarbonInfo = locationArray.map(location => ({ location: locationDescriptions[location], - carbonIntensity: locationIntensityMap[location], + carbonIntensity: averageIntensity.data[location], })); @ViewChild('assumptionsLimitation', { static: true }) public assumptionsLimitation!: ElementRef; diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index c2169102..adc4f4e3 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -10,14 +10,14 @@ import { FormatCostRangePipe } from '../pipes/format-cost-range.pipe'; import { HelperInfoComponent } from '../helper-info/helper-info.component'; const locationDescriptions: Record = { - global: 'Globally', - northAmerica: 'in North America', - europe: 'in Europe', - uk: 'in the UK', - asia: 'in Asia', - africa: 'in Africa', - oceania: 'in Oceania', - latinAmerica: 'in Latin America or the Caribbean', + WORLD: 'Globally', + 'NORTH AMERICA': 'in North America', + EUROPE: 'in Europe', + GBR: 'in the UK', + ASIA: 'in Asia', + AFRICA: 'in Africa', + OCEANIA: 'in Oceania', + 'LATIN AMERICA AND CARIBBEAN': 'in Latin America or the Caribbean', }; @Component({ @@ -150,10 +150,10 @@ export class CarbonEstimatorFormComponent implements OnInit { public handleSubmit() { const formValue = this.estimatorForm.getRawValue(); if (formValue.onPremise.serverLocation === 'unknown') { - formValue.onPremise.serverLocation = 'global'; + formValue.onPremise.serverLocation = 'WORLD'; } if (formValue.cloud.cloudLocation === 'unknown') { - formValue.cloud.cloudLocation = 'global'; + formValue.cloud.cloudLocation = 'WORLD'; } this.formSubmit.emit(formValue as EstimatorValues); } diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts b/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts index c0733248..57e802d1 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts @@ -19,22 +19,22 @@ export const defaultValues: Required = { upstream: { headCount: 100, desktopPercentage: 50, - employeeLocation: 'global', + employeeLocation: 'WORLD', }, onPremise: { estimateServerCount: false, - serverLocation: 'global', + serverLocation: 'WORLD', numberOfServers: 10, }, cloud: { noCloudServices: false, - cloudLocation: 'global', + cloudLocation: 'WORLD', cloudPercentage: 50, monthlyCloudBill: costRanges[0], }, downstream: { noDownstream: false, - customerLocation: 'global', + customerLocation: 'WORLD', monthlyActiveUsers: 100, mobilePercentage: 50, purposeOfSite: 'average', diff --git a/src/app/estimation/device-usage.spec.ts b/src/app/estimation/device-usage.spec.ts index c07a8b2c..e412f065 100644 --- a/src/app/estimation/device-usage.spec.ts +++ b/src/app/estimation/device-usage.spec.ts @@ -4,15 +4,15 @@ import { estimateEnergyEmissions } from './estimate-energy-emissions'; describe('createDeviceUsage()', () => { it('should expose device category', () => { - expect(createDeviceUsage(laptop, 'user', 'global', 0).category).toBe('user'); - expect(createDeviceUsage(laptop, 'server', 'global', 0).category).toBe('server'); - expect(createDeviceUsage(laptop, 'network', 'global', 0).category).toBe('network'); + expect(createDeviceUsage(laptop, 'user', 'WORLD', 0).category).toBe('user'); + expect(createDeviceUsage(laptop, 'server', 'WORLD', 0).category).toBe('server'); + expect(createDeviceUsage(laptop, 'network', 'WORLD', 0).category).toBe('network'); }); it('should estimate upstream emissions using device type', () => { spyOn(laptop, 'estimateYearlyUpstreamEmissions').and.callFake(() => 42); const deviceCount = 10; - const usage = createDeviceUsage(laptop, 'user', 'global', deviceCount); + const usage = createDeviceUsage(laptop, 'user', 'WORLD', deviceCount); expect(usage.estimateUpstreamEmissions()).toBe(42); expect(laptop.estimateYearlyUpstreamEmissions).toHaveBeenCalledOnceWith(deviceCount); @@ -21,9 +21,9 @@ describe('createDeviceUsage()', () => { it('should estimate direct emissions using device type and location', () => { spyOn(laptop, 'estimateYearlyEnergy').and.callFake(() => 42); const deviceCount = 100; - const usage = createDeviceUsage(laptop, 'user', 'global', deviceCount); + const usage = createDeviceUsage(laptop, 'user', 'WORLD', deviceCount); - const expectedEmissions = estimateEnergyEmissions(42, 'global'); + const expectedEmissions = estimateEnergyEmissions(42, 'WORLD'); expect(usage.estimateDirectEmissions()).toBe(expectedEmissions); expect(laptop.estimateYearlyEnergy).toHaveBeenCalledOnceWith(deviceCount); }); @@ -32,9 +32,9 @@ describe('createDeviceUsage()', () => { spyOn(laptop, 'estimateYearlyEnergy').and.callFake(() => 42); const deviceCount = 1000; const pue = 2; - const usage = createDeviceUsage(laptop, 'user', 'global', deviceCount, pue); + const usage = createDeviceUsage(laptop, 'user', 'WORLD', deviceCount, pue); - const expectedEmissions = estimateEnergyEmissions(84, 'global'); + const expectedEmissions = estimateEnergyEmissions(84, 'WORLD'); expect(usage.estimateDirectEmissions()).toBe(expectedEmissions); expect(laptop.estimateYearlyEnergy).toHaveBeenCalledOnceWith(deviceCount); }); diff --git a/src/app/estimation/estimate-downstream-emissions.spec.ts b/src/app/estimation/estimate-downstream-emissions.spec.ts index a179a0ba..0a166768 100644 --- a/src/app/estimation/estimate-downstream-emissions.spec.ts +++ b/src/app/estimation/estimate-downstream-emissions.spec.ts @@ -1,13 +1,13 @@ -import { Downstream, PurposeOfSite, basePurposeArray } from '../types/carbon-estimator'; +import { Downstream, DownstreamEstimation, PurposeOfSite, basePurposeArray } from '../types/carbon-estimator'; import { sumValues } from '../utils/number-object'; import { estimateDownstreamEmissions } from './estimate-downstream-emissions'; -describe('estimateDownstreamEmissions', () => { +describe('estimateDownstreamEmissions()', () => { it('should return no emissions if no downstream is requested', () => { const input: Downstream = { noDownstream: true, monthlyActiveUsers: 100, - customerLocation: 'global', + customerLocation: 'WORLD', mobilePercentage: 0, purposeOfSite: 'average', }; @@ -21,40 +21,40 @@ describe('estimateDownstreamEmissions', () => { return { noDownstream: false, monthlyActiveUsers: 100, - customerLocation: 'global', + customerLocation: 'WORLD', mobilePercentage: 0, purposeOfSite: purposeOfSite, }; } + function expectEmissionsCloseTo(actual: DownstreamEstimation, expected: DownstreamEstimation) { + expect(actual.endUser).withContext('endUser').toBeCloseTo(expected.endUser); + expect(actual.networkTransfer).withContext('networkTransfer').toBeCloseTo(expected.networkTransfer); + } + it('should return emissions for information site', () => { const result = estimateDownstreamEmissions(createInput('information')); - expect(result.endUser).toBeCloseTo(0.50222); - expect(result.networkTransfer).toBeCloseTo(0.0525016); + expectEmissionsCloseTo(result, { endUser: 0.5, networkTransfer: 0.05 }); }); it('should return emissions for e-commerce site', () => { const result = estimateDownstreamEmissions(createInput('eCommerce')); - expect(result.endUser).toBeCloseTo(3.13888); - expect(result.networkTransfer).toBeCloseTo(1.11322); + expectEmissionsCloseTo(result, { endUser: 3.13888, networkTransfer: 1.11322 }); }); it('should return emissions for social media site', () => { const result = estimateDownstreamEmissions(createInput('socialMedia')); - expect(result.endUser).toBeCloseTo(511.637); - expect(result.networkTransfer).toBeCloseTo(298.761); + expectEmissionsCloseTo(result, { endUser: 511.55, networkTransfer: 298.71 }); }); it('should return emissions for streaming site', () => { const result = estimateDownstreamEmissions(createInput('streaming')); - expect(result.endUser).toBeCloseTo(695.038); - expect(result.networkTransfer).toBeCloseTo(698.533); + expectEmissionsCloseTo(result, { endUser: 694.93, networkTransfer: 698.42 }); }); it('should return emissions based on average values', () => { const result = estimateDownstreamEmissions(createInput('average')); - expect(result.endUser).toBeCloseTo(302.579); - expect(result.networkTransfer).toBeCloseTo(249.615); + expectEmissionsCloseTo(result, { endUser: 302.53, networkTransfer: 249.57 }); }); it('should create average equivalent to average of all other purposes', () => { diff --git a/src/app/estimation/estimate-downstream-emissions.ts b/src/app/estimation/estimate-downstream-emissions.ts index 37de77a2..c3e6d437 100644 --- a/src/app/estimation/estimate-downstream-emissions.ts +++ b/src/app/estimation/estimate-downstream-emissions.ts @@ -5,10 +5,10 @@ import { BasePurposeOfSite, basePurposeArray, } from '../types/carbon-estimator'; -import { estimateEnergyEmissions, getCarbonIntensity } from './estimate-energy-emissions'; +import { estimateEnergyEmissions } from './estimate-energy-emissions'; import { Gb, Hour, KilowattHour } from '../types/units'; import { AverageDeviceType, averagePersonalComputer, mobile } from './device-type'; -import { co2 } from '@tgwf/co2'; +import { co2, averageIntensity } from '@tgwf/co2'; interface SiteInformation { averageMonthlyUserTime: Hour; @@ -98,7 +98,7 @@ function estimateNetworkEmissions(downstream: Downstream, downstreamDataTransfer const options = { gridIntensity: { device: 0, - network: getCarbonIntensity(downstream.customerLocation), + network: averageIntensity.data[downstream.customerLocation], dataCenter: 0, }, }; diff --git a/src/app/estimation/estimate-energy-emissions.spec.ts b/src/app/estimation/estimate-energy-emissions.spec.ts index 98fa7679..a98634a3 100644 --- a/src/app/estimation/estimate-energy-emissions.spec.ts +++ b/src/app/estimation/estimate-energy-emissions.spec.ts @@ -2,18 +2,18 @@ import { estimateEnergyEmissions } from './estimate-energy-emissions'; describe('Estimate Direct emissions', () => { it('Should estimate emissions using global average', () => { - expect(estimateEnergyEmissions(1, 'global')).toBeCloseTo(0.494); + expect(estimateEnergyEmissions(1, 'WORLD')).toBeCloseTo(0.494); }); it('Should estimate emissions using North America average', () => { - expect(estimateEnergyEmissions(1, 'northAmerica')).toBeCloseTo(0.356); + expect(estimateEnergyEmissions(1, 'NORTH AMERICA')).toBeCloseTo(0.378); }); it('Should estimate emissions using europe average', () => { - expect(estimateEnergyEmissions(1, 'europe')).toBeCloseTo(0.33); + expect(estimateEnergyEmissions(1, 'EUROPE')).toBeCloseTo(0.33); }); it('Should estimate emissions using uk average', () => { - expect(estimateEnergyEmissions(1, 'uk')).toBeCloseTo(0.238); + expect(estimateEnergyEmissions(1, 'GBR')).toBeCloseTo(0.256); }); }); diff --git a/src/app/estimation/estimate-energy-emissions.ts b/src/app/estimation/estimate-energy-emissions.ts index 0500e45d..4b841562 100644 --- a/src/app/estimation/estimate-energy-emissions.ts +++ b/src/app/estimation/estimate-energy-emissions.ts @@ -1,22 +1,7 @@ -import { KgCo2e, KgCo2ePerKwh, KilowattHour, gCo2ePerKwh } from '../types/units'; +import { KgCo2e, KilowattHour } from '../types/units'; import { WorldLocation } from '../types/carbon-estimator'; - -// Carbon Intensity figures sourced from https://ember-climate.org/data/data-tools/data-explorer/ -export const locationIntensityMap: Record = { - global: 0.494, - northAmerica: 0.356, - europe: 0.328, - uk: 0.238, - asia: 0.591, - africa: 0.559, - oceania: 0.508, - latinAmerica: 0.26, -}; +import { averageIntensity } from '@tgwf/co2'; export function estimateEnergyEmissions(energy: KilowattHour, location: WorldLocation): KgCo2e { - return locationIntensityMap[location] * energy; -} - -export function getCarbonIntensity(location: WorldLocation): gCo2ePerKwh { - return locationIntensityMap[location] * 1000; + return (averageIntensity.data[location] * energy) / 1000; } diff --git a/src/app/estimation/estimate-indirect-emissions.spec.ts b/src/app/estimation/estimate-indirect-emissions.spec.ts index 5a8fe77f..c82c4071 100644 --- a/src/app/estimation/estimate-indirect-emissions.spec.ts +++ b/src/app/estimation/estimate-indirect-emissions.spec.ts @@ -6,7 +6,7 @@ it('should return no emissions if cloud not used', () => { noCloudServices: true, cloudPercentage: 100, monthlyCloudBill: { min: 5000, max: 10000 }, - cloudLocation: 'global', + cloudLocation: 'WORLD', }; const result = estimateIndirectEmissions(input); expect(result).toEqual({ @@ -21,10 +21,10 @@ it('should return emissions based on ratio of costs, expanded to a years usage', noCloudServices: false, cloudPercentage: 50, monthlyCloudBill: { min: 0, max: 200 }, - cloudLocation: 'global', + cloudLocation: 'WORLD', }; const result = estimateIndirectEmissions(input); - expect(result.cloud).toBeCloseTo(10.733552 * 12); + expect(result.cloud).toBeCloseTo(128.78); expect(result.saas).toBe(0); expect(result.managed).toBe(0); }); diff --git a/src/app/services/carbon-estimation.service.spec.ts b/src/app/services/carbon-estimation.service.spec.ts index a8e3e87a..192f47e6 100644 --- a/src/app/services/carbon-estimation.service.spec.ts +++ b/src/app/services/carbon-estimation.service.spec.ts @@ -10,22 +10,22 @@ const emptyEstimatorValues: EstimatorValues = { upstream: { headCount: 0, desktopPercentage: 0, - employeeLocation: 'global', + employeeLocation: 'WORLD', }, onPremise: { estimateServerCount: false, - serverLocation: 'global', + serverLocation: 'WORLD', numberOfServers: 0, }, cloud: { noCloudServices: true, - cloudLocation: 'global', + cloudLocation: 'WORLD', cloudPercentage: 0, monthlyCloudBill: { min: 0, max: 200 }, }, downstream: { noDownstream: true, - customerLocation: 'global', + customerLocation: 'WORLD', monthlyActiveUsers: 0, mobilePercentage: 0, purposeOfSite: 'streaming', @@ -62,7 +62,7 @@ describe('CarbonEstimationService', () => { expect(service).toBeTruthy(); }); - describe('calculateCarbonEstimation', () => { + describe('calculateCarbonEstimation()', () => { it('should include version and zeroed values in estimation', () => { const estimation = service.calculateCarbonEstimation(emptyEstimatorValues); expect(estimation.version).toBe(version); @@ -78,7 +78,7 @@ describe('CarbonEstimationService', () => { upstream: { headCount: 1, desktopPercentage: 0, - employeeLocation: 'global', + employeeLocation: 'WORLD', }, }); checkTotalPercentage(estimation); @@ -100,11 +100,11 @@ describe('CarbonEstimationService', () => { upstream: { headCount: 4, desktopPercentage: 50, - employeeLocation: 'global', + employeeLocation: 'WORLD', }, onPremise: { estimateServerCount: false, - serverLocation: 'global', + serverLocation: 'WORLD', numberOfServers: 2, }, }; @@ -123,21 +123,21 @@ describe('CarbonEstimationService', () => { upstream: { headCount: 4, desktopPercentage: 50, - employeeLocation: 'uk', + employeeLocation: 'GBR', }, onPremise: { estimateServerCount: false, - serverLocation: 'global', + serverLocation: 'WORLD', numberOfServers: 2, }, }; const result = service.calculateCarbonEstimation(hardwareInput); - expect(result.upstreamEmissions.user).withContext('upstreamEmissions.user').toBeCloseTo(3.74); - expect(result.upstreamEmissions.server).withContext('upstreamEmissions.server').toBeCloseTo(8.6); - expect(result.upstreamEmissions.network).withContext('upstreamEmissions.network').toBeCloseTo(3.85); - expect(result.directEmissions.user).withContext('directEmissions.user').toBeCloseTo(0.92); - expect(result.directEmissions.server).withContext('directEmissions.server').toBeCloseTo(64.87); - expect(result.directEmissions.network).withContext('directEmissions.network').toBeCloseTo(18.02); + expect(result.upstreamEmissions.user).withContext('upstreamEmissions.user').toBeCloseTo(3.72); + expect(result.upstreamEmissions.server).withContext('upstreamEmissions.server').toBeCloseTo(8.56); + expect(result.upstreamEmissions.network).withContext('upstreamEmissions.network').toBeCloseTo(3.84); + expect(result.directEmissions.user).withContext('directEmissions.user').toBeCloseTo(0.99); + expect(result.directEmissions.server).withContext('directEmissions.server').toBeCloseTo(64.54); + expect(result.directEmissions.network).withContext('directEmissions.network').toBeCloseTo(18.37); }); }); @@ -152,11 +152,11 @@ describe('CarbonEstimationService', () => { upstream: { headCount: 100, desktopPercentage: 0, - employeeLocation: 'global', + employeeLocation: 'WORLD', }, onPremise: { estimateServerCount: true, - serverLocation: 'global', + serverLocation: 'WORLD', numberOfServers: 0, }, }; @@ -169,17 +169,17 @@ describe('CarbonEstimationService', () => { upstream: { headCount: 100, desktopPercentage: 0, - employeeLocation: 'global', + employeeLocation: 'WORLD', }, onPremise: { estimateServerCount: true, - serverLocation: 'global', + serverLocation: 'WORLD', numberOfServers: 0, }, cloud: { cloudPercentage: 50, noCloudServices: false, - cloudLocation: 'global', + cloudLocation: 'WORLD', monthlyCloudBill: { min: 0, max: 200 }, }, }; diff --git a/src/app/types/carbon-estimator.ts b/src/app/types/carbon-estimator.ts index 8241fb42..fd431794 100644 --- a/src/app/types/carbon-estimator.ts +++ b/src/app/types/carbon-estimator.ts @@ -90,14 +90,14 @@ export type Downstream = { export type DeviceCategory = 'user' | 'server' | 'network'; export const locationArray = [ - 'global', - 'uk', - 'europe', - 'northAmerica', - 'asia', - 'africa', - 'oceania', - 'latinAmerica', + 'WORLD', + 'GBR', + 'EUROPE', + 'NORTH AMERICA', + 'ASIA', + 'AFRICA', + 'OCEANIA', + 'LATIN AMERICA AND CARIBBEAN', ] as const; export type WorldLocation = (typeof locationArray)[number]; diff --git a/src/app/types/units.ts b/src/app/types/units.ts index 0ab43f43..5839b074 100644 --- a/src/app/types/units.ts +++ b/src/app/types/units.ts @@ -2,7 +2,5 @@ export type Watt = number; export type Hour = number; export type Year = number; export type KilowattHour = number; -export type KgCo2ePerKwh = number; -export type gCo2ePerKwh = number; export type KgCo2e = number; export type Gb = number; From 6f663111344b5e0314e28eba631bbbda12d53c3f Mon Sep 17 00:00:00 2001 From: Matthew Griffin Date: Thu, 2 May 2024 15:04:26 +0100 Subject: [PATCH 2/7] Can pass country name directly to CO2.js now it is in right format --- src/app/estimation/estimate-downstream-emissions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/estimation/estimate-downstream-emissions.ts b/src/app/estimation/estimate-downstream-emissions.ts index c3e6d437..a0881628 100644 --- a/src/app/estimation/estimate-downstream-emissions.ts +++ b/src/app/estimation/estimate-downstream-emissions.ts @@ -8,7 +8,7 @@ import { import { estimateEnergyEmissions } from './estimate-energy-emissions'; import { Gb, Hour, KilowattHour } from '../types/units'; import { AverageDeviceType, averagePersonalComputer, mobile } from './device-type'; -import { co2, averageIntensity } from '@tgwf/co2'; +import { co2 } from '@tgwf/co2'; interface SiteInformation { averageMonthlyUserTime: Hour; @@ -98,7 +98,7 @@ function estimateNetworkEmissions(downstream: Downstream, downstreamDataTransfer const options = { gridIntensity: { device: 0, - network: averageIntensity.data[downstream.customerLocation], + network: { country: downstream.customerLocation }, dataCenter: 0, }, }; From 31d802ea70f5a1eb218f41b14438dfa03c136961 Mon Sep 17 00:00:00 2001 From: Matthew Griffin Date: Fri, 3 May 2024 16:56:04 +0100 Subject: [PATCH 3/7] Remove unit types from docs --- docs/types.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/types.md b/docs/types.md index 08505d00..1ca32309 100644 --- a/docs/types.md +++ b/docs/types.md @@ -172,8 +172,6 @@ export type Watt = number; export type Hour = number; export type Year = number; export type KilowattHour = number; -export type KgCo2ePerKwh = number; -export type gCo2ePerKwh = number; export type KgCo2e = number; export type Gb = number; ``` From c934d9162a566996c1df392f3f6f1c61e0aa5a05 Mon Sep 17 00:00:00 2001 From: Matthew Griffin Date: Tue, 7 May 2024 15:03:18 +0100 Subject: [PATCH 4/7] Add a carbon intensity service to contain the use of CO2.js Pass down the carbon intensity instead of looking it up during calculations --- docs/estimation.md | 10 ++- docs/services.md | 32 ++++++- docs/types.md | 1 + src/app/estimation/device-usage.spec.ts | 18 ++-- src/app/estimation/device-usage.ts | 8 +- .../estimate-downstream-emissions.spec.ts | 28 ++++--- .../estimate-downstream-emissions.ts | 17 ++-- .../estimate-energy-emissions.spec.ts | 19 +---- .../estimation/estimate-energy-emissions.ts | 8 +- .../estimate-indirect-emissions.spec.ts | 53 ++++++------ .../estimation/estimate-indirect-emissions.ts | 6 +- .../carbon-estimation.service.spec.ts | 84 +++++++++++++++---- src/app/services/carbon-estimation.service.ts | 26 +++--- .../services/carbon-intensity.service.spec.ts | 16 ++++ src/app/services/carbon-intensity.service.ts | 15 ++++ src/app/types/units.ts | 1 + 16 files changed, 234 insertions(+), 108 deletions(-) create mode 100644 src/app/services/carbon-intensity.service.spec.ts create mode 100644 src/app/services/carbon-intensity.service.ts diff --git a/docs/estimation.md b/docs/estimation.md index f0e8db3b..bbfe8537 100644 --- a/docs/estimation.md +++ b/docs/estimation.md @@ -18,13 +18,13 @@ classDiagram class estimate-indirect-emissions{ <> - +estimateIndirectEmissions(input: Cloud) IndirectEstimation + +estimateIndirectEmissions(input: Cloud, intensity: gCo2ePerKwh) IndirectEstimation } class estimate-downstream-emissions{ <> +siteTypeInfo: Record~PurposeOfSite, SiteInformation~ - +estimateDownstreamEmissions(downstream: Downstream) DownstreamEstimation + +estimateDownstreamEmissions(downstream: Downstream, ...) DownstreamEstimation } } @@ -107,6 +107,7 @@ Estimate emissions from Indirect categories ##### Parameters `input:`[`Cloud`](types.md#estimatorvalues) - The inputs relevant to cloud. +`intensity:`[`gCo2ePerKwh`](types.md#units) - The Carbon intensity of the cloud region. ##### Returns @@ -138,6 +139,7 @@ Estimate emissions from Downstream categories ##### Parameters `downstream:`[`Downstream`](types.md#estimatorvalues) - The inputs relevant to downstream emissions. +`intensity:`[`gCo2ePerKwh`](types.md#units) - The Carbon intensity of the downstream region. ##### Returns @@ -288,7 +290,7 @@ Creates device usage without exposing the exact method of calculation. `type:`[`DeviceType`](#devicetype) - The type of device being used. `category:`[`DeviceCategory`](types#devicecategory) - The category the device usage falls under. -`location:`[`WorldLocation`](types#estimatorvalues) - The location the devices are being used in. +`intensity:`[`gCo2ePerKwh`](types#units) - The carbon intensity of the region the devices are being used in. `count: number` - The number of devices being used. `pue?: number` - The Power Usage Effectiveness of the device usage (optional, defaults to 1). @@ -307,7 +309,7 @@ Estimate emissions from energy used in a location. ##### Parameters `energy:`[`KilowattHour`](types.md#units) - Amount of energy used. -`location:`[`WorldLocation`](types.md#estimatorvalues) - The World Location where the energy was used for Carbon Intensity. +`intensity:`[`gCo2ePerKwh`](types.md#units) - The Carbon Intensity of the region where the energy was used. ##### Returns diff --git a/docs/services.md b/docs/services.md index a51e5506..ade60465 100644 --- a/docs/services.md +++ b/docs/services.md @@ -6,17 +6,24 @@ This page details the Angular services that are part of the application. classDiagram class CarbonEstimationService{ <> - -loggingService: LoggingService + -carbonIntensityService: CarbonIntensityService + -loggingService: LoggingService +calculateCarbonEstimation(formValue: EstimatorValues) CarbonEstimation +estimateServerCount(formValue: EstimatorValues) number -estimateDeviceUsage(formValue: EstimatorValues) DeviceUsage[] } + class CarbonIntensityService{ + <> + +getCarbonIntensity(location: WorldLocation) gCo2ePerKwh + } + class LoggingService{ <> +log(...output: any[]) } + CarbonEstimationService --> "-carbonIntensityService" CarbonIntensityService CarbonEstimationService --> "-loggingService" LoggingService ``` @@ -30,6 +37,7 @@ The main service responsible for producing a carbon estimate. Takes input form values and uses them to calculate a carbon estimation. Uses [LoggingService](#loggingservice) to output intermediate parts of the calculation. +Uses [CarbonIntensityService](#carbonintensityservice) to get the carbon intensity of input locations. Returns estimation as percentages. Uses functions in other modules to perform the calculation. @@ -57,13 +65,13 @@ classDiagram class estimate-indirect-emissions{ <> - +estimateIndirectEmissions(Cloud input) IndirectEstimation + +estimateIndirectEmissions(input: Cloud, intensity: gCo2ePerKwh) IndirectEstimation } class estimate-downstream-emissions{ <> +Record~PurposeOfSite, SiteInformation~ siteTypeInfo - +estimateDownstreamEmissions(Downstream downstream) DownstreamEstimation + +estimateDownstreamEmissions(Downstream downstream, intensity: gCo2ePerKwh) DownstreamEstimation } } @@ -93,6 +101,24 @@ Method is used as part of [`calculateCarbonEstimation()`](#calculatecarbonestima `number` - The estimated server count given the current input. +## CarbonIntensityService + +Currently a simple service to wrap the usage of the CO2.js library to reduce dependencies and allow a switch to a different provider in future. + +### Public Methods + +#### `getCarbonIntensity()` + +Gets a carbon intensity figure given a region. + +##### Parameters + +`location:`[`WorldLocation`](types.md#estimatorvalues) - The location to get the carbon intensity for. + +##### Returns + +[`gCo2ePerKwh`](types.md#units) - The carbon intensity of the location in grams of CO2 equivalent per Kilowatt hour of energy consumed. + ## LoggingService Currently a simple service to wrap console logging. diff --git a/docs/types.md b/docs/types.md index 1ca32309..9e404785 100644 --- a/docs/types.md +++ b/docs/types.md @@ -173,6 +173,7 @@ export type Hour = number; export type Year = number; export type KilowattHour = number; export type KgCo2e = number; +export type gCo2ePerKwh = number; export type Gb = number; ``` diff --git a/src/app/estimation/device-usage.spec.ts b/src/app/estimation/device-usage.spec.ts index e412f065..de066b4f 100644 --- a/src/app/estimation/device-usage.spec.ts +++ b/src/app/estimation/device-usage.spec.ts @@ -4,15 +4,15 @@ import { estimateEnergyEmissions } from './estimate-energy-emissions'; describe('createDeviceUsage()', () => { it('should expose device category', () => { - expect(createDeviceUsage(laptop, 'user', 'WORLD', 0).category).toBe('user'); - expect(createDeviceUsage(laptop, 'server', 'WORLD', 0).category).toBe('server'); - expect(createDeviceUsage(laptop, 'network', 'WORLD', 0).category).toBe('network'); + expect(createDeviceUsage(laptop, 'user', 0, 0).category).toBe('user'); + expect(createDeviceUsage(laptop, 'server', 0, 0).category).toBe('server'); + expect(createDeviceUsage(laptop, 'network', 0, 0).category).toBe('network'); }); it('should estimate upstream emissions using device type', () => { spyOn(laptop, 'estimateYearlyUpstreamEmissions').and.callFake(() => 42); const deviceCount = 10; - const usage = createDeviceUsage(laptop, 'user', 'WORLD', deviceCount); + const usage = createDeviceUsage(laptop, 'user', 0, deviceCount); expect(usage.estimateUpstreamEmissions()).toBe(42); expect(laptop.estimateYearlyUpstreamEmissions).toHaveBeenCalledOnceWith(deviceCount); @@ -21,9 +21,10 @@ describe('createDeviceUsage()', () => { it('should estimate direct emissions using device type and location', () => { spyOn(laptop, 'estimateYearlyEnergy').and.callFake(() => 42); const deviceCount = 100; - const usage = createDeviceUsage(laptop, 'user', 'WORLD', deviceCount); + const carbonIntensity = 500; + const usage = createDeviceUsage(laptop, 'user', carbonIntensity, deviceCount); - const expectedEmissions = estimateEnergyEmissions(42, 'WORLD'); + const expectedEmissions = estimateEnergyEmissions(42, carbonIntensity); expect(usage.estimateDirectEmissions()).toBe(expectedEmissions); expect(laptop.estimateYearlyEnergy).toHaveBeenCalledOnceWith(deviceCount); }); @@ -31,10 +32,11 @@ describe('createDeviceUsage()', () => { it('should apply pue to energy estimation if specified', () => { spyOn(laptop, 'estimateYearlyEnergy').and.callFake(() => 42); const deviceCount = 1000; + const carbonIntensity = 500; const pue = 2; - const usage = createDeviceUsage(laptop, 'user', 'WORLD', deviceCount, pue); + const usage = createDeviceUsage(laptop, 'user', carbonIntensity, deviceCount, pue); - const expectedEmissions = estimateEnergyEmissions(84, 'WORLD'); + const expectedEmissions = estimateEnergyEmissions(84, carbonIntensity); expect(usage.estimateDirectEmissions()).toBe(expectedEmissions); expect(laptop.estimateYearlyEnergy).toHaveBeenCalledOnceWith(deviceCount); }); diff --git a/src/app/estimation/device-usage.ts b/src/app/estimation/device-usage.ts index a5e054a7..606cf3b3 100644 --- a/src/app/estimation/device-usage.ts +++ b/src/app/estimation/device-usage.ts @@ -1,5 +1,5 @@ -import { DeviceCategory, WorldLocation } from '../types/carbon-estimator'; -import { KgCo2e } from '../types/units'; +import { DeviceCategory } from '../types/carbon-estimator'; +import { KgCo2e, gCo2ePerKwh } from '../types/units'; import { DeviceType } from './device-type'; import { estimateEnergyEmissions } from './estimate-energy-emissions'; @@ -12,7 +12,7 @@ export interface DeviceUsage { export function createDeviceUsage( type: DeviceType, category: DeviceCategory, - location: WorldLocation, + intensity: gCo2ePerKwh, count: number, pue?: number ): DeviceUsage { @@ -22,7 +22,7 @@ export function createDeviceUsage( estimateUpstreamEmissions: () => type.estimateYearlyUpstreamEmissions(count), estimateDirectEmissions: () => { const energy = type.estimateYearlyEnergy(count) * actualPue; - return estimateEnergyEmissions(energy, location); + return estimateEnergyEmissions(energy, intensity); }, }; } diff --git a/src/app/estimation/estimate-downstream-emissions.spec.ts b/src/app/estimation/estimate-downstream-emissions.spec.ts index 0a166768..2eea14f7 100644 --- a/src/app/estimation/estimate-downstream-emissions.spec.ts +++ b/src/app/estimation/estimate-downstream-emissions.spec.ts @@ -3,6 +3,8 @@ import { sumValues } from '../utils/number-object'; import { estimateDownstreamEmissions } from './estimate-downstream-emissions'; describe('estimateDownstreamEmissions()', () => { + const carbonIntensity = 500; + it('should return no emissions if no downstream is requested', () => { const input: Downstream = { noDownstream: true, @@ -11,7 +13,7 @@ describe('estimateDownstreamEmissions()', () => { mobilePercentage: 0, purposeOfSite: 'average', }; - expect(estimateDownstreamEmissions(input)).toEqual({ + expect(estimateDownstreamEmissions(input, carbonIntensity)).toEqual({ endUser: 0, networkTransfer: 0, }); @@ -33,36 +35,36 @@ describe('estimateDownstreamEmissions()', () => { } it('should return emissions for information site', () => { - const result = estimateDownstreamEmissions(createInput('information')); - expectEmissionsCloseTo(result, { endUser: 0.5, networkTransfer: 0.05 }); + const result = estimateDownstreamEmissions(createInput('information'), carbonIntensity); + expectEmissionsCloseTo(result, { endUser: 0.51, networkTransfer: 0.05 }); }); it('should return emissions for e-commerce site', () => { - const result = estimateDownstreamEmissions(createInput('eCommerce')); - expectEmissionsCloseTo(result, { endUser: 3.13888, networkTransfer: 1.11322 }); + const result = estimateDownstreamEmissions(createInput('eCommerce'), carbonIntensity); + expectEmissionsCloseTo(result, { endUser: 3.18, networkTransfer: 1.1 }); }); it('should return emissions for social media site', () => { - const result = estimateDownstreamEmissions(createInput('socialMedia')); - expectEmissionsCloseTo(result, { endUser: 511.55, networkTransfer: 298.71 }); + const result = estimateDownstreamEmissions(createInput('socialMedia'), carbonIntensity); + expectEmissionsCloseTo(result, { endUser: 517.851, networkTransfer: 296.41 }); }); it('should return emissions for streaming site', () => { - const result = estimateDownstreamEmissions(createInput('streaming')); - expectEmissionsCloseTo(result, { endUser: 694.93, networkTransfer: 698.42 }); + const result = estimateDownstreamEmissions(createInput('streaming'), carbonIntensity); + expectEmissionsCloseTo(result, { endUser: 703.48, networkTransfer: 693.03 }); }); it('should return emissions based on average values', () => { - const result = estimateDownstreamEmissions(createInput('average')); - expectEmissionsCloseTo(result, { endUser: 302.53, networkTransfer: 249.57 }); + const result = estimateDownstreamEmissions(createInput('average'), carbonIntensity); + expectEmissionsCloseTo(result, { endUser: 306.25, networkTransfer: 247.65 }); }); it('should create average equivalent to average of all other purposes', () => { const totalEmissions = basePurposeArray - .map(purpose => sumValues(estimateDownstreamEmissions(createInput(purpose)))) + .map(purpose => sumValues(estimateDownstreamEmissions(createInput(purpose), carbonIntensity))) .reduce((x, y) => x + y); const expectedAverage = totalEmissions / basePurposeArray.length; - const result = estimateDownstreamEmissions(createInput('average')); + const result = estimateDownstreamEmissions(createInput('average'), carbonIntensity); expect(sumValues(result)).toBeCloseTo(expectedAverage); }); }); diff --git a/src/app/estimation/estimate-downstream-emissions.ts b/src/app/estimation/estimate-downstream-emissions.ts index a0881628..f8152a84 100644 --- a/src/app/estimation/estimate-downstream-emissions.ts +++ b/src/app/estimation/estimate-downstream-emissions.ts @@ -6,7 +6,7 @@ import { basePurposeArray, } from '../types/carbon-estimator'; import { estimateEnergyEmissions } from './estimate-energy-emissions'; -import { Gb, Hour, KilowattHour } from '../types/units'; +import { Gb, Hour, KilowattHour, gCo2ePerKwh } from '../types/units'; import { AverageDeviceType, averagePersonalComputer, mobile } from './device-type'; import { co2 } from '@tgwf/co2'; @@ -54,7 +54,10 @@ export const siteTypeInfo: Record = addAverage({ }, }); -export function estimateDownstreamEmissions(downstream: Downstream): DownstreamEstimation { +export function estimateDownstreamEmissions( + downstream: Downstream, + downstreamIntensity: gCo2ePerKwh +): DownstreamEstimation { if (downstream.noDownstream) { return { endUser: 0, networkTransfer: 0 }; } @@ -63,7 +66,7 @@ export function estimateDownstreamEmissions(downstream: Downstream): DownstreamE downstream.monthlyActiveUsers, downstream.purposeOfSite ); - const endUserEmissions = estimateEndUserEmissions(downstream, downstreamDataTransfer); + const endUserEmissions = estimateEndUserEmissions(downstream, downstreamDataTransfer, downstreamIntensity); const networkEmissions = estimateNetworkEmissions(downstream, downstreamDataTransfer); return { endUser: endUserEmissions, networkTransfer: networkEmissions }; } @@ -72,10 +75,14 @@ function estimateDownstreamDataTransfer(monthlyActiveUsers: number, purposeOfSit return siteTypeInfo[purposeOfSite].averageMonthlyUserData * monthlyActiveUsers * 12; } -function estimateEndUserEmissions(downstream: Downstream, downstreamDataTransfer: number) { +function estimateEndUserEmissions( + downstream: Downstream, + downstreamDataTransfer: number, + downstreamIntensity: gCo2ePerKwh +) { const endUserTime = estimateEndUserTime(downstream.monthlyActiveUsers, downstream.purposeOfSite); const endUserEnergy = estimateEndUserEnergy(downstreamDataTransfer, endUserTime, downstream.mobilePercentage); - return estimateEnergyEmissions(endUserEnergy, downstream.customerLocation); + return estimateEnergyEmissions(endUserEnergy, downstreamIntensity); } function estimateEndUserTime(monthlyActiveUsers: number, purposeOfSite: PurposeOfSite): Hour { diff --git a/src/app/estimation/estimate-energy-emissions.spec.ts b/src/app/estimation/estimate-energy-emissions.spec.ts index a98634a3..5097ffb6 100644 --- a/src/app/estimation/estimate-energy-emissions.spec.ts +++ b/src/app/estimation/estimate-energy-emissions.spec.ts @@ -1,19 +1,8 @@ import { estimateEnergyEmissions } from './estimate-energy-emissions'; -describe('Estimate Direct emissions', () => { - it('Should estimate emissions using global average', () => { - expect(estimateEnergyEmissions(1, 'WORLD')).toBeCloseTo(0.494); - }); - - it('Should estimate emissions using North America average', () => { - expect(estimateEnergyEmissions(1, 'NORTH AMERICA')).toBeCloseTo(0.378); - }); - - it('Should estimate emissions using europe average', () => { - expect(estimateEnergyEmissions(1, 'EUROPE')).toBeCloseTo(0.33); - }); - - it('Should estimate emissions using uk average', () => { - expect(estimateEnergyEmissions(1, 'GBR')).toBeCloseTo(0.256); +describe('estimateEnergyEmissions()', () => { + it('Should estimate emissions using carbon intensity and convert to Kg', () => { + expect(estimateEnergyEmissions(1, 494)).toBeCloseTo(0.494); + expect(estimateEnergyEmissions(1000, 378)).toBeCloseTo(378); }); }); diff --git a/src/app/estimation/estimate-energy-emissions.ts b/src/app/estimation/estimate-energy-emissions.ts index 4b841562..025ad281 100644 --- a/src/app/estimation/estimate-energy-emissions.ts +++ b/src/app/estimation/estimate-energy-emissions.ts @@ -1,7 +1,5 @@ -import { KgCo2e, KilowattHour } from '../types/units'; -import { WorldLocation } from '../types/carbon-estimator'; -import { averageIntensity } from '@tgwf/co2'; +import { KgCo2e, KilowattHour, gCo2ePerKwh } from '../types/units'; -export function estimateEnergyEmissions(energy: KilowattHour, location: WorldLocation): KgCo2e { - return (averageIntensity.data[location] * energy) / 1000; +export function estimateEnergyEmissions(energy: KilowattHour, intensity: gCo2ePerKwh): KgCo2e { + return (intensity * energy) / 1000; } diff --git a/src/app/estimation/estimate-indirect-emissions.spec.ts b/src/app/estimation/estimate-indirect-emissions.spec.ts index c82c4071..eb33a523 100644 --- a/src/app/estimation/estimate-indirect-emissions.spec.ts +++ b/src/app/estimation/estimate-indirect-emissions.spec.ts @@ -1,30 +1,35 @@ import { Cloud } from '../types/carbon-estimator'; import { estimateIndirectEmissions } from './estimate-indirect-emissions'; -it('should return no emissions if cloud not used', () => { - const input: Cloud = { - noCloudServices: true, - cloudPercentage: 100, - monthlyCloudBill: { min: 5000, max: 10000 }, - cloudLocation: 'WORLD', - }; - const result = estimateIndirectEmissions(input); - expect(result).toEqual({ - cloud: 0, - saas: 0, - managed: 0, +describe('estimateIndirectEmissions()', () => { + const carbonIntensity = 500; + + it('should return no emissions if cloud not used', () => { + const input: Cloud = { + noCloudServices: true, + cloudPercentage: 100, + monthlyCloudBill: { min: 5000, max: 10000 }, + cloudLocation: 'WORLD', + }; + + const result = estimateIndirectEmissions(input, carbonIntensity); + expect(result).toEqual({ + cloud: 0, + saas: 0, + managed: 0, + }); }); -}); -it('should return emissions based on ratio of costs, expanded to a years usage', () => { - const input: Cloud = { - noCloudServices: false, - cloudPercentage: 50, - monthlyCloudBill: { min: 0, max: 200 }, - cloudLocation: 'WORLD', - }; - const result = estimateIndirectEmissions(input); - expect(result.cloud).toBeCloseTo(128.78); - expect(result.saas).toBe(0); - expect(result.managed).toBe(0); + it('should return emissions based on ratio of costs, expanded to a years usage', () => { + const input: Cloud = { + noCloudServices: false, + cloudPercentage: 50, + monthlyCloudBill: { min: 0, max: 200 }, + cloudLocation: 'WORLD', + }; + const result = estimateIndirectEmissions(input, carbonIntensity); + expect(result.cloud).toBeCloseTo(130.13); + expect(result.saas).toBe(0); + expect(result.managed).toBe(0); + }); }); diff --git a/src/app/estimation/estimate-indirect-emissions.ts b/src/app/estimation/estimate-indirect-emissions.ts index 0e9e2004..8573a267 100644 --- a/src/app/estimation/estimate-indirect-emissions.ts +++ b/src/app/estimation/estimate-indirect-emissions.ts @@ -1,12 +1,12 @@ import { Cloud, IndirectEstimation } from '../types/carbon-estimator'; import { estimateEnergyEmissions } from './estimate-energy-emissions'; -import { KgCo2e, KilowattHour } from '../types/units'; +import { KgCo2e, KilowattHour, gCo2ePerKwh } from '../types/units'; import { CLOUD_AVERAGE_PUE } from './constants'; // Calculated in spreadsheet, explained in assumptions-and-limitation component const COST_TO_KWH_RATIO = 0.156; const COST_TO_UPSTREAM_RATIO = 0.0164; -export function estimateIndirectEmissions(input: Cloud): IndirectEstimation { +export function estimateIndirectEmissions(input: Cloud, cloudIntensity: gCo2ePerKwh): IndirectEstimation { if (input.noCloudServices) { return { cloud: 0, @@ -16,7 +16,7 @@ export function estimateIndirectEmissions(input: Cloud): IndirectEstimation { } const midpoint = (input.monthlyCloudBill.min + input.monthlyCloudBill.max) / 2; const cloudEnergy = estimateCloudEnergy(midpoint); - const cloudDirectEmissions = estimateEnergyEmissions(cloudEnergy, input.cloudLocation); + const cloudDirectEmissions = estimateEnergyEmissions(cloudEnergy, cloudIntensity); const cloudUpstreamEmissions = estimateCloudUpstream(midpoint); return { cloud: cloudDirectEmissions + cloudUpstreamEmissions, saas: 0, managed: 0 }; } diff --git a/src/app/services/carbon-estimation.service.spec.ts b/src/app/services/carbon-estimation.service.spec.ts index 192f47e6..aec83797 100644 --- a/src/app/services/carbon-estimation.service.spec.ts +++ b/src/app/services/carbon-estimation.service.spec.ts @@ -1,10 +1,12 @@ import { TestBed } from '@angular/core/testing'; import { CarbonEstimationService } from './carbon-estimation.service'; -import { CarbonEstimation, EstimatorValues } from '../types/carbon-estimator'; +import { CarbonEstimation, EstimatorValues, WorldLocation } from '../types/carbon-estimator'; import { LoggingService } from './logging.service'; import { sumValues } from '../utils/number-object'; import { version } from '../../../package.json'; +import { CarbonIntensityService } from './carbon-intensity.service'; +import { gCo2ePerKwh } from '../types/units'; const emptyEstimatorValues: EstimatorValues = { upstream: { @@ -48,14 +50,32 @@ function checkTotalPercentage(estimation: CarbonEstimation) { describe('CarbonEstimationService', () => { let service: CarbonEstimationService; let loggingService: jasmine.SpyObj; + let carbonIntensityService: jasmine.SpyObj; + const mockCarbonIntensities: Record = { + WORLD: 500, + GBR: 250, + EUROPE: 300, + 'NORTH AMERICA': 400, + ASIA: 600, + AFRICA: 550, + OCEANIA: 500, + 'LATIN AMERICA AND CARIBBEAN': 250, + }; beforeEach(() => { - const spy = jasmine.createSpyObj('LoggingService', ['log']); + const logSpy = jasmine.createSpyObj('LoggingService', ['log']); + const intensitySpy = jasmine.createSpyObj('CarbonIntensityService', ['getCarbonIntensity']); TestBed.configureTestingModule({ - providers: [CarbonEstimationService, { provide: LoggingService, useValue: spy }], + providers: [ + CarbonEstimationService, + { provide: LoggingService, useValue: logSpy }, + { provide: CarbonIntensityService, useValue: intensitySpy }, + ], }); service = TestBed.inject(CarbonEstimationService); loggingService = TestBed.inject(LoggingService) as jasmine.SpyObj; + carbonIntensityService = TestBed.inject(CarbonIntensityService) as jasmine.SpyObj; + carbonIntensityService.getCarbonIntensity.and.callFake(location => mockCarbonIntensities[location]); }); it('should be created', () => { @@ -94,6 +114,42 @@ describe('CarbonEstimationService', () => { expect(loggingService.log).toHaveBeenCalledWith(jasmine.stringMatching(/^Estimated Downstream Emissions: .*/)); }); + it('should use service to find relevant carbon intensities', () => { + const input: EstimatorValues = { + upstream: { + employeeLocation: 'GBR', + headCount: 0, + desktopPercentage: 0, + }, + onPremise: { + serverLocation: 'EUROPE', + estimateServerCount: false, + numberOfServers: 0, + }, + cloud: { + cloudLocation: 'NORTH AMERICA', + noCloudServices: false, + cloudPercentage: 0, + monthlyCloudBill: { + min: 0, + max: 0, + }, + }, + downstream: { + customerLocation: 'WORLD', + noDownstream: false, + monthlyActiveUsers: 0, + mobilePercentage: 0, + purposeOfSite: 'streaming', + }, + }; + service.calculateCarbonEstimation(input); + expect(carbonIntensityService.getCarbonIntensity).toHaveBeenCalledWith('GBR'); + expect(carbonIntensityService.getCarbonIntensity).toHaveBeenCalledWith('EUROPE'); + expect(carbonIntensityService.getCarbonIntensity).toHaveBeenCalledWith('NORTH AMERICA'); + expect(carbonIntensityService.getCarbonIntensity).toHaveBeenCalledWith('WORLD'); + }); + it('calculates emissions for hardware', () => { const hardwareInput: EstimatorValues = { ...emptyEstimatorValues, @@ -109,12 +165,12 @@ describe('CarbonEstimationService', () => { }, }; const result = service.calculateCarbonEstimation(hardwareInput); - expect(result.upstreamEmissions.user).withContext('upstreamEmissions.user').toBeCloseTo(3.48); - expect(result.upstreamEmissions.server).withContext('upstreamEmissions.server').toBeCloseTo(8.01); - expect(result.upstreamEmissions.network).withContext('upstreamEmissions.network').toBeCloseTo(3.59); + expect(result.upstreamEmissions.user).withContext('upstreamEmissions.user').toBeCloseTo(3.45); + expect(result.upstreamEmissions.server).withContext('upstreamEmissions.server').toBeCloseTo(7.93); + expect(result.upstreamEmissions.network).withContext('upstreamEmissions.network').toBeCloseTo(3.56); expect(result.directEmissions.user).withContext('directEmissions.user').toBeCloseTo(1.79); - expect(result.directEmissions.server).withContext('directEmissions.server').toBeCloseTo(60.45); - expect(result.directEmissions.network).withContext('directEmissions.network').toBeCloseTo(22.67); + expect(result.directEmissions.server).withContext('directEmissions.server').toBeCloseTo(60.56); + expect(result.directEmissions.network).withContext('directEmissions.network').toBeCloseTo(22.71); }); it('calculates emissions for hardware where servers are in different location to employees', () => { @@ -132,12 +188,12 @@ describe('CarbonEstimationService', () => { }, }; const result = service.calculateCarbonEstimation(hardwareInput); - expect(result.upstreamEmissions.user).withContext('upstreamEmissions.user').toBeCloseTo(3.72); - expect(result.upstreamEmissions.server).withContext('upstreamEmissions.server').toBeCloseTo(8.56); - expect(result.upstreamEmissions.network).withContext('upstreamEmissions.network').toBeCloseTo(3.84); - expect(result.directEmissions.user).withContext('directEmissions.user').toBeCloseTo(0.99); - expect(result.directEmissions.server).withContext('directEmissions.server').toBeCloseTo(64.54); - expect(result.directEmissions.network).withContext('directEmissions.network').toBeCloseTo(18.37); + expect(result.upstreamEmissions.user).withContext('upstreamEmissions.user').toBeCloseTo(3.69); + expect(result.upstreamEmissions.server).withContext('upstreamEmissions.server').toBeCloseTo(8.49); + expect(result.upstreamEmissions.network).withContext('upstreamEmissions.network').toBeCloseTo(3.81); + expect(result.directEmissions.user).withContext('directEmissions.user').toBeCloseTo(0.96); + expect(result.directEmissions.server).withContext('directEmissions.server').toBeCloseTo(64.83); + expect(result.directEmissions.network).withContext('directEmissions.network').toBeCloseTo(18.23); }); }); diff --git a/src/app/services/carbon-estimation.service.ts b/src/app/services/carbon-estimation.service.ts index d0fd1464..787fcd81 100644 --- a/src/app/services/carbon-estimation.service.ts +++ b/src/app/services/carbon-estimation.service.ts @@ -10,12 +10,16 @@ import { version } from '../../../package.json'; import { desktop, laptop, network, server } from '../estimation/device-type'; import { ON_PREMISE_AVERAGE_PUE } from '../estimation/constants'; import { DeviceUsage, createDeviceUsage } from '../estimation/device-usage'; +import { CarbonIntensityService } from './carbon-intensity.service'; @Injectable({ providedIn: 'root', }) export class CarbonEstimationService { - constructor(private loggingService: LoggingService) {} + constructor( + private carbonIntensityService: CarbonIntensityService, + private loggingService: LoggingService + ) {} calculateCarbonEstimation(formValue: EstimatorValues): CarbonEstimation { this.loggingService.log(`Input Values: ${formatObject(formValue)}`); @@ -26,9 +30,11 @@ export class CarbonEstimationService { this.loggingService.log(`Estimated Upstream Emissions: ${formatCarbonEstimate(upstreamEmissions)}`); const directEmissions = estimateDirectEmissions(deviceUsage); this.loggingService.log(`Estimated Direct Emissions: ${formatCarbonEstimate(directEmissions)}`); - const indirectEmissions = estimateIndirectEmissions(formValue.cloud); + const indirectIntensity = this.carbonIntensityService.getCarbonIntensity(formValue.cloud.cloudLocation); + const indirectEmissions = estimateIndirectEmissions(formValue.cloud, indirectIntensity); this.loggingService.log(`Estimated Indirect Emissions: ${formatCarbonEstimate(indirectEmissions)}`); - const downstreamEmissions = estimateDownstreamEmissions(formValue.downstream); + const downstreamIntensity = this.carbonIntensityService.getCarbonIntensity(formValue.downstream.customerLocation); + const downstreamEmissions = estimateDownstreamEmissions(formValue.downstream, downstreamIntensity); this.loggingService.log(`Estimated Downstream Emissions: ${formatCarbonEstimate(downstreamEmissions)}`); return toPercentages({ @@ -63,14 +69,14 @@ export class CarbonEstimationService { `Estimated Device Counts: ${formatObject({ desktopCount, laptopCount, serverCount, employeeNetworkCount, serverNetworkCount })}` ); - const employeeLocation = formValue.upstream.employeeLocation; - const onPremLocation = formValue.onPremise.serverLocation; + const employeeIntensity = this.carbonIntensityService.getCarbonIntensity(formValue.upstream.employeeLocation); + const onPremIntensity = this.carbonIntensityService.getCarbonIntensity(formValue.onPremise.serverLocation); return [ - createDeviceUsage(desktop, 'user', employeeLocation, desktopCount), - createDeviceUsage(laptop, 'user', employeeLocation, laptopCount), - createDeviceUsage(network, 'network', employeeLocation, employeeNetworkCount, ON_PREMISE_AVERAGE_PUE), - createDeviceUsage(server, 'server', onPremLocation, serverCount, ON_PREMISE_AVERAGE_PUE), - createDeviceUsage(network, 'network', onPremLocation, serverNetworkCount, ON_PREMISE_AVERAGE_PUE), + createDeviceUsage(desktop, 'user', employeeIntensity, desktopCount), + createDeviceUsage(laptop, 'user', employeeIntensity, laptopCount), + createDeviceUsage(network, 'network', employeeIntensity, employeeNetworkCount, ON_PREMISE_AVERAGE_PUE), + createDeviceUsage(server, 'server', onPremIntensity, serverCount, ON_PREMISE_AVERAGE_PUE), + createDeviceUsage(network, 'network', onPremIntensity, serverNetworkCount, ON_PREMISE_AVERAGE_PUE), ]; } } diff --git a/src/app/services/carbon-intensity.service.spec.ts b/src/app/services/carbon-intensity.service.spec.ts new file mode 100644 index 00000000..159cd2fc --- /dev/null +++ b/src/app/services/carbon-intensity.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CarbonIntensityService } from './carbon-intensity.service'; + +describe('CarbonIntensityService', () => { + let service: CarbonIntensityService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CarbonIntensityService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/carbon-intensity.service.ts b/src/app/services/carbon-intensity.service.ts new file mode 100644 index 00000000..6e3d91cd --- /dev/null +++ b/src/app/services/carbon-intensity.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core'; +import { gCo2ePerKwh } from '../types/units'; +import { WorldLocation } from '../types/carbon-estimator'; +import { averageIntensity } from '@tgwf/co2'; + +@Injectable({ + providedIn: 'root', +}) +export class CarbonIntensityService { + constructor() {} + + getCarbonIntensity(location: WorldLocation): gCo2ePerKwh { + return averageIntensity.data[location]; + } +} diff --git a/src/app/types/units.ts b/src/app/types/units.ts index 5839b074..08e2e508 100644 --- a/src/app/types/units.ts +++ b/src/app/types/units.ts @@ -3,4 +3,5 @@ export type Hour = number; export type Year = number; export type KilowattHour = number; export type KgCo2e = number; +export type gCo2ePerKwh = number; export type Gb = number; From 4027e5b5af14cbe1750f42d0f737bbcf0d2ae7b2 Mon Sep 17 00:00:00 2001 From: Matthew Griffin Date: Tue, 7 May 2024 15:12:05 +0100 Subject: [PATCH 5/7] Updated assumptions and limitations component to use the intensity service --- .../assumptions-and-limitation.component.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/app/assumptions-and-limitation/assumptions-and-limitation.component.ts b/src/app/assumptions-and-limitation/assumptions-and-limitation.component.ts index acde5421..a19a5530 100644 --- a/src/app/assumptions-and-limitation/assumptions-and-limitation.component.ts +++ b/src/app/assumptions-and-limitation/assumptions-and-limitation.component.ts @@ -3,7 +3,7 @@ import { CLOUD_AVERAGE_PUE, ON_PREMISE_AVERAGE_PUE } from '../estimation/constan import { siteTypeInfo } from '../estimation/estimate-downstream-emissions'; import { PurposeOfSite, WorldLocation, locationArray, purposeOfSiteArray } from '../types/carbon-estimator'; import { DecimalPipe } from '@angular/common'; -import { averageIntensity } from '@tgwf/co2'; +import { CarbonIntensityService } from '../services/carbon-intensity.service'; const purposeDescriptions: Record = { information: 'Information', @@ -39,13 +39,17 @@ export class AssumptionsAndLimitationComponent implements AfterContentInit { time: siteTypeInfo[purpose].averageMonthlyUserTime, data: siteTypeInfo[purpose].averageMonthlyUserData, })); - readonly locationCarbonInfo = locationArray.map(location => ({ - location: locationDescriptions[location], - carbonIntensity: averageIntensity.data[location], - })); + readonly locationCarbonInfo; @ViewChild('assumptionsLimitation', { static: true }) public assumptionsLimitation!: ElementRef; + constructor(private intensityService: CarbonIntensityService) { + this.locationCarbonInfo = locationArray.map(location => ({ + location: locationDescriptions[location], + carbonIntensity: this.intensityService.getCarbonIntensity(location), + })); + } + public ngAfterContentInit(): void { this.assumptionsLimitation.nativeElement.focus(); } From b2738d2719968afcbbefd833585536bbf26b9234 Mon Sep 17 00:00:00 2001 From: Matthew Griffin Date: Tue, 7 May 2024 15:33:59 +0100 Subject: [PATCH 6/7] Added CarbonIntensityService into the components documentation --- docs/components.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/components.md b/docs/components.md index b47d865f..a9478684 100644 --- a/docs/components.md +++ b/docs/components.md @@ -4,7 +4,7 @@ This page details the Angular components that are part of the application and ho ```mermaid flowchart TB - subgraph components + subgraph Components CarbonEstimator["`CarbonEstimatorComponent carbon-estimator`"] CarbonEstimatorForm["`CarbonEstimatorFormComponent @@ -20,10 +20,11 @@ flowchart TB ExpansionPanel["`ExpansionPanelComponent expansion-panel`"] end - subgraph services + subgraph Services CarbonEstimationService + CarbonIntensityService end - subgraph pipes + subgraph Pipes FormatCostRangePipe end @@ -32,6 +33,7 @@ flowchart TB CarbonEstimatorForm --> HelperInfo & Note CarbonEstimator & CarbonEstimatorForm ---> CarbonEstimationService CarbonEstimatorForm & CarbonEstimation --> ExpansionPanel + CarbonEstimationService & Assumptions --> CarbonIntensityService ``` ## CarbonEstimatorComponent @@ -52,7 +54,8 @@ Visualises the Carbon Estimation result. ## AssumptionsAndLimitationComponent -Provides information on the Assumptions and Limitations of the estimation. +Provides information on the Assumptions and Limitations of the estimation. +Uses the [CarbonIntensityService](services.md#carbonintensityservice) to get the latest carbon intensity figures to display. ## HelperInfoComponent From d17696612d73d39a5bf994b37ab115ee3bc735b0 Mon Sep 17 00:00:00 2001 From: Matthew Griffin Date: Wed, 8 May 2024 10:42:16 +0100 Subject: [PATCH 7/7] Add a link to CO2.js and reword link to Ember --- .../assumptions-and-limitation.component.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/assumptions-and-limitation/assumptions-and-limitation.component.html b/src/app/assumptions-and-limitation/assumptions-and-limitation.component.html index c0fe02e3..36532f95 100644 --- a/src/app/assumptions-and-limitation/assumptions-and-limitation.component.html +++ b/src/app/assumptions-and-limitation/assumptions-and-limitation.component.html @@ -94,9 +94,10 @@

Power consumption

Carbon Intensity

- We make use of the latest available Carbon Intensity figures from the - Ember Data Explorer via - the CO2.js library. We limit the range of regions that can be selected, which are currently: + We make use of the latest available Carbon Intensity figures from + Ember via the + CO2.js library. We limit the range + of regions that can be selected, which are currently:

World LocationCarbon Intensity (Kg CO2e/kWh)Carbon Intensity (g CO2e/kWh)
{{ item.location }}{{ item.carbonIntensity }}{{ item.carbonIntensity | number: '0.0-0' }}