Conditions | 5 |
Total Lines | 444 |
Code Lines | 259 |
Lines | 0 |
Ratio | 0 % |
Changes | 0 |
Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.
For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.
Commonly applied refactorings include:
If many parameters/temporary variables are present:
1 | # -*- coding: utf-8 -*- |
||
64 | def main(optimize=True): |
||
65 | ########################################################################## |
||
66 | # Initialize the energy system and calculate necessary parameters |
||
67 | ########################################################################## |
||
68 | |||
69 | # Start time for calculating the total elapsed time. |
||
70 | start_simulation_time = time.time() |
||
71 | |||
72 | start = "2022-01-01" |
||
73 | |||
74 | # The maximum number of days depends on the given *.csv file. |
||
75 | n_days = 10 |
||
76 | n_days_in_year = 365 |
||
77 | |||
78 | # Create date and time objects. |
||
79 | start_date_obj = datetime.strptime(start, "%Y-%m-%d") |
||
80 | start_date = start_date_obj.date() |
||
81 | start_datetime = datetime.combine( |
||
82 | start_date_obj.date(), start_date_obj.time() |
||
83 | ) |
||
84 | end_datetime = start_datetime + timedelta(days=n_days) |
||
85 | |||
86 | # Import data. |
||
87 | |||
88 | filename = os.path.join(os.path.dirname(__file__), "solar_generation.csv") |
||
89 | data = pd.read_csv(filename) |
||
90 | |||
91 | # Change the index of data to be able to select data based on the time |
||
92 | # range. |
||
93 | data.index = pd.date_range(start="2022-01-01", periods=len(data), freq="h") |
||
94 | |||
95 | # Choose the range of the solar potential and demand |
||
96 | # based on the selected simulation period. |
||
97 | solar_potential = data.SolarGen.loc[start_datetime:end_datetime] |
||
98 | hourly_demand = data.Demand.loc[start_datetime:end_datetime] |
||
99 | peak_solar_potential = solar_potential.max() |
||
100 | peak_demand = hourly_demand.max() |
||
101 | |||
102 | # Create the energy system. |
||
103 | date_time_index = solph.create_time_index( |
||
104 | number=n_days * 24, start=start_date |
||
105 | ) |
||
106 | energysystem = solph.EnergySystem( |
||
107 | timeindex=date_time_index, infer_last_interval=False |
||
108 | ) |
||
109 | |||
110 | # -------------------- BUSES -------------------- |
||
111 | # Create electricity and diesel buses. |
||
112 | b_el_ac = solph.buses.Bus(label="electricity_ac") |
||
113 | b_el_dc = solph.buses.Bus(label="electricity_dc") |
||
114 | b_diesel = solph.buses.Bus(label="diesel") |
||
115 | |||
116 | # -------------------- SOURCES -------------------- |
||
117 | diesel_cost = 0.65 # currency/l |
||
118 | diesel_density = 0.846 # kg/l |
||
119 | diesel_lhv = 11.83 # kWh/kg |
||
120 | diesel_source = solph.components.Source( |
||
121 | label="diesel_source", |
||
122 | outputs={ |
||
123 | b_diesel: solph.flows.Flow( |
||
124 | variable_costs=diesel_cost / diesel_density / diesel_lhv |
||
125 | ) |
||
126 | }, |
||
127 | ) |
||
128 | |||
129 | # EPC stands for the equivalent periodical costs. |
||
130 | epc_pv = 152.62 # currency/kW/year |
||
131 | pv = solph.components.Source( |
||
132 | label="pv", |
||
133 | outputs={ |
||
134 | b_el_dc: solph.flows.Flow( |
||
135 | fix=solar_potential / peak_solar_potential, |
||
136 | nominal_capacity=solph.Investment( |
||
137 | ep_costs=epc_pv * n_days / n_days_in_year |
||
138 | ), |
||
139 | variable_costs=0, |
||
140 | ) |
||
141 | }, |
||
142 | ) |
||
143 | |||
144 | # -------------------- CONVERTERS -------------------- |
||
145 | # The diesel genset assumed to have a fixed efficiency of 33%. |
||
146 | |||
147 | # The output power of the diesel genset can only vary between |
||
148 | # the given minimum and maximum loads, which represent the fraction |
||
149 | # of the optimal capacity obtained from the optimization. |
||
150 | |||
151 | epc_diesel_genset = 84.80 # currency/kW/year |
||
152 | variable_cost_diesel_genset = 0.045 # currency/kWh |
||
153 | min_load = 0.2 |
||
154 | max_load = 1.0 |
||
155 | diesel_genset = solph.components.Converter( |
||
156 | label="diesel_genset", |
||
157 | inputs={b_diesel: solph.flows.Flow()}, |
||
158 | outputs={ |
||
159 | b_el_ac: solph.flows.Flow( |
||
160 | variable_costs=variable_cost_diesel_genset, |
||
161 | min=min_load, |
||
162 | max=max_load, |
||
163 | nominal_capacity=solph.Investment( |
||
164 | ep_costs=epc_diesel_genset * n_days / n_days_in_year, |
||
165 | maximum=2 * peak_demand, |
||
166 | ), |
||
167 | nonconvex=solph.NonConvex(), |
||
168 | ) |
||
169 | }, |
||
170 | conversion_factors={b_el_ac: 0.33}, |
||
171 | ) |
||
172 | |||
173 | # The rectifier assumed to have a fixed efficiency of 98%. |
||
174 | epc_rectifier = 62.35 # currency/kW/year |
||
175 | rectifier = solph.components.Converter( |
||
176 | label="rectifier", |
||
177 | inputs={ |
||
178 | b_el_ac: solph.flows.Flow( |
||
179 | nominal_capacity=solph.Investment( |
||
180 | ep_costs=epc_rectifier * n_days / n_days_in_year |
||
181 | ), |
||
182 | variable_costs=0, |
||
183 | ) |
||
184 | }, |
||
185 | outputs={b_el_dc: solph.flows.Flow()}, |
||
186 | conversion_factors={ |
||
187 | b_el_dc: 0.98, |
||
188 | }, |
||
189 | ) |
||
190 | |||
191 | # The inverter assumed to have a fixed efficiency of 98%. |
||
192 | epc_inverter = 62.35 # currency/kW/year |
||
193 | inverter = solph.components.Converter( |
||
194 | label="inverter", |
||
195 | inputs={ |
||
196 | b_el_dc: solph.flows.Flow( |
||
197 | nominal_capacity=solph.Investment( |
||
198 | ep_costs=epc_inverter * n_days / n_days_in_year |
||
199 | ), |
||
200 | variable_costs=0, |
||
201 | ) |
||
202 | }, |
||
203 | outputs={b_el_ac: solph.flows.Flow()}, |
||
204 | conversion_factors={ |
||
205 | b_el_ac: 0.98, |
||
206 | }, |
||
207 | ) |
||
208 | |||
209 | # -------------------- STORAGE -------------------- |
||
210 | epc_battery = 101.00 # currency/kWh/year |
||
211 | battery = solph.components.GenericStorage( |
||
212 | label="battery", |
||
213 | nominal_capacity=solph.Investment( |
||
214 | ep_costs=epc_battery * n_days / n_days_in_year |
||
215 | ), |
||
216 | inputs={b_el_dc: solph.flows.Flow(variable_costs=0)}, |
||
217 | outputs={ |
||
218 | b_el_dc: solph.flows.Flow( |
||
219 | nominal_capacity=solph.Investment(ep_costs=0) |
||
220 | ) |
||
221 | }, |
||
222 | initial_storage_level=0.0, |
||
223 | min_storage_level=0.0, |
||
224 | max_storage_level=1.0, |
||
225 | balanced=True, |
||
226 | inflow_conversion_factor=0.9, |
||
227 | outflow_conversion_factor=0.9, |
||
228 | invest_relation_input_capacity=1, |
||
229 | invest_relation_output_capacity=0.5, |
||
230 | ) |
||
231 | |||
232 | # -------------------- SINKS -------------------- |
||
233 | demand_el = solph.components.Sink( |
||
234 | label="electricity_demand", |
||
235 | inputs={ |
||
236 | b_el_ac: solph.flows.Flow( |
||
237 | fix=hourly_demand / peak_demand, |
||
238 | nominal_capacity=peak_demand, |
||
239 | ) |
||
240 | }, |
||
241 | ) |
||
242 | |||
243 | excess_el = solph.components.Sink( |
||
244 | label="excess_el", |
||
245 | inputs={b_el_dc: solph.flows.Flow()}, |
||
246 | ) |
||
247 | |||
248 | # Add all objects to the energy system. |
||
249 | energysystem.add( |
||
250 | pv, |
||
251 | diesel_source, |
||
252 | b_el_dc, |
||
253 | b_el_ac, |
||
254 | b_diesel, |
||
255 | inverter, |
||
256 | rectifier, |
||
257 | diesel_genset, |
||
258 | battery, |
||
259 | demand_el, |
||
260 | excess_el, |
||
261 | ) |
||
262 | |||
263 | ########################################################################## |
||
264 | # Optimise the energy system |
||
265 | ########################################################################## |
||
266 | |||
267 | # The higher the MipGap or ratioGap, the faster the solver would converge, |
||
268 | # but the less accurate the results would be. |
||
269 | solver_option = {"gurobi": {"MipGap": "0.02"}, "cbc": {"ratioGap": "0.02"}} |
||
270 | solver = "cbc" |
||
271 | |||
272 | if optimize is False: |
||
273 | return energysystem |
||
274 | |||
275 | model = solph.Model(energysystem) |
||
276 | model.solve( |
||
277 | solver=solver, |
||
278 | solve_kwargs={"tee": True}, |
||
279 | cmdline_options=solver_option[solver], |
||
280 | ) |
||
281 | |||
282 | # End of the calculation time. |
||
283 | end_simulation_time = time.time() |
||
284 | |||
285 | ########################################################################## |
||
286 | # Process the results |
||
287 | ########################################################################## |
||
288 | |||
289 | results = solph.processing.results(model) |
||
290 | |||
291 | results_pv = solph.views.node(results=results, node="pv") |
||
292 | results_diesel_source = solph.views.node( |
||
293 | results=results, node="diesel_source" |
||
294 | ) |
||
295 | results_diesel_genset = solph.views.node( |
||
296 | results=results, node="diesel_genset" |
||
297 | ) |
||
298 | results_inverter = solph.views.node(results=results, node="inverter") |
||
299 | results_rectifier = solph.views.node(results=results, node="rectifier") |
||
300 | results_battery = solph.views.node(results=results, node="battery") |
||
301 | results_demand_el = solph.views.node( |
||
302 | results=results, node="electricity_demand" |
||
303 | ) |
||
304 | results_excess_el = solph.views.node(results=results, node="excess_el") |
||
305 | |||
306 | # -------------------- SEQUENCES (DYNAMIC) -------------------- |
||
307 | # Hourly demand profile. |
||
308 | sequences_demand = results_demand_el["sequences"][ |
||
309 | (("electricity_ac", "electricity_demand"), "flow") |
||
310 | ] |
||
311 | |||
312 | # Hourly profiles for solar potential and pv production. |
||
313 | sequences_pv = results_pv["sequences"][(("pv", "electricity_dc"), "flow")] |
||
314 | |||
315 | # Hourly profiles for diesel consumption and electricity production |
||
316 | # in the diesel genset. |
||
317 | # The 'flow' from oemof is in kWh and must be converted to |
||
318 | # kg by dividing it by the lower heating value and then to |
||
319 | # liter by dividing it by the diesel density. |
||
320 | sequences_diesel_consumption = ( |
||
321 | results_diesel_source["sequences"][ |
||
322 | (("diesel_source", "diesel"), "flow") |
||
323 | ] |
||
324 | / diesel_lhv |
||
325 | / diesel_density |
||
326 | ) |
||
327 | |||
328 | # Hourly profiles for electricity production in the diesel genset. |
||
329 | sequences_diesel_genset = results_diesel_genset["sequences"][ |
||
330 | (("diesel_genset", "electricity_ac"), "flow") |
||
331 | ] |
||
332 | |||
333 | # Hourly profiles for excess ac and dc electricity production. |
||
334 | sequences_excess = results_excess_el["sequences"][ |
||
335 | (("electricity_dc", "excess_el"), "flow") |
||
336 | ] |
||
337 | |||
338 | # -------------------- SCALARS (STATIC) -------------------- |
||
339 | capacity_diesel_genset = results_diesel_genset["scalars"][ |
||
340 | (("diesel_genset", "electricity_ac"), "invest") |
||
341 | ] |
||
342 | |||
343 | # Define a tolerance to force 'too close' numbers to the `min_load` |
||
344 | # and to 0 to be the same as the `min_load` and 0. |
||
345 | tol = 1e-8 |
||
346 | load_diesel_genset = sequences_diesel_genset / capacity_diesel_genset |
||
347 | sequences_diesel_genset[np.abs(load_diesel_genset) < tol] = 0 |
||
348 | sequences_diesel_genset[np.abs(load_diesel_genset - min_load) < tol] = ( |
||
349 | min_load * capacity_diesel_genset |
||
350 | ) |
||
351 | sequences_diesel_genset[np.abs(load_diesel_genset - max_load) < tol] = ( |
||
352 | max_load * capacity_diesel_genset |
||
353 | ) |
||
354 | |||
355 | capacity_pv = results_pv["scalars"][(("pv", "electricity_dc"), "invest")] |
||
356 | capacity_inverter = results_inverter["scalars"][ |
||
357 | (("electricity_dc", "inverter"), "invest") |
||
358 | ] |
||
359 | capacity_rectifier = results_rectifier["scalars"][ |
||
360 | (("electricity_ac", "rectifier"), "invest") |
||
361 | ] |
||
362 | capacity_battery = results_battery["scalars"][ |
||
363 | (("electricity_dc", "battery"), "invest") |
||
364 | ] |
||
365 | |||
366 | total_cost_component = ( |
||
367 | ( |
||
368 | epc_diesel_genset * capacity_diesel_genset |
||
369 | + epc_pv * capacity_pv |
||
370 | + epc_rectifier * capacity_rectifier |
||
371 | + epc_inverter * capacity_inverter |
||
372 | + epc_battery * capacity_battery |
||
373 | ) |
||
374 | * n_days |
||
375 | / n_days_in_year |
||
376 | ) |
||
377 | |||
378 | # The only componnet with the variable cost is the diesl genset |
||
379 | total_cost_variable = ( |
||
380 | variable_cost_diesel_genset * sequences_diesel_genset.sum(axis=0) |
||
381 | ) |
||
382 | |||
383 | total_cost_diesel = diesel_cost * sequences_diesel_consumption.sum(axis=0) |
||
384 | total_revenue = ( |
||
385 | total_cost_component + total_cost_variable + total_cost_diesel |
||
386 | ) |
||
387 | total_demand = sequences_demand.sum(axis=0) |
||
388 | |||
389 | # Levelized cost of electricity in the system in currency's Cent per kWh. |
||
390 | lcoe = 100 * total_revenue / total_demand |
||
391 | |||
392 | # The share of renewable energy source used to cover the demand. |
||
393 | res = ( |
||
394 | 100 |
||
395 | * sequences_pv.sum(axis=0) |
||
396 | / (sequences_diesel_genset.sum(axis=0) + sequences_pv.sum(axis=0)) |
||
397 | ) |
||
398 | |||
399 | # The amount of excess electricity (which must probably be dumped). |
||
400 | excess_rate = ( |
||
401 | 100 |
||
402 | * sequences_excess.sum(axis=0) |
||
403 | / (sequences_excess.sum(axis=0) + sequences_demand.sum(axis=0)) |
||
404 | ) |
||
405 | |||
406 | ########################################################################## |
||
407 | # Print the results in the terminal |
||
408 | ########################################################################## |
||
409 | |||
410 | print("\n" + 50 * "*") |
||
411 | print( |
||
412 | f"Simulation Time:\t {end_simulation_time-start_simulation_time:.2f} s" |
||
413 | ) |
||
414 | print(50 * "*") |
||
415 | print(f"Peak Demand:\t {sequences_demand.max():.0f} kW") |
||
416 | print(f"LCOE:\t\t {lcoe:.2f} cent/kWh") |
||
417 | print(f"RES:\t\t {res:.0f}%") |
||
418 | print(f"Excess:\t\t {excess_rate:.1f}% of the total production") |
||
419 | print(50 * "*") |
||
420 | print("Optimal Capacities:") |
||
421 | print("-------------------") |
||
422 | print(f"Diesel Genset:\t {capacity_diesel_genset:.0f} kW") |
||
423 | print(f"PV:\t\t {capacity_pv:.0f} kW") |
||
424 | print(f"Battery:\t {capacity_battery:.0f} kW") |
||
425 | print(f"Inverter:\t {capacity_inverter:.0f} kW") |
||
426 | print(f"Rectifier:\t {capacity_rectifier:.0f} kW") |
||
427 | print(50 * "*") |
||
428 | |||
429 | ########################################################################## |
||
430 | # Plot the duration curve for the diesel genset |
||
431 | ########################################################################## |
||
432 | |||
433 | if plt is not None: |
||
434 | # Create the duration curve for the diesel genset. |
||
435 | fig, ax = plt.subplots(figsize=(10, 5)) |
||
436 | |||
437 | # Sort the power generated by the diesel genset in descending order. |
||
438 | diesel_genset_duration_curve = np.sort(sequences_diesel_genset)[::-1] |
||
439 | |||
440 | percentage = ( |
||
441 | 100 |
||
442 | * np.arange(1, len(diesel_genset_duration_curve) + 1) |
||
443 | / len(diesel_genset_duration_curve) |
||
444 | ) |
||
445 | |||
446 | ax.scatter( |
||
447 | percentage, |
||
448 | diesel_genset_duration_curve, |
||
449 | color="dodgerblue", |
||
450 | marker="+", |
||
451 | ) |
||
452 | |||
453 | # Plot a horizontal line representing the minimum load |
||
454 | ax.axhline( |
||
455 | y=min_load * capacity_diesel_genset, |
||
456 | color="crimson", |
||
457 | linestyle="--", |
||
458 | ) |
||
459 | min_load_annotation_text = ( |
||
460 | f"minimum load: {min_load * capacity_diesel_genset:0.2f} kW" |
||
461 | ) |
||
462 | ax.annotate( |
||
463 | min_load_annotation_text, |
||
464 | xy=(100, min_load * capacity_diesel_genset), |
||
465 | xytext=(0, 5), |
||
466 | textcoords="offset pixels", |
||
467 | horizontalalignment="right", |
||
468 | verticalalignment="bottom", |
||
469 | ) |
||
470 | |||
471 | # Plot a horizontal line representing the maximum load |
||
472 | ax.axhline( |
||
473 | y=max_load * capacity_diesel_genset, |
||
474 | color="crimson", |
||
475 | linestyle="--", |
||
476 | ) |
||
477 | max_load_annotation_text = ( |
||
478 | f"maximum load: {max_load * capacity_diesel_genset:0.2f} kW" |
||
479 | ) |
||
480 | ax.annotate( |
||
481 | max_load_annotation_text, |
||
482 | xy=(100, max_load * capacity_diesel_genset), |
||
483 | xytext=(0, -5), |
||
484 | textcoords="offset pixels", |
||
485 | horizontalalignment="right", |
||
486 | verticalalignment="top", |
||
487 | ) |
||
488 | |||
489 | ax.set_title( |
||
490 | "Duration Curve for the Diesel Genset Electricity Production", |
||
491 | fontweight="bold", |
||
492 | ) |
||
493 | ax.set_ylabel("diesel genset production [kW]") |
||
494 | ax.set_xlabel("percentage of annual operation [%]") |
||
495 | |||
496 | # Create the second axis on the right side of the diagram |
||
497 | # representing the operation load of the diesel genset. |
||
498 | second_ax = ax.secondary_yaxis( |
||
499 | "right", |
||
500 | functions=( |
||
501 | lambda x: x / capacity_diesel_genset * 100, |
||
|
|||
502 | lambda x: x * capacity_diesel_genset / 100, |
||
503 | ), |
||
504 | ) |
||
505 | second_ax.set_ylabel("diesel genset operation load [%]") |
||
506 | |||
507 | plt.show() |
||
508 | |||
512 |