aiphoria 0.0.1__py3-none-any.whl → 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. aiphoria/__init__.py +59 -0
  2. aiphoria/core/__init__.py +55 -0
  3. aiphoria/core/builder.py +305 -0
  4. aiphoria/core/datachecker.py +1808 -0
  5. aiphoria/core/dataprovider.py +806 -0
  6. aiphoria/core/datastructures.py +1686 -0
  7. aiphoria/core/datavisualizer.py +431 -0
  8. aiphoria/core/datavisualizer_data/LICENSE +21 -0
  9. aiphoria/core/datavisualizer_data/datavisualizer_plotly.html +5561 -0
  10. aiphoria/core/datavisualizer_data/pako.min.js +2 -0
  11. aiphoria/core/datavisualizer_data/plotly-3.0.0.min.js +3879 -0
  12. aiphoria/core/flowmodifiersolver.py +1754 -0
  13. aiphoria/core/flowsolver.py +1472 -0
  14. aiphoria/core/logger.py +113 -0
  15. aiphoria/core/network_graph.py +136 -0
  16. aiphoria/core/network_graph_data/ECHARTS_LICENSE +202 -0
  17. aiphoria/core/network_graph_data/echarts_min.js +45 -0
  18. aiphoria/core/network_graph_data/network_graph.html +76 -0
  19. aiphoria/core/network_graph_data/network_graph.js +1391 -0
  20. aiphoria/core/parameters.py +269 -0
  21. aiphoria/core/types.py +20 -0
  22. aiphoria/core/utils.py +362 -0
  23. aiphoria/core/visualizer_parameters.py +7 -0
  24. aiphoria/data/example_scenario.xlsx +0 -0
  25. aiphoria/example.py +66 -0
  26. aiphoria/lib/docs/dynamic_stock.py +124 -0
  27. aiphoria/lib/odym/modules/ODYM_Classes.py +362 -0
  28. aiphoria/lib/odym/modules/ODYM_Functions.py +1299 -0
  29. aiphoria/lib/odym/modules/__init__.py +1 -0
  30. aiphoria/lib/odym/modules/dynamic_stock_model.py +808 -0
  31. aiphoria/lib/odym/modules/test/DSM_test_known_results.py +762 -0
  32. aiphoria/lib/odym/modules/test/ODYM_Classes_test_known_results.py +107 -0
  33. aiphoria/lib/odym/modules/test/ODYM_Functions_test_known_results.py +136 -0
  34. aiphoria/lib/odym/modules/test/__init__.py +2 -0
  35. aiphoria/runner.py +678 -0
  36. aiphoria-0.8.0.dist-info/METADATA +119 -0
  37. aiphoria-0.8.0.dist-info/RECORD +40 -0
  38. {aiphoria-0.0.1.dist-info → aiphoria-0.8.0.dist-info}/WHEEL +1 -1
  39. aiphoria-0.8.0.dist-info/licenses/LICENSE +21 -0
  40. aiphoria-0.0.1.dist-info/METADATA +0 -5
  41. aiphoria-0.0.1.dist-info/RECORD +0 -5
  42. {aiphoria-0.0.1.dist-info → aiphoria-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,808 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Class DynamicStockModel
4
+ Check https://github.com/IndEcol/ODYM for latest version.
5
+
6
+ Methods for efficient handling of dynamic stock models (DSMs)
7
+
8
+ Created on Mon Jun 30 17:21:28 2014
9
+
10
+ @author: Stefan Pauliuk, NTNU Trondheim, Norway, later Uni Freiburg, Germany
11
+ with contributions from
12
+ Sebastiaan Deetman, CML, Leiden, NL
13
+ Tomer Fishman, IDC Herzliya, IL
14
+ Chris Mutel, PSI, Villingen, CH
15
+
16
+ standard abbreviation: DSM or dsm
17
+
18
+ dependencies:
19
+ numpy >= 1.9
20
+ scipy >= 0.14
21
+
22
+ Repository for this class, documentation, and tutorials: https://github.com/IndEcol/ODYM
23
+
24
+ """
25
+
26
+ import numpy as np
27
+ import scipy.stats
28
+
29
+ def __version__():
30
+ """Return a brief version string and statement for this class."""
31
+ return str('1.0'), str('Class DynamicStockModel, dsm. Version 1.0. Last change: July 25th, 2019. Check https://github.com/IndEcol/ODYM for latest version.')
32
+
33
+
34
+ class DynamicStockModel(object):
35
+
36
+ """ Class containing a dynamic stock model
37
+
38
+ Attributes
39
+ ----------
40
+ t : Series of years or other time intervals
41
+ i : Discrete time series of inflow to stock
42
+
43
+ o : Discrete time series of outflow from stock
44
+ o_c :Discrete time series of outflow from stock, by cohort
45
+
46
+ s_c : dynamic stock model (stock broken down by year and age- cohort)
47
+ s : Discrete time series for stock, total
48
+
49
+ lt : lifetime distribution: dictionary
50
+
51
+ pdf: probability density function, distribution of outflow from a specific age-cohort
52
+
53
+ sf: survival function for different age-cohorts, year x age-cohort table
54
+
55
+
56
+ name : string, optional
57
+ Name of the dynamic stock model, default is 'DSM'
58
+ """
59
+
60
+ """
61
+ Basic initialisation and dimension check methods
62
+ """
63
+
64
+ def __init__(self, t=None, i=None, o=None, s=None, lt=None, s_c=None, o_c=None, name='DSM', pdf=None, sf=None):
65
+ """ Init function. Assign the input data to the instance of the object."""
66
+ self.t = t # optional
67
+
68
+ self.i = i # optional
69
+
70
+ self.s = s # optional
71
+ self.s_c = s_c # optional
72
+
73
+ self.o = o # optional
74
+ self.o_c = o_c # optional
75
+
76
+ if lt is not None:
77
+ for ThisKey in lt.keys():
78
+ # If we have the same scalar lifetime, stdDev, etc., for all cohorts,
79
+ # replicate this value to full length of the time vector
80
+ if ThisKey != 'Type':
81
+ if np.array(lt[ThisKey]).shape[0] == 1:
82
+ lt[ThisKey] = np.tile(lt[ThisKey], len(t))
83
+
84
+ self.lt = lt # optional
85
+ self.name = name # optional
86
+
87
+ self.pdf = pdf # optional
88
+ self.sf = sf # optional
89
+
90
+ """ Part 1: Checks and balances: """
91
+
92
+ def dimension_check(self):
93
+ """ This method checks which variables are present and checks whether data types and dimensions match
94
+ """
95
+ # Compile a little report on the presence and dimensions of the elements in the SUT
96
+ try:
97
+ DimReport = str('<br><b> Checking dimensions of dynamic stock model ' + self.name + '.')
98
+ if self.t is not None:
99
+ DimReport += str('Time vector is present with ' + str(len(self.t)) + ' years.<br>')
100
+ else:
101
+ DimReport += str('Time vector is not present.<br>')
102
+ if self.i is not None:
103
+ DimReport += str('Inflow vector is present with ' +
104
+ str(len(self.i)) + ' years.<br>')
105
+ else:
106
+ DimReport += str('Inflow is not present.<br>')
107
+ if self.s is not None:
108
+ DimReport += str('Total stock is present with ' + str(len(self.s)) + ' years.<br>')
109
+ else:
110
+ DimReport += str('Total stock is not present.<br>')
111
+ if self.s_c is not None:
112
+ DimReport += str('Stock by cohorts is present with ' + str(len(self.s_c)
113
+ ) + ' years and ' + str(len(self.s_c[0])) + ' cohorts.<br>')
114
+ else:
115
+ DimReport += str('Stock by cohorts is not present.<br>')
116
+ if self.o is not None:
117
+ DimReport += str('Total outflow is present with ' +
118
+ str(len(self.o)) + ' years.<br>')
119
+ else:
120
+ DimReport += str('Total outflow is not present.<br>')
121
+ if self.o_c is not None:
122
+ DimReport += str('Outflow by cohorts is present with ' +
123
+ str(len(self.o_c)) + ' years and ' + str(len(self.o_c[0])) + ' cohorts.<br>')
124
+ else:
125
+ DimReport += str('Outflow by cohorts is not present.<br>')
126
+ if self.lt is not None:
127
+ DimReport += str('Lifetime distribution is present with type ' +
128
+ str(self.lt['Type']) + '.<br>')
129
+ else:
130
+ DimReport += str('Lifetime distribution is not present.<br>')
131
+ return DimReport
132
+ except:
133
+ return str('<br><b> Checking dimensions of dynamic stock model ' + self.name + ' failed.')
134
+
135
+ def compute_stock_change(self):
136
+ """ Determine stock change from time series for stock. Formula: stock_change(t) = stock(t) - stock(t-1)."""
137
+ if self.s is not None:
138
+ stock_change = np.zeros(len(self.s))
139
+ stock_change[0] = self.s[0]
140
+ stock_change[1::] = np.diff(self.s)
141
+ return stock_change
142
+ else:
143
+ return None
144
+
145
+ def check_stock_balance(self):
146
+ """ Check wether inflow, outflow, and stock are balanced. If possible, the method returns the vector 'Balance', where Balance = inflow - outflow - stock_change"""
147
+ try:
148
+ Balance = self.i - self.o - self.compute_stock_change()
149
+ return Balance
150
+ except:
151
+ # Could not determine balance. At least one of the variables is not defined.
152
+ return None
153
+
154
+ def compute_stock_total(self):
155
+ """Determine total stock as row sum of cohort-specific stock."""
156
+ if self.s is not None:
157
+ return self.s
158
+ else:
159
+ try:
160
+ self.s = self.s_c.sum(axis=1)
161
+ return self.s
162
+ except:
163
+ return None # No stock by cohorts exists, and total stock cannot be computed
164
+
165
+ def compute_outflow_total(self):
166
+ """Determine total outflow as row sum of cohort-specific outflow."""
167
+ if self.o is not None:
168
+ # Total outflow is already defined. Doing nothing.
169
+ return self.o
170
+ else:
171
+ try:
172
+ self.o = self.o_c.sum(axis=1)
173
+ return self.o
174
+ except:
175
+ return None # No outflow by cohorts exists, and total outflow cannot be computed
176
+
177
+ def compute_outflow_mb(self):
178
+ """Compute outflow from process via mass balance.
179
+ Needed in cases where lifetime is zero."""
180
+ try:
181
+ self.o = self.i - self.compute_stock_change()
182
+ return self.o
183
+ except:
184
+ return None # Variables to compute outflow were not present
185
+
186
+ """ Part 2: Lifetime model. """
187
+
188
+ def compute_outflow_pdf(self):
189
+ """
190
+ Lifetime model. The method compute outflow_pdf returns an array year-by-cohort of the probability of a item added to stock in year m (aka cohort m) leaves in in year n. This value equals pdf(n,m).
191
+ The pdf is computed from the survival table sf, where the type of the lifetime distribution enters.
192
+ The shape of the output pdf array is NoofYears * NoofYears, but the meaning is years by age-cohorts.
193
+ The method does nothing if the pdf alreay exists.
194
+ """
195
+ if self.pdf is None:
196
+ self.compute_sf() # computation of pdfs moved to this method: compute survival functions sf first, then calculate pdfs from sf.
197
+ self.pdf = np.zeros((len(self.t), len(self.t)))
198
+ self.pdf[np.diag_indices(len(self.t))] = np.ones(len(self.t)) - self.sf.diagonal(0)
199
+ for m in range(0,len(self.t)):
200
+ self.pdf[np.arange(m+1,len(self.t)),m] = -1 * np.diff(self.sf[np.arange(m,len(self.t)),m])
201
+ return self.pdf
202
+ else:
203
+ # pdf already exists
204
+ return self.pdf
205
+
206
+
207
+ def compute_sf(self): # survival functions
208
+ """
209
+ Survival table self.sf(m,n) denotes the share of an inflow in year n (age-cohort) still present at the end of year m (after m-n years).
210
+ The computation is self.sf(m,n) = ProbDist.sf(m-n), where ProbDist is the appropriate scipy function for the lifetime model chosen.
211
+ For lifetimes 0 the sf is also 0, meaning that the age-cohort leaves during the same year of the inflow.
212
+ The method compute outflow_sf returns an array year-by-cohort of the surviving fraction of a flow added to stock in year m (aka cohort m) in in year n. This value equals sf(n,m).
213
+ This is the only method for the inflow-driven model where the lifetime distribution directly enters the computation. All other stock variables are determined by mass balance.
214
+ The shape of the output sf array is NoofYears * NoofYears, and the meaning is years by age-cohorts.
215
+ The method does nothing if the sf alreay exists. For example, sf could be assigned to the dynamic stock model from an exogenous computation to save time.
216
+ """
217
+ if self.sf is None:
218
+ self.sf = np.zeros((len(self.t), len(self.t)))
219
+ # Perform specific computations and checks for each lifetime distribution:
220
+
221
+ if self.lt['Type'] == 'Fixed': # fixed lifetime, age-cohort leaves the stock in the model year when the age specified as 'Mean' is reached.
222
+ for m in range(0, len(self.t)): # cohort index
223
+ self.sf[m::,m] = np.multiply(1, (np.arange(0,len(self.t)-m) < self.lt['Mean'][m])) # converts bool to 0/1
224
+ # Example: if Lt is 3.5 years fixed, product will still be there after 0, 1, 2, and 3 years, gone after 4 years.
225
+
226
+ # if self.lt['Type'] == 'Simple': # Implement simple first-order decay
227
+ # for m in range(0, len(self.t)): # Loop over each cohort index
228
+ # mean_lifetime = self.lt['Mean'][m]
229
+ # # Calculate decay constant k based on mean lifetime
230
+ # k = ln(2) / half-life #if half-life is provided; otherwise:
231
+ # k = 1 / mean_lifetime # For mean lifetime-based decay constant
232
+ # # Create decay factors for each time step
233
+ # decay_factors = np.exp(-k * np.arange(0, len(self.t) - m))
234
+ # # Apply the decay factor to each cohort over time
235
+ # self.sf[m::, m] = decay_factors
236
+
237
+ if self.lt['Type'] == 'Simple': # Implement simple first-order decay
238
+ for m in range(0, len(self.t)): # Loop over each cohort index
239
+ mean_lifetime = self.lt['Mean'][m]
240
+
241
+ # Decide on decay constant k
242
+ if hasattr(self, 'half_life') and self.half_life is not None:
243
+ k = np.log(2) / self.half_life
244
+ else:
245
+ k = 1 / mean_lifetime # For mean lifetime-based decay
246
+
247
+ # Create decay factors for each time step
248
+ decay_factors = np.exp(-k * np.arange(0, len(self.t) - m))
249
+
250
+ # Apply decay factor to the cohort
251
+ self.sf[m::, m] = decay_factors
252
+
253
+
254
+
255
+ if self.lt['Type'] == 'Normal': # normally distributed lifetime with mean and standard deviation. Watch out for nonzero values
256
+ # for negative ages, no correction or truncation done here. Cf. note below.
257
+ for m in range(0, len(self.t)): # cohort index
258
+ if self.lt['Mean'][m] != 0: # For products with lifetime of 0, sf == 0
259
+ self.sf[m::,m] = scipy.stats.norm.sf(np.arange(0,len(self.t)-m), loc=self.lt['Mean'][m], scale=self.lt['StdDev'][m])
260
+ # NOTE: As normal distributions have nonzero pdf for negative ages, which are physically impossible,
261
+ # these outflow contributions can either be ignored (violates the mass balance) or
262
+ # allocated to the zeroth year of residence, the latter being implemented in the method compute compute_o_c_from_s_c.
263
+ # As alternative, use lognormal or folded normal distribution options.
264
+
265
+ if self.lt['Type'] == 'FoldedNormal': # Folded normal distribution, cf. https://en.wikipedia.org/wiki/Folded_normal_distribution
266
+ for m in range(0, len(self.t)): # cohort index
267
+ if self.lt['Mean'][m] != 0: # For products with lifetime of 0, sf == 0
268
+ self.sf[m::,m] = scipy.stats.foldnorm.sf(np.arange(0,len(self.t)-m), self.lt['Mean'][m]/self.lt['StdDev'][m], 0, scale=self.lt['StdDev'][m])
269
+ # NOTE: call this option with the parameters of the normal distribution mu and sigma of curve BEFORE folding,
270
+ # curve after folding will have different mu and sigma.
271
+
272
+ if self.lt['Type'] == 'LogNormal': # lognormal distribution
273
+ # Here, the mean and stddev of the lognormal curve,
274
+ # not those of the underlying normal distribution, need to be specified! conversion of parameters done here:
275
+ for m in range(0, len(self.t)): # cohort index
276
+ if self.lt['Mean'][m] != 0: # For products with lifetime of 0, sf == 0
277
+ # calculate parameter mu of underlying normal distribution:
278
+ LT_LN = np.log(self.lt['Mean'][m] / np.sqrt(1 + self.lt['Mean'][m] * self.lt['Mean'][m] / (self.lt['StdDev'][m] * self.lt['StdDev'][m])))
279
+ # calculate parameter sigma of underlying normal distribution:
280
+ SG_LN = np.sqrt(np.log(1 + self.lt['Mean'][m] * self.lt['Mean'][m] / (self.lt['StdDev'][m] * self.lt['StdDev'][m])))
281
+ # compute survial function
282
+ self.sf[m::,m] = scipy.stats.lognorm.sf(np.arange(0,len(self.t)-m), s=SG_LN, loc = 0, scale=np.exp(LT_LN))
283
+ # values chosen according to description on
284
+ # https://docs.scipy.org/doc/scipy-0.13.0/reference/generated/scipy.stats.lognorm.html
285
+ # Same result as EXCEL function "=LOGNORM.VERT(x;LT_LN;SG_LN;TRUE)"
286
+
287
+ if self.lt['Type'] == 'Weibull': # Weibull distribution with standard definition of scale and shape parameters
288
+ for m in range(0, len(self.t)): # cohort index
289
+ if self.lt['Shape'][m] != 0: # For products with lifetime of 0, sf == 0
290
+ self.sf[m::,m] = scipy.stats.weibull_min.sf(np.arange(0,len(self.t)-m), c=self.lt['Shape'][m], loc = 0, scale=self.lt['Scale'][m])
291
+
292
+ elif self.lt['Type'] in ['LandfillDecayWood', 'LandfillDecayPaper']: # FOD method IPCC https://www.ipcc-nggip.iges.or.jp/public/2019rf/pdf/5_Volume5/19R_V5_3_Ch03_SWDS.pdf
293
+ decay_rates = {'LandfillDecayWood': 0.05, 'LandfillDecayPaper': 0.025} # IPCC default decay rates
294
+ doc_values = {'LandfillDecayWood': 0.5, 'LandfillDecayPaper': 0.4} # IPCC default fraction of degradable organic carbon (DOC) for paper and wood
295
+ doc_f_values = {'LandfillDecayWood': 0.77, 'LandfillDecayPaper': 0.5} # IPCC default fraction of DOC that decomposes (DOC_f) for paper and wood
296
+
297
+ #The landfill adjustments are scaling factors applied to modify the default decay rates (k values) or half-lives (t1/2) of degradable organic carbon (DOC) in landfills under different conditions.
298
+ landfill_adjustments = {'Dry': 1.0, 'Wet': 1.2, 'Managed': 1.5} # Dry: no adjustment for dry landfills (set to 1); Wet:20% faster decay in wet landfills (k=0.024); Managed:50% faster decay in managed (k=0.03). Note: common k=0.02 is used as the ratios relative to the dry condition
299
+
300
+ # Get the value for landfill type
301
+ lt_type = self.lt['Type']
302
+ landfill_type = self.lt["condition"][0]
303
+
304
+ k_base = decay_rates[lt_type]
305
+ k_adjusted = k_base * landfill_adjustments.get(landfill_type, 1.0)
306
+
307
+ DOC = self.lt.get('DOC', doc_values[lt_type])
308
+ DOC_f = self.lt.get('DOC_f', doc_f_values[lt_type])
309
+ # IPCC default methane correction factor (MCF)
310
+ MCF = self.lt.get('MCF', 1.0)
311
+ # IPCC default fraction of methane in landfill gas (F)
312
+ F = self.lt.get('F', 0.5)
313
+ # IPCC default fraction of methane recovered (R)
314
+ R = self.lt.get('R', 0.0)
315
+ # IPCC default oxidation factor (OX)
316
+ OX = self.lt.get('OX', 0.0)
317
+
318
+ for m in range(len(self.t)): # Loop over each cohort index
319
+ decay_factors = np.exp(-k_adjusted * np.arange(len(self.t) - m)) # Compute decay factors for this cohort
320
+ DDOCm_decomposed = 1 - decay_factors
321
+ CH4_generated = DDOCm_decomposed * DOC * DOC_f * MCF * F * (16 / 12)
322
+
323
+ self.sf[m:, m] = decay_factors * DOC * DOC_f * MCF * (1 - R) * (1 - OX)
324
+
325
+ return self.sf
326
+ else:
327
+ # sf already exists
328
+ return self.sf
329
+
330
+
331
+ """
332
+ Part 3: Inflow driven model
333
+ Given: inflow, lifetime dist.
334
+ Default order of methods:
335
+ 1) determine stock by cohort
336
+ 2) determine total stock
337
+ 2) determine outflow by cohort
338
+ 3) determine total outflow
339
+ 4) check mass balance.
340
+ """
341
+
342
+ def compute_s_c_inflow_driven(self):
343
+ """ With given inflow and lifetime distribution, the method builds the stock by cohort.
344
+ """
345
+ if self.i is not None:
346
+ if self.lt is not None:
347
+ self.compute_sf()
348
+ self.s_c = np.einsum('c,tc->tc', self.i, self.sf) # See numpy's np.einsum for documentation.
349
+ # This command means: s_c[t,c] = i[c] * sf[t,c] for all t, c
350
+ # from the perspective of the stock the inflow has the dimension age-cohort,
351
+ # as each inflow(t) is added to the age-cohort c = t
352
+ return self.s_c
353
+ else:
354
+ # No lifetime distribution specified
355
+ return None
356
+ else:
357
+ # No inflow specified
358
+ return None
359
+
360
+ def compute_o_c_from_s_c(self):
361
+ """Compute outflow by cohort from stock by cohort."""
362
+ if self.s_c is not None:
363
+ if self.o_c is None:
364
+ self.o_c = np.zeros(self.s_c.shape)
365
+ self.o_c[1::,:] = -1 * np.diff(self.s_c,n=1,axis=0)
366
+ self.o_c[np.diag_indices(len(self.t))] = self.i - np.diag(self.s_c) # allow for outflow in year 0 already
367
+ return self.o_c
368
+ else:
369
+ # o_c already exists. Doing nothing.
370
+ return self.o_c
371
+ else:
372
+ # s_c does not exist. Doing nothing
373
+ return None
374
+
375
+ def compute_i_from_s(self, InitialStock):
376
+ """Given a stock at t0 broken down by different cohorts tx ... t0, an "initial stock".
377
+ This method calculates the original inflow that generated this stock.
378
+ Example:
379
+ """
380
+ if self.i is None: # only in cases where no inflow has been specified.
381
+ if len(InitialStock) == len(self.t):
382
+ self.i = np.zeros(len(self.t))
383
+ # construct the sf of a product of cohort tc surviving year t
384
+ # using the lifetime distributions of the past age-cohorts
385
+ self.compute_sf()
386
+ for Cohort in range(0, len(self.t)):
387
+ if self.sf[-1,Cohort] != 0:
388
+ self.i[Cohort] = InitialStock[Cohort] / self.sf[-1,Cohort]
389
+ else:
390
+ self.i[Cohort] = 0 # Not possible with given lifetime distribution
391
+ return self.i
392
+ else:
393
+ # The length of t and InitialStock needs to be equal
394
+ return None
395
+ else:
396
+ # i already exists. Doing nothing
397
+ return None
398
+
399
+ def compute_evolution_initialstock(self,InitialStock,SwitchTime):
400
+ """ Assume InitialStock is a vector that contains the age structure of the stock at time t0,
401
+ and it covers as many historic cohorts as there are elements in it.
402
+ This method then computes the future stock and outflow from the year SwitchTime onwards.
403
+ Only future years, i.e., years after SwitchTime, are computed.
404
+ NOTE: This method ignores and deletes previously calculated s_c and o_c.
405
+ The InitialStock is a vector of the age-cohort composition of the stock at SwitchTime, with length SwitchTime"""
406
+ if self.lt is not None:
407
+ self.s_c = np.zeros((len(self.t), len(self.t)))
408
+ self.o_c = np.zeros((len(self.t), len(self.t)))
409
+ self.compute_sf()
410
+ # Extract and renormalize array describing fate of initialstock:
411
+ Shares_Left = self.sf[SwitchTime,0:SwitchTime].copy()
412
+ self.s_c[SwitchTime,0:SwitchTime] = InitialStock # Add initial stock to s_c
413
+ self.s_c[SwitchTime::,0:SwitchTime] = np.tile(InitialStock.transpose(),(len(self.t)-SwitchTime,1)) * self.sf[SwitchTime::,0:SwitchTime] / np.tile(Shares_Left,(len(self.t)-SwitchTime,1))
414
+ return self.s_c
415
+
416
+
417
+
418
+ """
419
+ Part 4: Stock driven model
420
+ Given: total stock, lifetime dist.
421
+ Default order of methods:
422
+ 1) determine inflow, outflow by cohort, and stock by cohort
423
+ 2) determine total outflow
424
+ 3) determine stock change
425
+ 4) check mass balance.
426
+ """
427
+
428
+ def compute_stock_driven_model(self, NegativeInflowCorrect = False):
429
+ """ With given total stock and lifetime distribution,
430
+ the method builds the stock by cohort and the inflow.
431
+ """
432
+ if self.s is not None:
433
+ if self.lt is not None:
434
+ self.s_c = np.zeros((len(self.t), len(self.t)))
435
+ self.o_c = np.zeros((len(self.t), len(self.t)))
436
+ self.i = np.zeros(len(self.t))
437
+ # construct the sf of a product of cohort tc remaining in the stock in year t
438
+ self.compute_sf() # Computes sf if not present already.
439
+ # First year:
440
+ if self.sf[0, 0] != 0: # Else, inflow is 0.
441
+ self.i[0] = self.s[0] / self.sf[0, 0]
442
+ self.s_c[:, 0] = self.i[0] * self.sf[:, 0] # Future decay of age-cohort of year 0.
443
+ self.o_c[0, 0] = self.i[0] - self.s_c[0, 0]
444
+ # all other years:
445
+ for m in range(1, len(self.t)): # for all years m, starting in second year
446
+ # 1) Compute outflow from previous age-cohorts up to m-1
447
+ self.o_c[m, 0:m] = self.s_c[m-1, 0:m] - self.s_c[m, 0:m] # outflow table is filled row-wise, for each year m.
448
+ # 2) Determine inflow from mass balance:
449
+ if NegativeInflowCorrect is False: # if no correction for negative inflows is made
450
+ if self.sf[m,m] != 0: # Else, inflow is 0.
451
+ self.i[m] = (self.s[m] - self.s_c[m, :].sum()) / self.sf[m,m] # allow for outflow during first year by rescaling with 1/sf[m,m]
452
+ # 3) Add new inflow to stock and determine future decay of new age-cohort
453
+ self.s_c[m::, m] = self.i[m] * self.sf[m::, m]
454
+ self.o_c[m, m] = self.i[m] * (1 - self.sf[m, m])
455
+ # 2a) Correct remaining stock in cases where inflow would be negative:
456
+ if NegativeInflowCorrect is True: # if the stock declines faster than according to the lifetime model, this option allows to extract additional stock items.
457
+ # The negative inflow correction implemented here was developed in a joined effort by Sebastiaan Deetman and Stefan Pauliuk.
458
+ InflowTest = self.s[m] - self.s_c[m, :].sum()
459
+ if InflowTest < 0: # if stock-driven model would yield negative inflow
460
+ Delta = -1 * InflowTest # Delta > 0!
461
+ self.i[m] = 0 # Set inflow to 0 and distribute mass balance gap onto remaining cohorts:
462
+ if self.s_c[m,:].sum() != 0:
463
+ Delta_percent = Delta / self.s_c[m,:].sum()
464
+ # Distribute gap equally across all cohorts (each cohort is adjusted by the same %, based on surplus with regards to the prescribed stock)
465
+ # Delta_percent is a % value <= 100%
466
+ else:
467
+ Delta_percent = 0 # stock in this year is already zero, method does not work in this case.
468
+ # correct for outflow and stock in current and future years
469
+ # adjust the entire stock AFTER year m as well, stock is lowered in year m, so future cohort survival also needs to decrease.
470
+ self.o_c[m, :] = self.o_c[m, :] + (self.s_c[m, :] * Delta_percent) # increase outflow according to the lost fraction of the stock, based on Delta_c
471
+ self.s_c[m::,0:m] = self.s_c[m::,0:m] * (1-Delta_percent) # shrink future description of stock from previous age-cohorts by factor Delta_percent in current AND future years.
472
+ else: # If no negative inflow would occur
473
+ if self.sf[m,m] != 0: # Else, inflow is 0.
474
+ self.i[m] = (self.s[m] - self.s_c[m, :].sum()) / self.sf[m,m] # allow for outflow during first year by rescaling with 1/sf[m,m]
475
+ # Add new inflow to stock and determine future decay of new age-cohort
476
+ self.s_c[m::, m] = self.i[m] * self.sf[m::, m]
477
+ self.o_c[m, m] = self.i[m] * (1 - self.sf[m, m])
478
+ # NOTE: This method of negative inflow correction is only of of many plausible methods of increasing the outflow to keep matching stock levels.
479
+ # It assumes that the surplus stock is removed in the year that it becomes obsolete. Each cohort loses the same fraction.
480
+ # Modellers need to try out whether this method leads to justifiable results.
481
+ # In some situations it is better to change the lifetime assumption than using the NegativeInflowCorrect option.
482
+
483
+ return self.s_c, self.o_c, self.i
484
+ else:
485
+ # No lifetime distribution specified
486
+ return None, None, None
487
+ else:
488
+ # No stock specified
489
+ return None, None, None
490
+
491
+
492
+ def compute_stock_driven_model_initialstock(self,InitialStock,SwitchTime,NegativeInflowCorrect = False):
493
+ """ With given total stock and lifetime distribution, the method builds the stock by cohort and the inflow.
494
+ The extra parameter InitialStock is a vector that contains the age structure of the stock at the END of the year Switchtime -1 = t0.
495
+ ***
496
+ Convention 1: Stocks are measured AT THE END OF THE YEAR. Flows occur DURING THE YEAR.
497
+ Convention 2: The model time t spans both historic and future age-cohorts, and the index SwitchTime -1 indicates the first future age-cohort.
498
+ Convention 3: SwitchTime = len(InitialStock) + 1, that means SwitchTime is counted starting from 1 and not 0.
499
+ Convention 4: The future stock time series has 0 as its first len(InitialStock) elements.
500
+ ***
501
+ In the year SwitchTime the model switches from the historic stock to the stock-driven approach.
502
+ The year SwitchTime is the first year with the stock-driven approach.
503
+ InitialStock contains the age-cohort composition of the stock AT THE END of year SwitchTime -1.
504
+ InitialStock must have length = SwithTime -1.
505
+ For the option "NegativeInflowCorrect", see the explanations for the method compute_stock_driven_model(self, NegativeInflowCorrect = True).
506
+ NegativeInflowCorrect only affects the future stock time series and works exactly as for the stock-driven model without initial stock.
507
+ """
508
+ if self.s is not None:
509
+ if self.lt is not None:
510
+ self.s_c = np.zeros((len(self.t), len(self.t)))
511
+ self.s_c[SwitchTime -2,0:SwitchTime-1] = InitialStock # assign initialstock to stock-by-cohort variable at END OF YEAR SwitchTime (here -1, because indexing starts at 0.).
512
+ self.o_c = np.zeros((len(self.t), len(self.t)))
513
+ self.i = np.zeros(len(self.t))
514
+
515
+ # construct the sdf of a product of cohort tc leaving the stock in year t
516
+ self.compute_sf() # Computes sf if not present already.
517
+ # Construct historic inflows
518
+ for c in range(0,SwitchTime -1):
519
+ if self.sf[SwitchTime -2,c] != 0:
520
+ self.i[c] = InitialStock[c] / self.sf[SwitchTime -2,c]
521
+ else:
522
+ self.i[c] = InitialStock[c]
523
+
524
+ # Add stock from historic inflow
525
+ self.s_c[:,0:SwitchTime-1] = np.einsum('tc,c->tc',self.sf[:,0:SwitchTime-1],self.i[0:SwitchTime-1])
526
+ # calculate historic outflow
527
+ for m in range(0,SwitchTime-1):
528
+ self.o_c[m, m] = self.i[m] * (1 - self.sf[m, m])
529
+ self.o_c[m+1::,m] = self.s_c[m:-1,m] - self.s_c[m+1::,m]
530
+ # for future: year-by-year computation, starting from SwitchTime
531
+ if NegativeInflowCorrect is False:
532
+ for m in range(SwitchTime-1, len(self.t)): # for all years m, starting at SwitchTime
533
+ # 1) Determine inflow from mass balance:
534
+ if self.sf[m,m] != 0: # Else, inflow is 0.
535
+ self.i[m] = (self.s[m] - self.s_c[m, :].sum()) / self.sf[m,m] # allow for outflow during first year by rescaling with 1/sf[m,m]
536
+ # NOTE: The stock-driven method may lead to negative inflows, if the stock development is in contradiction with the lifetime model.
537
+ # In such situations the lifetime assumption must be changed, either by directly using different lifetime values or by adjusting the outlfows,
538
+ # cf. the option NegativeInflowCorrect in the method compute_stock_driven_model.
539
+ # 2) Add new inflow to stock and determine future decay of new age-cohort
540
+ self.s_c[m::, m] = self.i[m] * self.sf[m::, m]
541
+ self.o_c[m, m] = self.i[m] * (1 - self.sf[m, m])
542
+ self.o_c[m+1::,m] = self.s_c[m:-1,m] - self.s_c[m+1::,m]
543
+ if NegativeInflowCorrect is True:
544
+ for m in range(SwitchTime-1, len(self.t)): # for all years m, starting at SwitchTime
545
+ self.o_c[m, 0:m] = self.s_c[m-1, 0:m] - self.s_c[m, 0:m] # outflow table is filled row-wise, for each year m.
546
+ # 1) Determine text inflow from mass balance:
547
+ InflowTest = self.s[m] - self.s_c[m, :].sum()
548
+ if InflowTest < 0:
549
+ Delta = -1 * InflowTest # Delta > 0!
550
+ self.i[m] = 0 # Set inflow to 0 and distribute mass balance gap onto remaining cohorts:
551
+ if self.s_c[m,:].sum() != 0:
552
+ Delta_percent = Delta / self.s_c[m,:].sum()
553
+ # Distribute gap equally across all cohorts (each cohort is adjusted by the same %, based on surplus with regards to the prescribed stock)
554
+ # Delta_percent is a % value <= 100%
555
+ else:
556
+ Delta_percent = 0 # stock in this year is already zero, method does not work in this case.
557
+ # correct for outflow and stock in current and future years
558
+ # adjust the entire stock AFTER year m as well, stock is lowered in year m, so future cohort survival also needs to decrease.
559
+ # print(InflowTest)
560
+ # print((self.s_c[m, :] * Delta_percent).sum())
561
+ # print('_')
562
+ self.o_c[m, :] = self.o_c[m, :] + (self.s_c[m, :] * Delta_percent).copy() # increase outflow according to the lost fraction of the stock, based on Delta_c
563
+ self.s_c[m::,0:m] = self.s_c[m::,0:m] * (1-Delta_percent.copy()) # shrink future description of stock from previous age-cohorts by factor Delta_percent in current AND future years.
564
+ else:
565
+ if self.sf[m,m] != 0: # Else, inflow is 0.
566
+ self.i[m] = (self.s[m] - self.s_c[m, :].sum()) / self.sf[m,m] # allow for outflow during first year by rescaling with 1/sf[m,m]
567
+ # 2) Add new inflow to stock and determine future decay of new age-cohort
568
+ self.s_c[m::, m] = self.i[m] * self.sf[m::, m]
569
+ self.o_c[m, m] = self.i[m] * (1 - self.sf[m, m])
570
+ # Add historic stock series to total stock s:
571
+ self.s[0:SwitchTime-1]= self.s_c[0:SwitchTime-1,:].sum(axis =1).copy()
572
+ return self.s_c, self.o_c, self.i
573
+ else:
574
+ # No lifetime distribution specified
575
+ return None, None, None
576
+ else:
577
+ # No stock specified
578
+ return None, None, None
579
+
580
+
581
+ def compute_stock_driven_model_initialstock_typesplit(self,FutureStock,InitialStock,SFArrayCombined,TypeSplit):
582
+ """
583
+ With given total future stock and lifetime distribution, the method builds the stock by cohort and the inflow.
584
+ The age structure of the initial stock is given for each technology, and a type split of total inflow into different technology types is given as well.
585
+
586
+ SPECIFICATION: Stocks are always measured AT THE END of the discrete time interval.
587
+
588
+ Indices:
589
+ t: time: Entire time frame: from earliest age-cohort to latest model year.
590
+ c: age-cohort: same as time.
591
+ T: Switch time: DEFINED as first year where historic stock is NOT present, = last year where historic stock is present +1.
592
+ Switchtime is calculated internally, by subtracting the length of the historic stock from the total model length.
593
+ g: product type
594
+
595
+ Data:
596
+ FutureStock[t], total future stock at end of each year, starting at T
597
+ InitialStock[c,g], 0...T-1;0...T-1, stock at the end of T-1, by age-cohort c, ranging from 0...T-1, and product type g
598
+ c-dimension has full length, all future years must be 0.
599
+ SFArrayCombined[t,c,g], Survival function of age-cohort c at end of year t for product type g
600
+ this array spans both historic and future age-cohorts
601
+ Typesplit[t,g], splits total inflow into product types for future years
602
+
603
+ The extra parameter InitialStock is a vector that contains the age structure of the stock at time t0, and it covers as many historic cohorts as there are elements in it.
604
+ In the year SwitchTime the model switches from the historic stock to the stock-driven approach.
605
+ Only future years, i.e., years after SwitchTime, are computed and returned.
606
+ The InitialStock is a vector of the age-cohort composition of the stock at SwitchTime, with length SwitchTime.
607
+ The parameter TypeSplit splits the total inflow into Ng types. """
608
+
609
+ if self.s is not None:
610
+ if self.lt is not None:
611
+
612
+ SwitchTime = SFArrayCombined.shape[0] - FutureStock.shape[0]
613
+ Ntt = SFArrayCombined.shape[0] # Total no of years
614
+ Nt0 = FutureStock.shape[0] # No of future years
615
+ Ng = SFArrayCombined.shape[2] # No of product groups
616
+
617
+ s_cg = np.zeros((Nt0,Ntt,Ng)) # stock for future years, all age-cohorts and product
618
+ o_cg = np.zeros((Nt0,Ntt,Ng)) # outflow by future years, all cohorts and products
619
+ i_g = np.zeros((Ntt,Ng)) # inflow by product
620
+
621
+ # Construct historic inflows
622
+ for c in range(0,SwitchTime): # for all historic age-cohorts til SwitchTime - 1:
623
+ for g in range(0,Ng):
624
+ if SFArrayCombined[SwitchTime-1,c,g] != 0:
625
+ i_g[c,g] = InitialStock[c,g] / SFArrayCombined[SwitchTime-1,c,g]
626
+
627
+ # if InitialStock is 0, historic inflow also remains 0,
628
+ # as it has no impact on future anymore.
629
+
630
+ # If survival function is 0 but initial stock is not, the data are inconsisent and need to be revised.
631
+ # For example, a safety-relevant device with 5 years fixed lifetime but a 10 year old device is present.
632
+ # Such items will be ignored and break the mass balance.
633
+
634
+ # year-by-year computation, starting from SwitchTime
635
+ for t in range(SwitchTime, Ntt): # for all years t, starting at SwitchTime
636
+ # 1) Compute stock at the end of the year:
637
+ s_cg[t - SwitchTime,:,:] = np.einsum('cg,cg->cg',i_g,SFArrayCombined[t,:,:])
638
+ # 2) Compute outflow during year t from previous age-cohorts:
639
+ if t == SwitchTime:
640
+ o_cg[t -SwitchTime,:,:] = InitialStock - s_cg[t -SwitchTime,:,:]
641
+ else:
642
+ o_cg[t -SwitchTime,:,:] = s_cg[t -SwitchTime -1,:,:] - s_cg[t -SwitchTime,:,:] # outflow table is filled row-wise, for each year t.
643
+ # 3) Determine total inflow from mass balance:
644
+ i0 = FutureStock[t -SwitchTime] - s_cg[t - SwitchTime,:,:].sum()
645
+ # 4) Add new inflow to stock and determine future decay of new age-cohort
646
+ i_g[t,:] = TypeSplit[t -SwitchTime,:] * i0
647
+ for g in range(0,Ng): # Correct for share of inflow leaving during first year.
648
+ if SFArrayCombined[t,t,g] != 0: # Else, inflow leaves within the same year and stock modelling is useless
649
+ i_g[t,g] = i_g[t,g] / SFArrayCombined[t,t,g] # allow for outflow during first year by rescaling with 1/SF[t,t,g]
650
+ s_cg[t -SwitchTime,t,g] = i_g[t,g] * SFArrayCombined[t,t,g]
651
+ o_cg[t -SwitchTime,t,g] = i_g[t,g] * (1 - SFArrayCombined[t,t,g])
652
+
653
+ # Add total values of parameter to enable mass balance check:
654
+ self.s_c = s_cg.sum(axis =2)
655
+ self.o_c = o_cg.sum(axis =2)
656
+ self.i = i_g[SwitchTime::,:].sum(axis =1)
657
+
658
+ return s_cg, o_cg, i_g
659
+ else:
660
+ # No lifetime distribution specified
661
+ return None, None, None
662
+ else:
663
+ # No stock specified
664
+ return None, None, None
665
+
666
+ def compute_stock_driven_model_initialstock_typesplit_negativeinflowcorrect(self,SwitchTime,InitialStock,SFArrayCombined,TypeSplit,NegativeInflowCorrect = False):
667
+ """
668
+ With given total future stock and lifetime distribution, the method builds the stock by cohort and the inflow.
669
+ The age structure of the initial stock is given for each technology, and a type split of total inflow into different technology types is given as well.
670
+ For the option "NegativeInflowCorrect", see the explanations for the method compute_stock_driven_model(self, NegativeInflowCorrect = True).
671
+ NegativeInflowCorrect only affects the future stock time series and works exactly as for the stock-driven model without initial stock.
672
+
673
+ SPECIFICATION: Stocks are always measured AT THE END of the discrete time interval.
674
+
675
+ Indices:
676
+ t: time: Entire time frame: from earliest age-cohort to latest model year.
677
+ c: age-cohort: same as time.
678
+ T: Switch time: DEFINED as first year where historic stock is NOT present, = last year where historic stock is present +1.
679
+ Switchtime must be given as argument. Example: if the first three age-cohorts are historic, SwitchTime is 3, which indicates the 4th year.
680
+ That also means that the first 3 time-entries for the stock and typesplit arrays must be 0.
681
+ g: product type
682
+
683
+ Data:
684
+ s[t], total future stock time series, at end of each year, starting at T, trailing 0s for historic years.
685
+ ! is not handed over with the function call but earlier, when defining the dsm.
686
+ InitialStock[c,g], 0...T-1;0...T-1, stock at the end of T-1, by age-cohort c, ranging from 0...T-1, and product type g
687
+ c-dimension has full length, all future years must be 0.
688
+ SFArrayCombined[t,c,g], Survival function of age-cohort c at end of year t for product type g
689
+ this array spans both historic and future age-cohorts
690
+ Typesplit[t,g], splits total inflow into product types for future years
691
+ NegativeInflowCorrect BOOL, retains items in stock if their leaving would lead to negative inflows.
692
+
693
+ The extra parameter InitialStock is a vector that contains the age structure of the stock at time t0, and it covers as many historic cohorts as there are elements in it.
694
+ In the year SwitchTime the model switches from the historic stock to the stock-driven approach.
695
+ Only future years, i.e., years after SwitchTime, are computed and returned.
696
+ The InitialStock is a vector of the age-cohort composition of the stock at SwitchTime, with length SwitchTime.
697
+ The parameter TypeSplit splits the total inflow into Ng types. """
698
+
699
+ if self.s is not None:
700
+ if self.lt is not None:
701
+
702
+ Ntt = SFArrayCombined.shape[0] # Total no of years
703
+ Ng = SFArrayCombined.shape[2] # No of product groups
704
+
705
+ s_cg = np.zeros((Ntt,Ntt,Ng)) # stock for future years, all age-cohorts and products
706
+ o_cg = np.zeros((Ntt,Ntt,Ng)) # outflow by future years, all cohorts and products
707
+ i_g = np.zeros((Ntt,Ng)) # inflow for all years by product
708
+ NIC_Flags = np.zeros((Ntt,1)) # inflow flog for future years, will be set to calculated negative inflow value if negative inflow occurs and is corrected for.
709
+
710
+ self.s_c = np.zeros((len(self.t), len(self.t)))
711
+ self.o_c = np.zeros((len(self.t), len(self.t)))
712
+ self.i = np.zeros(len(self.t))
713
+
714
+ # construct the sdf of a product of cohort tc leaving the stock in year t
715
+ self.compute_sf() # Computes sf if not present already.
716
+ # Construct historic inflows
717
+ for c in range(0,SwitchTime): # for all historic age-cohorts til SwitchTime - 1:
718
+ for g in range(0,Ng):
719
+ if SFArrayCombined[SwitchTime-1,c,g] != 0:
720
+ i_g[c,g] = InitialStock[c,g] / SFArrayCombined[SwitchTime-1,c,g]
721
+
722
+ # if InitialStock is 0, historic inflow also remains 0,
723
+ # as it has no impact on future anymore.
724
+
725
+ # If survival function is 0 but initial stock is not, the data are inconsisent and need to be revised.
726
+ # For example, a safety-relevant device with 5 years fixed lifetime but a 10 year old device is present.
727
+ # Such items will be ignored and break the mass balance.
728
+
729
+ # Compute stocks from historic inflows
730
+ s_cg[:,0:SwitchTime,:] = np.einsum('tcg,cg->tcg',SFArrayCombined[:,0:SwitchTime,:],i_g[0:SwitchTime,:])
731
+ # calculate historic outflows
732
+ for m in range(0,SwitchTime):
733
+ o_cg[m,m,:] = i_g[m,:] * (1 - SFArrayCombined[m,m,:])
734
+ o_cg[m+1::,m,:] = s_cg[m:-1,m,:] - s_cg[m+1::,m,:]
735
+ # add historic age-cohorts to total stock:
736
+ self.s[0:SwitchTime] = np.einsum('tcg->t',s_cg[0:SwitchTime,:,:])
737
+
738
+ # for future: year-by-year computation, starting from SwitchTime
739
+ if NegativeInflowCorrect is False:
740
+ for m in range(SwitchTime, len(self.t)): # for all years m, starting at SwitchTime
741
+ # 1) Determine inflow from mass balance:
742
+ i0_test = self.s[m] - s_cg[m,:,:].sum()
743
+ if i0_test < 0:
744
+ NIC_Flags[m] = i0_test
745
+ for g in range(0,Ng):
746
+ if SFArrayCombined[m,m,g] != 0: # Else, inflow is 0.
747
+ i_g[m,g] = TypeSplit[m,g] * i0_test / SFArrayCombined[m,m,g] # allow for outflow during first year by rescaling with 1/sf[m,m]
748
+ # NOTE: The stock-driven method may lead to negative inflows, if the stock development is in contradiction with the lifetime model.
749
+ # In such situations the lifetime assumption must be changed, either by directly using different lifetime values or by adjusting the outlfows,
750
+ # cf. the option NegativeInflowCorrect in the method compute_stock_driven_model.
751
+ # 2) Add new inflow to stock and determine future decay of new age-cohort
752
+ s_cg[m::,m,g] = i_g[m,g] * SFArrayCombined[m::,m,g]
753
+ o_cg[m,m,g] = i_g[m,g] * (1 - SFArrayCombined[m,m,g])
754
+ o_cg[m+1::,m,g] = s_cg[m:-1,m,g] - s_cg[m+1::,m,g]
755
+
756
+ if NegativeInflowCorrect is True:
757
+ for m in range(SwitchTime, len(self.t)): # for all years m, starting at SwitchTime
758
+ # 1) Determine inflow from mass balance:
759
+ i0_test = self.s[m] - s_cg[m,:,:].sum()
760
+ if i0_test < 0:
761
+ NIC_Flags[m] = i0_test
762
+ Delta = -1 * i0_test # Delta > 0!
763
+ i_g[m,:] = 0 # Set inflow to 0 and distribute mass balance gap onto remaining cohorts:
764
+ if s_cg[m,:,:].sum() != 0:
765
+ Delta_percent = Delta / s_cg[m,:,:].sum()
766
+ # Distribute gap equally across all cohorts (each cohort is adjusted by the same %, based on surplus with regards to the prescribed stock)
767
+ # Delta_percent is a % value <= 100%
768
+ else:
769
+ Delta_percent = 0 # stock in this year is already zero, method does not work in this case.
770
+ # correct for outflow and stock in current and future years
771
+ # adjust the entire stock AFTER year m as well, stock is lowered in year m, so future cohort survival also needs to decrease.
772
+ o_cg[m, :,:] = o_cg[m, :,:] + (s_cg[m, :,:] * Delta_percent).copy() # increase outflow according to the lost fraction of the stock, based on Delta_c
773
+ s_cg[m::,0:m,:] = s_cg[m::,0:m,:] * (1-Delta_percent.copy()) # shrink future description of stock from previous age-cohorts by factor Delta_percent in current AND future years.
774
+ o_cg[m+1::,:,:] = s_cg[m:-1,:,:] - s_cg[m+1::,:,:] # recalculate future outflows
775
+
776
+ else:
777
+ for g in range(0,Ng):
778
+ if SFArrayCombined[m,m,g] != 0: # Else, inflow is 0.
779
+ i_g[m,g] = TypeSplit[m,g] * i0_test / SFArrayCombined[m,m,g] # allow for outflow during first year by rescaling with 1/sf[m,m]
780
+ # NOTE: The stock-driven method may lead to negative inflows, if the stock development is in contradiction with the lifetime model.
781
+ # In such situations the lifetime assumption must be changed, either by directly using different lifetime values or by adjusting the outlfows,
782
+ # cf. the option NegativeInflowCorrect in the method compute_stock_driven_model.
783
+ # 2) Add new inflow to stock and determine future decay of new age-cohort
784
+ s_cg[m::,m,g] = i_g[m,g] * SFArrayCombined[m::,m,g]
785
+ o_cg[m,m,g] = i_g[m,g] * (1 - SFArrayCombined[m,m,g])
786
+ o_cg[m+1::,m,g] = s_cg[m:-1,m,g] - s_cg[m+1::,m,g]
787
+
788
+ # Add total values of parameter to enable mass balance check:
789
+ self.s_c = s_cg.sum(axis =2)
790
+ self.o_c = o_cg.sum(axis =2)
791
+ self.i = i_g.sum(axis =1)
792
+
793
+ return s_cg, o_cg, i_g, NIC_Flags
794
+
795
+ else:
796
+ # No lifetime distribution specified
797
+ return None, None, None, None
798
+ else:
799
+ # No stock specified
800
+ return None, None, None, None
801
+
802
+
803
+
804
+ #
805
+ #
806
+ # The end.
807
+ #
808
+