-
Notifications
You must be signed in to change notification settings - Fork 402
/
service.go
403 lines (318 loc) · 13 KB
/
service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
// Copyright (C) 2024 Storj Labs, Inc.
// See LICENSE for copying information.
package emission
import (
"math"
"time"
"github.com/zeebo/errs"
)
// Error describes internal emission service error.
var Error = errs.Class("emission service")
const (
decimalMultiplier = 1000
tbToBytesMultiplier = 1e-12
gbToBytesMultiplier = 1e-9
dayHours = 24
yearDays = 365.25
twelveMonths = 12
)
const (
hyperscaler = 0
corporateDC = 1
storjStandard = 2
storjReused = 3
storjNew = 4
modalityCount = 5
)
var (
unitless = Unit{}
byteUnit = Unit{byte: 1}
hour = Unit{hour: 1}
kilogram = Unit{kilogram: 1}
wattHourPerByte = Unit{watt: 1, hour: 1, byte: -1}
kilogramPerWattHour = Unit{kilogram: 1, watt: -1, hour: -1}
kilogramPerByte = Unit{kilogram: 1, byte: -1}
kilogramPerByteHour = Unit{kilogram: 1, byte: -1, hour: -1}
)
// Service is an emission service.
// Performs emissions impact calculations.
//
// architecture: Service
type Service struct {
config Config
}
// NewService creates a new Service with the given configuration.
func NewService(config Config) *Service {
return &Service{config: config}
}
// Impact represents emission impact from different sources.
type Impact struct {
EstimatedKgCO2eStorj float64
EstimatedKgCO2eHyperscaler float64
EstimatedKgCO2eCorporateDC float64
EstimatedFractionSavingsAgainstHyperscaler float64
EstimatedFractionSavingsAgainstCorporateDC float64
}
// Row holds data row of predefined number of values.
type Row [modalityCount]Val
// CalculationInput holds input data needed to perform emission impact calculations.
type CalculationInput struct {
AmountOfDataInTB float64 // The amount of data in terabytes or terabyte-duration.
Duration time.Duration // The Duration over which the data is measured.
IsTBDuration bool // true if AmountOfDataInTB is in terabytes-duration, false if in terabytes.
}
// CalculateImpact calculates emission impact coming from different sources e.g. Storj, hyperscaler or corporateDC.
func (sv *Service) CalculateImpact(input *CalculationInput) (*Impact, error) {
// Define a data row of services expansion factors.
expansionFactor := sv.prepareExpansionFactorRow()
// Define a data row of services region count.
regionCount := sv.prepareRegionCountRow()
// Define a data row of services network weighting.
networkWeighting, err := sv.prepareNetworkWeightingRow()
if err != nil {
return nil, Error.Wrap(err)
}
// Define a data row of services utilization fractions.
modalityUtilization := sv.prepareUtilizationRow()
// Define a data row of services hard drive life period.
driveLifetime := sv.prepareDriveLifetimeRow()
// Define a data row of services hard drive embodied carbon emission.
driveEmbodiedCarbon := sv.prepareDriveEmbodiedCarbonEmissionRow()
// Define a data row of services hard drive amortized embodied carbon emission.
amortizedEmbodiedCarbon := prepareDriveAmortizedEmbodiedCarbonEmissionRow(driveEmbodiedCarbon, driveLifetime)
// Define a data row of services carbon emission from powering hard drives.
carbonFromPower := sv.prepareCarbonFromDrivePoweringRow()
timeStored := hour.Value(input.Duration.Seconds() / 3600)
// Define a data row of services carbon emission from write and repair actions.
carbonFromWritesAndRepairs, err := sv.prepareCarbonFromWritesAndRepairsRow(timeStored)
if err != nil {
return nil, Error.Wrap(err)
}
// Define a data row of services carbon emission from metadata overhead.
carbonPerByteMetadataOverhead, err := sv.prepareCarbonPerByteMetadataOverheadRow()
if err != nil {
return nil, Error.Wrap(err)
}
// Define a data row of services total carbon emission per byte of data.
carbonTotalPerByte, err := sumRows(amortizedEmbodiedCarbon, carbonFromPower, carbonFromWritesAndRepairs, carbonPerByteMetadataOverhead)
if err != nil {
return nil, Error.Wrap(err)
}
// Define a data row of services effective carbon emission per byte of data.
effectiveCarbonPerByte := prepareEffectiveCarbonPerByteRow(carbonTotalPerByte, modalityUtilization)
// Define a data row of services total carbon emission.
totalCarbon := prepareTotalCarbonRow(input, effectiveCarbonPerByte, expansionFactor, regionCount, timeStored)
// Calculate Storj blended value.
storjBlended, err := calculateStorjBlended(networkWeighting, totalCarbon)
if err != nil {
return nil, Error.Wrap(err)
}
// Calculate emission impact per service.
oneKilogram := kilogram.Value(1)
rv := &Impact{
EstimatedKgCO2eStorj: storjBlended.Div(oneKilogram).Value,
EstimatedKgCO2eHyperscaler: totalCarbon[hyperscaler].Div(oneKilogram).Value,
EstimatedKgCO2eCorporateDC: totalCarbon[corporateDC].Div(oneKilogram).Value,
}
if rv.EstimatedKgCO2eHyperscaler != 0 {
estimatedFractionSavingsAgainstHyperscaler, err := unitless.Value(1).Sub(unitless.Value(rv.EstimatedKgCO2eStorj / rv.EstimatedKgCO2eHyperscaler))
if err != nil {
return nil, Error.Wrap(err)
}
rv.EstimatedFractionSavingsAgainstHyperscaler = estimatedFractionSavingsAgainstHyperscaler.Value
}
if rv.EstimatedKgCO2eCorporateDC != 0 {
estimatedFractionSavingsAgainstCorporateDC, err := unitless.Value(1).Sub(unitless.Value(rv.EstimatedKgCO2eStorj / rv.EstimatedKgCO2eCorporateDC))
if err != nil {
return nil, Error.Wrap(err)
}
rv.EstimatedFractionSavingsAgainstCorporateDC = estimatedFractionSavingsAgainstCorporateDC.Value
}
return rv, nil
}
// CalculateSavedTrees calculates saved trees count based on emission impact.
func (sv *Service) CalculateSavedTrees(impact float64) int64 {
return int64(math.Round(impact / sv.config.AverageCO2SequesteredByTree))
}
func (sv *Service) prepareExpansionFactorRow() *Row {
storjExpansionFactor := unitless.Value(sv.config.StorjExpansionFactor)
row := new(Row)
row[hyperscaler] = unitless.Value(sv.config.HyperscalerExpansionFactor)
row[corporateDC] = unitless.Value(sv.config.CorporateDCExpansionFactor)
row[storjStandard] = storjExpansionFactor
row[storjReused] = storjExpansionFactor
row[storjNew] = storjExpansionFactor
return row
}
func (sv *Service) prepareRegionCountRow() *Row {
storjRegionCount := unitless.Value(sv.config.StorjRegionCount)
row := new(Row)
row[hyperscaler] = unitless.Value(sv.config.HyperscalerRegionCount)
row[corporateDC] = unitless.Value(sv.config.CorporateDCRegionCount)
row[storjStandard] = storjRegionCount
row[storjReused] = storjRegionCount
row[storjNew] = storjRegionCount
return row
}
func (sv *Service) prepareNetworkWeightingRow() (*Row, error) {
row := new(Row)
row[storjStandard] = unitless.Value(sv.config.StorjStandardNetworkWeighting)
row[storjNew] = unitless.Value(sv.config.StorjNewNetworkWeighting)
storjNotNewNodesFraction, err := unitless.Value(1).Sub(row[storjNew])
if err != nil {
return nil, err
}
storjReusedVal, err := storjNotNewNodesFraction.Sub(row[storjStandard])
if err != nil {
return nil, err
}
row[storjReused] = storjReusedVal
return row, nil
}
func (sv *Service) prepareUtilizationRow() *Row {
storjUtilizationFraction := unitless.Value(sv.config.StorjUtilizationFraction)
row := new(Row)
row[hyperscaler] = unitless.Value(sv.config.HyperscalerUtilizationFraction)
row[corporateDC] = unitless.Value(sv.config.CorporateDCUtilizationFraction)
row[storjStandard] = storjUtilizationFraction
row[storjReused] = storjUtilizationFraction
row[storjNew] = storjUtilizationFraction
return row
}
func (sv *Service) prepareDriveLifetimeRow() *Row {
standardDriveLife := yearsToHours(sv.config.StandardDriveLife)
shortenedDriveLife := yearsToHours(sv.config.ShortenedDriveLife)
extendedDriveLife := yearsToHours(sv.config.ExtendedDriveLife)
row := new(Row)
row[hyperscaler] = standardDriveLife
row[corporateDC] = standardDriveLife
row[storjStandard] = extendedDriveLife
row[storjReused] = shortenedDriveLife
row[storjNew] = extendedDriveLife
return row
}
func (sv *Service) prepareDriveEmbodiedCarbonEmissionRow() *Row {
newDriveEmbodiedCarbon := kilogramPerByte.Value(sv.config.NewDriveEmbodiedCarbon * tbToBytesMultiplier)
noEmbodiedCarbon := kilogramPerByte.Value(0)
row := new(Row)
row[hyperscaler] = newDriveEmbodiedCarbon
row[corporateDC] = newDriveEmbodiedCarbon
row[storjStandard] = noEmbodiedCarbon
row[storjReused] = noEmbodiedCarbon
row[storjNew] = newDriveEmbodiedCarbon
return row
}
func prepareDriveAmortizedEmbodiedCarbonEmissionRow(driveCarbonRow, driveLifetimeRow *Row) *Row {
row := new(Row)
for modality := 0; modality < modalityCount; modality++ {
row[modality] = driveCarbonRow[modality].Div(driveLifetimeRow[modality])
}
return row
}
func (sv *Service) prepareCarbonFromDrivePoweringRow() *Row {
carbonFromDrivePowering := kilogramPerByteHour.Value(sv.config.CarbonFromDrivePowering * tbToBytesMultiplier / dayHours / yearDays)
noCarbonFromDrivePowering := kilogramPerByteHour.Value(0)
row := new(Row)
row[hyperscaler] = carbonFromDrivePowering
row[corporateDC] = carbonFromDrivePowering
row[storjStandard] = noCarbonFromDrivePowering
row[storjReused] = carbonFromDrivePowering
row[storjNew] = carbonFromDrivePowering
return row
}
func (sv *Service) prepareCarbonFromWritesAndRepairsRow(timeStored Val) (*Row, error) {
writeEnergy := wattHourPerByte.Value(sv.config.WriteEnergy * gbToBytesMultiplier)
CO2PerEnergy := kilogramPerWattHour.Value(sv.config.CO2PerEnergy / decimalMultiplier)
noCarbonFromWritesAndRepairs := kilogramPerByteHour.Value(0)
// this raises 1+monthlyFractionOfDataRepaired to power of 12
// TODO(jt): should we be doing this?
dataRepaired := byteUnit.Value(sv.config.RepairedData / tbToBytesMultiplier)
expandedData := byteUnit.Value(sv.config.ExpandedData / tbToBytesMultiplier)
monthlyFractionOfDataRepaired := dataRepaired.Div(expandedData)
repairFactor := unitless.Value(1)
for i := 0; i < twelveMonths; i++ {
monthlyFraction, err := unitless.Value(1).Add(monthlyFractionOfDataRepaired)
if err != nil {
return nil, err
}
repairFactor = repairFactor.Mul(monthlyFraction)
}
row := new(Row)
row[hyperscaler] = noCarbonFromWritesAndRepairs
row[corporateDC] = noCarbonFromWritesAndRepairs
for modality := storjStandard; modality < modalityCount; modality++ {
row[modality] = writeEnergy.Mul(CO2PerEnergy).Mul(repairFactor).Div(timeStored)
}
return row, nil
}
func (sv *Service) prepareCarbonPerByteMetadataOverheadRow() (*Row, error) {
noCarbonPerByteMetadataOverhead := kilogramPerByteHour.Value(0)
row := new(Row)
row[hyperscaler] = noCarbonPerByteMetadataOverhead
row[corporateDC] = noCarbonPerByteMetadataOverhead
storjGCPCarbon := kilogram.Value(sv.config.StorjGCPCarbon)
storjCRDBCarbon := kilogram.Value(sv.config.StorjCRDBCarbon)
monthlyStorjGCPAndCRDBCarbon, err := storjGCPCarbon.Add(storjCRDBCarbon)
if err != nil {
return nil, err
}
storjEdgeCarbon := kilogram.Value(sv.config.StorjEdgeCarbon)
monthlyStorjGCPAndCRDBAndEdgeCarbon, err := monthlyStorjGCPAndCRDBCarbon.Add(storjEdgeCarbon)
if err != nil {
return nil, err
}
storjAnnualCarbon := monthlyStorjGCPAndCRDBAndEdgeCarbon.Mul(unitless.Value(twelveMonths))
storjExpandedNetworkStorage := byteUnit.Value(sv.config.StorjExpandedNetworkStorage / tbToBytesMultiplier)
carbonOverheadPerByte := storjAnnualCarbon.Div(storjExpandedNetworkStorage.Mul(yearsToHours(1)))
for modality := storjStandard; modality < modalityCount; modality++ {
row[modality] = carbonOverheadPerByte
}
return row, nil
}
func yearsToHours(v float64) Val {
return hour.Value(v * yearDays * dayHours)
}
func prepareEffectiveCarbonPerByteRow(carbonTotalPerByteRow, utilizationRow *Row) *Row {
row := new(Row)
for modality := 0; modality < modalityCount; modality++ {
row[modality] = carbonTotalPerByteRow[modality].Div(utilizationRow[modality])
}
return row
}
func prepareTotalCarbonRow(input *CalculationInput, effectiveCarbonPerByteRow, expansionFactorRow, regionCountRow *Row, timeStored Val) *Row {
amountOfData := byteUnit.Value(input.AmountOfDataInTB / tbToBytesMultiplier)
// We don't include timeStored amount value if data type is already TB-duration.
if input.IsTBDuration {
timeStored = hour.Value(1)
}
row := new(Row)
for modality := 0; modality < modalityCount; modality++ {
row[modality] = effectiveCarbonPerByteRow[modality].Mul(amountOfData).Mul(timeStored).Mul(expansionFactorRow[modality]).Mul(regionCountRow[modality])
}
return row
}
func calculateStorjBlended(networkWeightingRow, totalCarbonRow *Row) (Val, error) {
storjReusedTotalCarbon := networkWeightingRow[storjReused].Mul(totalCarbonRow[storjReused])
storjNewAndReusedTotalCarbon, err := networkWeightingRow[storjNew].Mul(totalCarbonRow[storjNew]).Add(storjReusedTotalCarbon)
if err != nil {
return Val{}, err
}
storjBlended, err := networkWeightingRow[storjStandard].Mul(totalCarbonRow[storjStandard]).Add(storjNewAndReusedTotalCarbon)
if err != nil {
return Val{}, err
}
return storjBlended, nil
}
func sumRows(v ...*Row) (*Row, error) {
rv := v[0]
for _, l := range v[1:] {
for i := 0; i < len(l); i++ {
newVal, err := rv[i].Add(l[i])
if err != nil {
return nil, err
}
rv[i] = newVal
}
}
return rv, nil
}