/
demand_driven_deployment_inst.py
458 lines (424 loc) · 18.8 KB
/
demand_driven_deployment_inst.py
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
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
"""
This cyclus archetype uses time series methods to predict the demand and supply
for future time steps and manages the deployment of facilities to ensure
supply is greater than demand. Time series predicition methods can be used
in this archetype.
"""
import random
import copy
import math
from collections import defaultdict
import numpy as np
import scipy as sp
from cyclus.agents import Institution, Agent
from cyclus import lib
import cyclus.typesystem as ts
import d3ploy.solver as solver
import d3ploy.NO_solvers as no
import d3ploy.DO_solvers as do
import d3ploy.ML_solvers as ml
import d3ploy.deployment_inst as di
CALC_METHODS = {}
class DemandDrivenDeploymentInst(Institution):
"""
This institution deploys facilities based on demand curves using
time series methods.
"""
facility_commod = ts.MapStringString(
doc="A map of facilities and each of their corresponding" +
" output commodities",
tooltip="Map of facilities and output commodities in the " +
"institution",
alias=['facility_commod', 'facility', 'commod'],
uilabel="Facility and Commodities"
)
facility_capacity = ts.MapStringDouble(
doc="A map of facilities and each of their corresponding" +
" capacities",
tooltip="Map of facilities and capacities in the " +
"institution",
alias=['facility_capacity', 'facility', 'capacity'],
uilabel="Facility and Capacities"
)
facility_pref = ts.MapStringString(
doc="A map of facilities and each of their corresponding" +
" preferences",
tooltip="Map of facilities and preferences in the " +
"institution",
alias=['facility_pref', 'facility', 'pref'],
uilabel="Facility and Preferences",
default={}
)
facility_constraintcommod = ts.MapStringString(
doc="A map of facilities and each of their corresponding" +
" constraint commodity",
tooltip="Map of facilities and constraint commodities in the " +
"institution",
alias=['facility_constraintcommod', 'facility', 'constraintcommod'],
uilabel="Facility and Constraint Commodities",
default={}
)
facility_constraintval = ts.MapStringDouble(
doc="A map of facilities and each of their corresponding" +
" constraint values",
tooltip="Map of facilities and constraint values in the " +
"institution",
alias=['facility_constraintval', 'facility', 'constraintval'],
uilabel="Facility and Constraint Commodity Values",
default={}
)
facility_sharing = ts.MapStringDouble(
doc="A map of facilities that share a commodity",
tooltip="Map of facilities and percentages of sharing",
alias=['facility_sharing', 'facility', 'percentage'],
uilabel="Facility and Percentages",
default={}
)
demand_eq = ts.String(
doc="This is the string for the demand equation of the driving commodity. " +
"The equation should use `t' as the dependent variable",
tooltip="Demand equation for driving commodity",
uilabel="Demand Equation")
calc_method = ts.String(
doc="This is the calculated method used to determine the supply and demand " +
"for the commodities of this institution. Currently this can be ma for " +
"moving average, or arma for autoregressive moving average.",
tooltip="Calculation method used to predict supply/demand",
uilabel="Calculation Method")
record = ts.Bool(
doc="Indicates whether or not the institution should record it's output to text " +
"file outputs. The output files match the name of the demand commodity of the " +
"institution.",
tooltip="Boolean to indicate whether or not to record output to text file.",
uilabel="Record to Text",
default=False)
driving_commod = ts.String(
doc="Sets the driving commodity for the institution. That is the " +
"commodity that no_inst will deploy against the demand equation.",
tooltip="Driving Commodity",
uilabel="Driving Commodity",
default="POWER"
)
installed_cap = ts.Bool(
doc="True if facility deployment is governed by installed capacity. " +
"False if deployment is governed by actual commodity supply",
tooltip="Boolean to indicate whether or not to use installed" +
"capacity as supply",
uilabel="installed cap",
default=False)
steps = ts.Int(
doc="The number of timesteps forward to predict supply and demand",
tooltip="The number of predicted steps forward",
uilabel="Timesteps for Prediction",
default=1
)
back_steps = ts.Int(
doc="This is the number of steps backwards from the current time step" +
"that will be used to make the prediction. If this is set to '0'" +
"then the calculation will use all values in the time series.",
tooltip="",
uilabel="Back Steps",
default=5)
supply_std_dev = ts.Double(
doc="The standard deviation adjustment for the supple side.",
tooltip="The standard deviation adjustment for the supple side.",
uilabel="Supply Std Dev",
default=0
)
buffer_type = ts.MapStringString(
doc="Indicates whether the buffer is a relative or absolute value," +
"rel: % value, abs: double value, for each commodity",
tooltip="Supply buffer as a relative or absolute value for," +
"each commodity",
alias=[
'buffer_type',
'commod',
'type'],
uilabel="Supply Buffer type",
default={})
supply_buffer = ts.MapStringDouble(
doc="Supply buffer size: relative or absolute value ",
tooltip="Supply buffer Amount.",
alias=['supply_buffer', 'commod', 'buffer'],
uilabel="Supply Buffer",
default={}
)
degree = ts.Int(
doc="The degree of the fitting polynomial.",
tooltip="The degree of the fitting polynomial, if using calc methods" +
" poly, fft, holtz-winter and exponential smoothing." +
" Additionally, degree is used to as the 'period' input to " +
"the stepwise_seasonal method.",
uilabel="Degree Polynomial Fit / Period for stepwise_seasonal",
default=1
)
os_time = ts.Int(
doc="The number of oversupply timesteps before decommission",
tooltip="",
uilabel="Oversupply Time Limit",
default=120
)
os_int = ts.Int(
doc="The number of facilities over capacity " +
"for a given commodity that is allowed. i.e If this" +
" value is 1. One facility capacity over demand is considered" +
" an oversupplied situtation.",
tooltip="",
uilabel="Oversupply Fac Limit",
default=1
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.commodity_supply = {}
self.commodity_demand = {}
self.installed_capacity = {}
self.fac_commod = {}
self.commod_os = {}
self.fresh = True
CALC_METHODS['ma'] = no.predict_ma
CALC_METHODS['arma'] = no.predict_arma
CALC_METHODS['arch'] = no.predict_arch
CALC_METHODS['poly'] = do.polyfit_regression
CALC_METHODS['exp_smoothing'] = do.exp_smoothing
CALC_METHODS['holt_winters'] = do.holt_winters
CALC_METHODS['fft'] = do.fft
CALC_METHODS['sw_seasonal'] = ml.stepwise_seasonal
def print_variables(self):
print('commodities: %s' % self.commodity_dict)
print('demand_eq: %s' % self.demand_eq)
print('calc_method: %s' % self.calc_method)
print('record: %s' % str(self.record))
print('steps: %i' % self.steps)
print('back_steps: %i' % self.back_steps)
print('supply_std_dev: %f' % self.supply_std_dev)
def enter_notify(self):
super().enter_notify()
if self.fresh:
# convert input into dictionary
self.commodity_dict = di.build_dict(
self.facility_commod,
self.facility_capacity,
self.facility_pref,
self.facility_constraintcommod,
self.facility_constraintval,
self.facility_sharing)
for commod, proto_dict in self.commodity_dict.items():
self.commod_os[commod] = 0
protos = proto_dict.keys()
for proto in protos:
self.fac_commod[proto] = commod
self.commod_list = list(self.commodity_dict.keys())
for commod in self.commod_list:
self.installed_capacity[commod] = defaultdict(float)
self.installed_capacity[commod][0] = 0.
for commod, commod_dict in self.commodity_dict.items():
for proto, proto_dict in commod_dict.items():
if proto_dict['constraint_commod'] != '0':
self.commod_list.append(
proto_dict['constraint_commod'])
for commod, commod_dict in self.commodity_dict.items():
tot = 0
for proto, proto_dict in commod_dict.items():
tot += proto_dict['share']
if tot != 0 and tot != 100:
print("Share preferences do not add to 100")
raise Exception()
self.buffer_dict = di.build_buffer_dict(self.supply_buffer,
self.commod_list)
self.buffer_type_dict = di.build_buffer_type_dict(
self.buffer_type, self.commod_list)
for commod in self.commod_list:
lib.TIME_SERIES_LISTENERS["supply" +
commod].append(self.extract_supply)
lib.TIME_SERIES_LISTENERS["demand" +
commod].append(self.extract_demand)
self.commodity_supply[commod] = defaultdict(float)
self.commodity_demand[commod] = defaultdict(float)
self.commod_mins = solver.find_mins(self.commodity_dict)
for child in self.children:
if child.prototype not in self.fac_commod:
continue
itscommod = self.fac_commod[child.prototype]
self.installed_capacity[itscommod][0] += \
self.commodity_dict[itscommod][child.prototype]['cap']
self.fresh = False
def decision(self):
"""
This is the tock method for decision the institution. Here the
institution determines the difference in supply and demand and
makes the the decision to deploy facilities or not.
"""
time = self.context.time
for commod, proto_dict in self.commodity_dict.items():
diff, supply, demand = self.calc_diff(commod, time)
lib.record_time_series('calc_supply' + commod, self, supply)
lib.record_time_series('calc_demand' + commod, self, demand)
if diff < 0:
if self.installed_cap:
deploy_dict, self.commodity_dict = solver.deploy_solver(
self.installed_capacity, self.commodity_dict, commod, diff, time)
else:
deploy_dict, self.commodity_dict = solver.deploy_solver(
self.commodity_supply, self.commodity_dict, commod, diff, time)
for proto, num in deploy_dict.items():
for i in range(num):
self.context.schedule_build(self, proto)
# update installed capacity dict
self.installed_capacity[commod][time + 1] = \
self.installed_capacity[commod][time]
for proto, num in deploy_dict.items():
self.installed_capacity[commod][time + 1] += \
self.commodity_dict[commod][proto]['cap'] * num
else:
self.installed_capacity[commod][time +
1] = self.installed_capacity[commod][time]
os_limit = self.commod_mins[commod] * self.os_int
if diff > os_limit:
self.commod_os[commod] += 1
else:
self.commod_os[commod] = 0
if diff > os_limit and self.commod_os[commod] > self.os_time:
solver.decommission_oldest(self, self.commodity_dict[commod], diff, commod, time)
if self.record:
out_text = "Time " + str(time) + \
" Deployed " + str(len(self.children))
out_text += " supply " + \
str(self.commodity_supply[commod][time])
out_text += " demand " + \
str(self.commodity_demand[commod][time]) + "\n"
with open(commod + ".txt", 'a') as f:
f.write(out_text)
for child in self.children:
if child.exit_time == time:
itscommod = self.fac_commod[child.prototype]
self.installed_capacity[itscommod][time + 1] -= \
self.commodity_dict[itscommod][child.prototype]['cap']
def calc_diff(self, commod, time):
"""
This function calculates the different in supply and demand for a given facility
Parameters
----------
time : int
This is the time step that the difference is being calculated for.
Returns
-------
diff : double
This is the difference between supply and demand at [time]
supply : double
The calculated supply of the supply commodity at [time].
demand : double
The calculated demand of the demand commodity at [time]
"""
if time not in self.commodity_demand[commod]:
if commod == self.driving_commod:
t = time
self.commodity_demand[commod][time] = eval(self.demand_eq)
else:
self.commodity_demand[commod][time] = 0.0
if time not in self.commodity_supply[commod]:
self.commodity_supply[commod][time] = 0.0
supply = self.predict_supply(commod)
if self.buffer_type_dict[commod] == 'rel':
demand = self.predict_demand(
commod, time) * (1 + self.buffer_dict[commod])
elif self.buffer_type_dict[commod] == 'abs':
demand = self.predict_demand(
commod, time) + self.buffer_dict[commod]
else:
raise Exception(
'You can only choose rel or abs types for buffer type')
diff = supply - demand
return diff, supply, demand
def predict_supply(self, commod):
def target(incommod):
if self.installed_cap:
return self.installed_capacity[incommod]
else:
return self.commodity_supply[incommod]
if self.calc_method in ['arma', 'ma', 'arch']:
supply = CALC_METHODS[self.calc_method](target(commod),
steps=self.steps,
std_dev=self.supply_std_dev,
back_steps=self.back_steps)
elif self.calc_method in ['poly', 'exp_smoothing', 'holt_winters', 'fft']:
supply = CALC_METHODS[self.calc_method](target(commod),
back_steps=self.back_steps,
degree=self.degree,
steps=self.steps)
elif self.calc_method in ['sw_seasonal']:
supply = CALC_METHODS[self.calc_method](
target(commod), period=self.degree)
else:
raise ValueError(
'The input calc_method is not valid. Check again.')
return supply
def predict_demand(self, commod, time):
if commod == self.driving_commod:
demand = self.demand_calc(time + 1)
self.commodity_demand[commod][time + 1] = demand
else:
if self.calc_method in ['arma', 'ma', 'arch']:
demand = CALC_METHODS[self.calc_method](self.commodity_demand[commod],
steps=self.steps,
std_dev=self.supply_std_dev,
back_steps=self.back_steps)
elif self.calc_method in ['poly', 'exp_smoothing', 'holt_winters', 'fft']:
demand = CALC_METHODS[self.calc_method](self.commodity_demand[commod],
back_steps=self.back_steps,
degree=self.degree,
steps=self.steps)
elif self.calc_method in ['sw_seasonal']:
demand = CALC_METHODS[self.calc_method](
self.commodity_demand[commod], period=self.degree)
else:
raise ValueError(
'The input calc_method is not valid. Check again.')
return demand
def extract_supply(self, agent, time, value, commod):
"""
Gather information on the available supply of a commodity over the
lifetime of the simulation.
Parameters
----------
agent : cyclus agent
This is the agent that is making the call to the listener.
time : int
Timestep that the call is made.
value : object
This is the value of the object being recorded in the time
series.
"""
commod = commod[6:]
self.commodity_supply[commod][time] += value
# update commodities
# self.commodity_dict[commod] = {agent.prototype: value}
def extract_demand(self, agent, time, value, commod):
"""
Gather information on the demand of a commodity over the
lifetime of the simulation.
Parameters
----------
agent : cyclus agent
This is the agent that is making the call to the listener.
time : int
Timestep that the call is made.
value : object
This is the value of the object being recorded in the time
series.
"""
commod = commod[6:]
self.commodity_demand[commod][time] += value
def demand_calc(self, time):
"""
Calculate the electrical demand at a given timestep (time).
Parameters
----------
time : int
The timestep that the demand will be calculated at.
Returns
-------
demand : The calculated demand at a given timestep.
"""
t = time
demand = eval(self.demand_eq)
return demand