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