exerpy 0.0.3__py3-none-any.whl → 0.0.4__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.
- exerpy/__init__.py +1 -1
- exerpy/analyses.py +269 -24
- exerpy/components/heat_exchanger/base.py +2 -2
- exerpy/components/heat_exchanger/condenser.py +2 -2
- exerpy/components/turbomachinery/compressor.py +38 -10
- exerpy/components/turbomachinery/pump.py +38 -10
- exerpy/cost_estimation/__init__.py +65 -0
- exerpy/cost_estimation/turton.py +1260 -0
- exerpy/data/cost_correlations/cepci_index.json +135 -0
- exerpy/data/cost_correlations/component_mapping.json +450 -0
- exerpy/data/cost_correlations/material_factors.json +428 -0
- exerpy/data/cost_correlations/pressure_factors.json +206 -0
- exerpy/data/cost_correlations/turton2008.json +726 -0
- exerpy/data/cost_correlations/turton2008_design_analysis_synthesis_components_tables.pdf +0 -0
- exerpy/data/cost_correlations/turton2008_design_analysis_synthesis_components_theory.pdf +0 -0
- exerpy/functions.py +1 -1
- exerpy/parser/from_tespy/tespy_config.py +1 -0
- {exerpy-0.0.3.dist-info → exerpy-0.0.4.dist-info}/METADATA +2 -2
- {exerpy-0.0.3.dist-info → exerpy-0.0.4.dist-info}/RECORD +21 -12
- {exerpy-0.0.3.dist-info → exerpy-0.0.4.dist-info}/WHEEL +0 -0
- {exerpy-0.0.3.dist-info → exerpy-0.0.4.dist-info}/licenses/LICENSE +0 -0
exerpy/__init__.py
CHANGED
exerpy/analyses.py
CHANGED
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
5
6
|
import numpy as np
|
|
6
7
|
import pandas as pd
|
|
7
8
|
from tabulate import tabulate
|
|
@@ -71,6 +72,10 @@ class ExergyAnalysis:
|
|
|
71
72
|
Creates an instance from a JSON file containing system data.
|
|
72
73
|
exergy_results(print_results=True)
|
|
73
74
|
Displays and returns tables of exergy analysis results.
|
|
75
|
+
plot_exergy_waterfall(title=None, figsize=(12, 10), exclude_components=None, show_plot=True)
|
|
76
|
+
Creates an exergy destruction waterfall diagram visualizing exergy flow through the system.
|
|
77
|
+
print_exergy_summary()
|
|
78
|
+
Prints a concise text summary of the exergy analysis results.
|
|
74
79
|
export_to_json(output_path)
|
|
75
80
|
Exports the model and analysis results to a JSON file.
|
|
76
81
|
_serialize()
|
|
@@ -193,6 +198,13 @@ class ExergyAnalysis:
|
|
|
193
198
|
else:
|
|
194
199
|
# Calculate E_F, E_D, E_P
|
|
195
200
|
component.calc_exergy_balance(self.Tamb, self.pamb, self.split_physical_exergy)
|
|
201
|
+
|
|
202
|
+
# Update is_dissipative flag based on actual E_P value for Valve components
|
|
203
|
+
# This is needed because when split_physical_exergy=False, valves may become
|
|
204
|
+
# dissipative (E_P = nan) even if not initially marked as such based on temperatures
|
|
205
|
+
if component.__class__.__name__ == "Valve" and np.isnan(component.E_P):
|
|
206
|
+
component.is_dissipative = True
|
|
207
|
+
|
|
196
208
|
# Safely calculate y and y* avoiding division by zero
|
|
197
209
|
if self.E_F != 0:
|
|
198
210
|
component.y = component.E_D / self.E_F
|
|
@@ -563,6 +575,214 @@ class ExergyAnalysis:
|
|
|
563
575
|
|
|
564
576
|
return df_component_results, df_material_connection_results, df_non_material_connection_results
|
|
565
577
|
|
|
578
|
+
def plot_exergy_waterfall(self, title=None, figsize=(12, 10), exclude_components=None, show_plot=True):
|
|
579
|
+
"""
|
|
580
|
+
Create an exergy destruction waterfall diagram.
|
|
581
|
+
|
|
582
|
+
This method visualizes the exergy flow through the system as a waterfall chart,
|
|
583
|
+
showing how exergy is destroyed in each component from the exergetic fuel (100%)
|
|
584
|
+
down to the exergetic product and losses.
|
|
585
|
+
|
|
586
|
+
Parameters
|
|
587
|
+
----------
|
|
588
|
+
title : str, optional
|
|
589
|
+
Title for the plot. If None, no title is displayed.
|
|
590
|
+
figsize : tuple, optional
|
|
591
|
+
Figure size as (width, height) in inches. Default is (12, 10).
|
|
592
|
+
exclude_components : list, optional
|
|
593
|
+
List of component names to exclude from the diagram.
|
|
594
|
+
By default, all components with NaN E_F (Exergetic Fuel) are excluded,
|
|
595
|
+
as well as CycleCloser and PowerBus components.
|
|
596
|
+
show_plot : bool, optional
|
|
597
|
+
Whether to display the plot immediately. Default is True.
|
|
598
|
+
|
|
599
|
+
Returns
|
|
600
|
+
-------
|
|
601
|
+
fig : matplotlib.figure.Figure
|
|
602
|
+
The figure object containing the waterfall diagram.
|
|
603
|
+
ax : matplotlib.axes.Axes
|
|
604
|
+
The axes object of the waterfall diagram.
|
|
605
|
+
|
|
606
|
+
Raises
|
|
607
|
+
------
|
|
608
|
+
RuntimeError
|
|
609
|
+
If the exergy analysis has not been performed yet (analyse() not called).
|
|
610
|
+
|
|
611
|
+
Notes
|
|
612
|
+
-----
|
|
613
|
+
- The waterfall diagram displays exergy values as percentages of the total fuel exergy.
|
|
614
|
+
- Components are sorted by their exergy destruction rate (y [%]) in descending order.
|
|
615
|
+
- Each bar represents the remaining exergy after destruction in that component.
|
|
616
|
+
- Red bars indicate exergy destruction in components.
|
|
617
|
+
- Blue bar represents the initial exergetic fuel (100%).
|
|
618
|
+
- Green bar represents the final exergetic product.
|
|
619
|
+
|
|
620
|
+
Examples
|
|
621
|
+
--------
|
|
622
|
+
>>> analysis = ExergyAnalysis.from_tespy(network, Tamb=288.15, pamb=101325) # doctest: +SKIP
|
|
623
|
+
>>> analysis.analyse(E_F={'inputs': ['fuel']}, E_P={'outputs': ['power']}) # doctest: +SKIP
|
|
624
|
+
>>> fig, ax = analysis.plot_exergy_waterfall(title='Power Plant Exergy Waterfall') # doctest: +SKIP
|
|
625
|
+
>>> fig.savefig('exergy_waterfall.pdf') # doctest: +SKIP
|
|
626
|
+
|
|
627
|
+
See Also
|
|
628
|
+
--------
|
|
629
|
+
exergy_results : Display tabular exergy analysis results.
|
|
630
|
+
print_exergy_summary : Print a text summary of exergy analysis.
|
|
631
|
+
"""
|
|
632
|
+
# Check if analysis has been performed
|
|
633
|
+
if not hasattr(self, "epsilon") or self.epsilon is None:
|
|
634
|
+
raise RuntimeError("Exergy analysis has not been performed yet. Please call analyse() first.")
|
|
635
|
+
|
|
636
|
+
# Get component results without printing
|
|
637
|
+
df_component_results, _, _ = self.exergy_results(print_results=False)
|
|
638
|
+
|
|
639
|
+
# Default exclusions: empty list, but filter for valid E_F
|
|
640
|
+
if exclude_components is None:
|
|
641
|
+
exclude_components = []
|
|
642
|
+
|
|
643
|
+
# Get total values from df_component_results
|
|
644
|
+
total_row = df_component_results[df_component_results["Component"] == "TOT"].iloc[0]
|
|
645
|
+
epsilon_total = total_row["ε [%]"]
|
|
646
|
+
E_L_total = total_row["E_L [kW]"]
|
|
647
|
+
E_F_total = total_row["E_F [kW]"]
|
|
648
|
+
exergetic_loss_percent = (E_L_total / E_F_total) * 100 if E_F_total != 0 else 0
|
|
649
|
+
|
|
650
|
+
# Filter components (exclude TOT, components with NaN E_F, and specified components)
|
|
651
|
+
component_data = df_component_results[
|
|
652
|
+
(df_component_results["Component"] != "TOT")
|
|
653
|
+
& (df_component_results["E_F [kW]"].notna())
|
|
654
|
+
& (~df_component_results["Component"].isin(exclude_components))
|
|
655
|
+
& (df_component_results["y [%]"].notna())
|
|
656
|
+
].copy()
|
|
657
|
+
|
|
658
|
+
# Sort by y [%] in descending order
|
|
659
|
+
component_data = component_data.sort_values("y [%]", ascending=False)
|
|
660
|
+
|
|
661
|
+
# Create bar values: Start at 100%, decrease by each component's y [%]
|
|
662
|
+
bar_values = [100.0]
|
|
663
|
+
current_value = 100.0
|
|
664
|
+
for y in component_data["y [%]"]:
|
|
665
|
+
current_value -= y
|
|
666
|
+
bar_values.append(current_value)
|
|
667
|
+
bar_values.append(epsilon_total) # Final bar is the exergetic product
|
|
668
|
+
|
|
669
|
+
# Create labels for spaces between bars
|
|
670
|
+
space_labels = ["Exergetic fuel"] + list(component_data["Component"]) + ["Exergetic loss", "Exergetic product"]
|
|
671
|
+
|
|
672
|
+
# Create the figure
|
|
673
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
674
|
+
|
|
675
|
+
# Number of bars and spaces
|
|
676
|
+
n_bars = len(bar_values)
|
|
677
|
+
|
|
678
|
+
# Create horizontal bars at positions 0, 1, 2, ..., n_bars-1
|
|
679
|
+
bar_positions = np.arange(n_bars)
|
|
680
|
+
bar_colors = ["#1565C0"] + ["#D32F2F"] * (n_bars - 2) + ["#2E7D32"]
|
|
681
|
+
# Blue for fuel, red for destruction, green for product
|
|
682
|
+
|
|
683
|
+
for i, (pos, value, color) in enumerate(zip(bar_positions, bar_values, bar_colors, strict=False)):
|
|
684
|
+
ax.barh(pos, value, color=color, alpha=0.8, height=0.6)
|
|
685
|
+
# Add value label inside the bar on the right
|
|
686
|
+
ax.text(
|
|
687
|
+
value - 2, pos, f"{value:.2f}%", va="center", ha="right", fontsize=9, fontweight="bold", color="white"
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
# Add space labels between bars
|
|
691
|
+
# Space positions: between bars, so at 0.5, 1.5, 2.5, ..., and above/below
|
|
692
|
+
space_positions = [-0.5] + [i + 0.5 for i in range(n_bars - 1)] + [n_bars - 0.5]
|
|
693
|
+
|
|
694
|
+
for i, (space_pos, label) in enumerate(zip(space_positions, space_labels, strict=False)):
|
|
695
|
+
if i == 0: # Exergetic fuel - above first bar
|
|
696
|
+
ax.text(2, space_pos, label, va="center", ha="left", fontsize=10, fontweight="bold", style="italic")
|
|
697
|
+
elif i == len(space_labels) - 2: # Exergetic loss
|
|
698
|
+
loss_label = f"{label} (-{exergetic_loss_percent:.2f}%)"
|
|
699
|
+
ax.text(
|
|
700
|
+
2, space_pos, loss_label, va="center", ha="left", fontsize=10, fontweight="bold", style="italic"
|
|
701
|
+
)
|
|
702
|
+
elif i == len(space_labels) - 1: # Exergetic product - below last bar
|
|
703
|
+
ax.text(2, space_pos, label, va="center", ha="left", fontsize=10, fontweight="bold", style="italic")
|
|
704
|
+
else: # Component labels
|
|
705
|
+
component_idx = i - 1
|
|
706
|
+
destruction_rate = component_data.iloc[component_idx]["y [%]"]
|
|
707
|
+
label_with_rate = f"{label} (-{destruction_rate:.2f}%)"
|
|
708
|
+
ax.text(2, space_pos, label_with_rate, va="center", ha="left", fontsize=10, fontweight="bold")
|
|
709
|
+
|
|
710
|
+
# Customize plot
|
|
711
|
+
ax.set_yticks(bar_positions)
|
|
712
|
+
ax.set_yticklabels([""] * n_bars) # Empty labels since we have custom labels
|
|
713
|
+
ax.set_xlabel("Exergy [%]", fontsize=12, fontweight="bold")
|
|
714
|
+
|
|
715
|
+
if title is not None:
|
|
716
|
+
ax.set_title(title, fontsize=14, fontweight="bold", pad=20)
|
|
717
|
+
|
|
718
|
+
ax.set_xlim(0, 100)
|
|
719
|
+
ax.set_ylim(-1, n_bars)
|
|
720
|
+
ax.grid(axis="x", alpha=0.3, linestyle="--")
|
|
721
|
+
ax.axvline(x=0, color="black", linewidth=0.8)
|
|
722
|
+
ax.invert_yaxis() # Invert so highest bar is at top
|
|
723
|
+
|
|
724
|
+
plt.tight_layout()
|
|
725
|
+
|
|
726
|
+
if show_plot:
|
|
727
|
+
plt.show()
|
|
728
|
+
|
|
729
|
+
return fig, ax
|
|
730
|
+
|
|
731
|
+
def print_exergy_summary(self):
|
|
732
|
+
"""
|
|
733
|
+
Print a text summary of the exergy analysis results.
|
|
734
|
+
|
|
735
|
+
This method provides a concise summary of the overall system exergy performance,
|
|
736
|
+
including fuel exergy, total destruction, losses, and efficiency.
|
|
737
|
+
|
|
738
|
+
Raises
|
|
739
|
+
------
|
|
740
|
+
RuntimeError
|
|
741
|
+
If the exergy analysis has not been performed yet (analyse() not called).
|
|
742
|
+
|
|
743
|
+
Notes
|
|
744
|
+
-----
|
|
745
|
+
The summary includes:
|
|
746
|
+
- Exergetic Fuel: Normalized to 100%
|
|
747
|
+
- Total Exergy Destruction: Sum of all component exergy destructions as % of fuel
|
|
748
|
+
- Exergetic Loss: Exergy losses to the environment as % of fuel
|
|
749
|
+
- Exergetic Product (ε): Overall system exergy efficiency as %
|
|
750
|
+
|
|
751
|
+
Examples
|
|
752
|
+
--------
|
|
753
|
+
>>> analysis = ExergyAnalysis.from_tespy(network, Tamb=288.15, pamb=101325) # doctest: +SKIP
|
|
754
|
+
>>> analysis.analyse(E_F={'inputs': ['fuel']}, E_P={'outputs': ['power']}) # doctest: +SKIP
|
|
755
|
+
>>> analysis.print_exergy_summary() # doctest: +SKIP
|
|
756
|
+
Exergy Analysis Summary:
|
|
757
|
+
Exergetic Fuel: 100.00%
|
|
758
|
+
Total Exergy Destruction: 35.42%
|
|
759
|
+
Exergetic Loss: 5.12%
|
|
760
|
+
Exergetic Product (ε): 59.46%
|
|
761
|
+
|
|
762
|
+
See Also
|
|
763
|
+
--------
|
|
764
|
+
exergy_results : Display detailed tabular results.
|
|
765
|
+
plot_exergy_waterfall : Create a visual waterfall diagram.
|
|
766
|
+
"""
|
|
767
|
+
# Check if analysis has been performed
|
|
768
|
+
if not hasattr(self, "epsilon") or self.epsilon is None:
|
|
769
|
+
raise RuntimeError("Exergy analysis has not been performed yet. Please call analyse() first.")
|
|
770
|
+
|
|
771
|
+
# Get component results without printing
|
|
772
|
+
df_component_results, _, _ = self.exergy_results(print_results=False)
|
|
773
|
+
|
|
774
|
+
total_row = df_component_results[df_component_results["Component"] == "TOT"].iloc[0]
|
|
775
|
+
epsilon_total = total_row["ε [%]"]
|
|
776
|
+
E_L_total = total_row["E_L [kW]"]
|
|
777
|
+
E_F_total = total_row["E_F [kW]"]
|
|
778
|
+
exergetic_loss_percent = (E_L_total / E_F_total) * 100 if E_F_total != 0 else 0
|
|
779
|
+
|
|
780
|
+
print("\nExergy Analysis Summary:")
|
|
781
|
+
print("Exergetic Fuel: 100.00%")
|
|
782
|
+
print(f"Total Exergy Destruction: {100 - epsilon_total - exergetic_loss_percent:.2f}%")
|
|
783
|
+
print(f"Exergetic Loss: {exergetic_loss_percent:.2f}%")
|
|
784
|
+
print(f"Exergetic Product (epsilon): {epsilon_total:.2f}%")
|
|
785
|
+
|
|
566
786
|
def export_to_json(self, output_path):
|
|
567
787
|
"""
|
|
568
788
|
Export the model to a JSON file.
|
|
@@ -581,6 +801,7 @@ class ExergyAnalysis:
|
|
|
581
801
|
-----
|
|
582
802
|
This method serializes the model using the internal _serialize method
|
|
583
803
|
and writes the resulting data to a JSON file with indentation.
|
|
804
|
+
NaN values are converted to null for valid JSON output.
|
|
584
805
|
"""
|
|
585
806
|
data = self._serialize()
|
|
586
807
|
with open(output_path, "w") as json_file:
|
|
@@ -610,21 +831,21 @@ class ExergyAnalysis:
|
|
|
610
831
|
for comp_name, comp_data in comps.items():
|
|
611
832
|
comp = self.components[comp_name]
|
|
612
833
|
comp_data["exergy_results"] = {
|
|
613
|
-
"E_F": getattr(comp, "E_F", None),
|
|
614
|
-
"E_P": getattr(comp, "E_P", None),
|
|
615
|
-
"E_D": getattr(comp, "E_D", None),
|
|
616
|
-
"epsilon": getattr(comp, "epsilon", None),
|
|
617
|
-
"y": getattr(comp, "y", None),
|
|
618
|
-
"y_star": getattr(comp, "y_star", None),
|
|
834
|
+
"E_F": _nan_to_none(getattr(comp, "E_F", None)),
|
|
835
|
+
"E_P": _nan_to_none(getattr(comp, "E_P", None)),
|
|
836
|
+
"E_D": _nan_to_none(getattr(comp, "E_D", None)),
|
|
837
|
+
"epsilon": _nan_to_none(getattr(comp, "epsilon", None)),
|
|
838
|
+
"y": _nan_to_none(getattr(comp, "y", None)),
|
|
839
|
+
"y_star": _nan_to_none(getattr(comp, "y_star", None)),
|
|
619
840
|
}
|
|
620
841
|
|
|
621
842
|
# add overall system exergy results
|
|
622
843
|
export["system_results"] = {
|
|
623
|
-
"E_F": getattr(self, "E_F", None),
|
|
624
|
-
"E_P": getattr(self, "E_P", None),
|
|
625
|
-
"E_D": getattr(self, "E_D", None),
|
|
626
|
-
"E_L": getattr(self, "E_L", None),
|
|
627
|
-
"epsilon": getattr(self, "epsilon", None),
|
|
844
|
+
"E_F": _nan_to_none(getattr(self, "E_F", None)),
|
|
845
|
+
"E_P": _nan_to_none(getattr(self, "E_P", None)),
|
|
846
|
+
"E_D": _nan_to_none(getattr(self, "E_D", None)),
|
|
847
|
+
"E_L": _nan_to_none(getattr(self, "E_L", None)),
|
|
848
|
+
"epsilon": _nan_to_none(getattr(self, "epsilon", None)),
|
|
628
849
|
}
|
|
629
850
|
|
|
630
851
|
return export
|
|
@@ -748,6 +969,13 @@ def _construct_components(component_data, connection_data, Tamb):
|
|
|
748
969
|
return components # Return the dictionary of created components
|
|
749
970
|
|
|
750
971
|
|
|
972
|
+
def _nan_to_none(value):
|
|
973
|
+
"""Convert NaN/Inf floats to None for valid JSON serialization."""
|
|
974
|
+
if isinstance(value, float) and not np.isfinite(value):
|
|
975
|
+
return None
|
|
976
|
+
return value
|
|
977
|
+
|
|
978
|
+
|
|
751
979
|
def _load_json(json_path):
|
|
752
980
|
"""
|
|
753
981
|
Load and validate a JSON file.
|
|
@@ -1184,7 +1412,15 @@ class ExergoeconomicAnalysis:
|
|
|
1184
1412
|
self.equations[counter] = {"kind": "boundary", "object": [name], "property": "c_TOT"}
|
|
1185
1413
|
counter += 1
|
|
1186
1414
|
else:
|
|
1187
|
-
|
|
1415
|
+
# When there are power outlets, we still need at least one boundary condition
|
|
1416
|
+
# for power inlets to fix the absolute cost value. The aux_power_eq only
|
|
1417
|
+
# equalizes specific costs but doesn't set the actual value.
|
|
1418
|
+
if conn.get("C_TOT"):
|
|
1419
|
+
idx = conn["CostVar_index"]["exergy"]
|
|
1420
|
+
self._A[counter, idx] = 1
|
|
1421
|
+
self._b[counter] = conn.get("C_TOT", 0)
|
|
1422
|
+
self.equations[counter] = {"kind": "boundary", "object": [name], "property": "c_TOT"}
|
|
1423
|
+
counter += 1
|
|
1188
1424
|
|
|
1189
1425
|
# 3. Auxiliary equations for the equality of the specific costs
|
|
1190
1426
|
# of all power flows at the input or output of the system.
|
|
@@ -1320,26 +1556,26 @@ class ExergoeconomicAnalysis:
|
|
|
1320
1556
|
E_M = m_val * e_M # mechanical exergy flow [kW]
|
|
1321
1557
|
|
|
1322
1558
|
conn["C_T"] = C_solution[conn["CostVar_index"]["T"]]
|
|
1323
|
-
conn["c_T"] = conn["C_T"] / E_T if E_T != 0 else
|
|
1559
|
+
conn["c_T"] = conn["C_T"] / E_T if E_T != 0 else 0.0
|
|
1324
1560
|
|
|
1325
1561
|
conn["C_M"] = C_solution[conn["CostVar_index"]["M"]]
|
|
1326
|
-
conn["c_M"] = conn["C_M"] / E_M if E_M != 0 else
|
|
1562
|
+
conn["c_M"] = conn["C_M"] / E_M if E_M != 0 else 0.0
|
|
1327
1563
|
|
|
1328
1564
|
conn["C_PH"] = conn["C_T"] + conn["C_M"]
|
|
1329
|
-
conn["c_PH"] = conn["C_PH"] / (E_T + E_M) if (E_T + E_M) != 0 else
|
|
1565
|
+
conn["c_PH"] = conn["C_PH"] / (E_T + E_M) if (E_T + E_M) != 0 else 0.0
|
|
1330
1566
|
|
|
1331
1567
|
if self.chemical_exergy_enabled:
|
|
1332
1568
|
e_CH = conn.get("e_CH", 0) # chemical specific exergy [kJ/kg]
|
|
1333
1569
|
E_CH = m_val * e_CH # chemical exergy flow [kW]
|
|
1334
1570
|
conn["C_CH"] = C_solution[conn["CostVar_index"]["CH"]]
|
|
1335
|
-
conn["c_CH"] = conn["C_CH"] / E_CH if E_CH != 0 else
|
|
1571
|
+
conn["c_CH"] = conn["C_CH"] / E_CH if E_CH != 0 else 0.0
|
|
1336
1572
|
conn["C_TOT"] = conn["C_T"] + conn["C_M"] + conn["C_CH"]
|
|
1337
1573
|
total_E = E_T + E_M + E_CH
|
|
1338
|
-
conn["c_TOT"] = conn["C_TOT"] / total_E if total_E != 0 else
|
|
1574
|
+
conn["c_TOT"] = conn["C_TOT"] / total_E if total_E != 0 else 0.0
|
|
1339
1575
|
else:
|
|
1340
1576
|
conn["C_TOT"] = conn["C_T"] + conn["C_M"]
|
|
1341
1577
|
total_E = E_T + E_M
|
|
1342
|
-
conn["c_TOT"] = conn["C_TOT"] / total_E if total_E != 0 else
|
|
1578
|
+
conn["c_TOT"] = conn["C_TOT"] / total_E if total_E != 0 else 0.0
|
|
1343
1579
|
elif kind in {"heat", "power"}:
|
|
1344
1580
|
conn["C_TOT"] = C_solution[conn["CostVar_index"]["exergy"]]
|
|
1345
1581
|
conn["c_TOT"] = conn["C_TOT"] / conn.get("E", 1)
|
|
@@ -1465,7 +1701,7 @@ class ExergoeconomicAnalysis:
|
|
|
1465
1701
|
dict
|
|
1466
1702
|
Mapping from component name to tuple (balance, is_balanced),
|
|
1467
1703
|
where balance is the residual and is_balanced is True if
|
|
1468
|
-
|
|
1704
|
+
abs(balance) <= tol.
|
|
1469
1705
|
"""
|
|
1470
1706
|
from .components.helpers.cycle_closer import CycleCloser
|
|
1471
1707
|
|
|
@@ -1476,10 +1712,20 @@ class ExergoeconomicAnalysis:
|
|
|
1476
1712
|
inlet_sum = 0.0
|
|
1477
1713
|
outlet_sum = 0.0
|
|
1478
1714
|
for conn in self.connections.values():
|
|
1715
|
+
# Use sum of cost components instead of C_TOT, because C_TOT may have been
|
|
1716
|
+
# modified by loss attribution (Step 6) which is not part of the matrix equation.
|
|
1717
|
+
kind = conn.get("kind", "material")
|
|
1718
|
+
if kind == "material":
|
|
1719
|
+
cost = (conn.get("C_T", 0) or 0) + (conn.get("C_M", 0) or 0)
|
|
1720
|
+
if self.chemical_exergy_enabled:
|
|
1721
|
+
cost += conn.get("C_CH", 0) or 0
|
|
1722
|
+
else:
|
|
1723
|
+
# For heat/power streams, use C_TOT directly (no loss attribution applies)
|
|
1724
|
+
cost = conn.get("C_TOT", 0) or 0
|
|
1479
1725
|
if conn.get("target_component") == name:
|
|
1480
|
-
inlet_sum +=
|
|
1726
|
+
inlet_sum += cost
|
|
1481
1727
|
if conn.get("source_component") == name:
|
|
1482
|
-
outlet_sum +=
|
|
1728
|
+
outlet_sum += cost
|
|
1483
1729
|
comp.C_in = inlet_sum
|
|
1484
1730
|
comp.C_out = outlet_sum
|
|
1485
1731
|
z_cost = getattr(comp, "Z_costs", 0)
|
|
@@ -1489,11 +1735,11 @@ class ExergoeconomicAnalysis:
|
|
|
1489
1735
|
|
|
1490
1736
|
all_ok = all(flag for _, flag in balances.values())
|
|
1491
1737
|
if all_ok:
|
|
1492
|
-
|
|
1738
|
+
logging.info("All component cost balances are fulfilled.")
|
|
1493
1739
|
else:
|
|
1494
1740
|
for name, (bal, ok) in balances.items():
|
|
1495
1741
|
if not ok:
|
|
1496
|
-
|
|
1742
|
+
logging.warning(f"Balance for component {name} not fulfilled: residual = {bal:.6f}")
|
|
1497
1743
|
|
|
1498
1744
|
return balances
|
|
1499
1745
|
|
|
@@ -1522,7 +1768,6 @@ class ExergoeconomicAnalysis:
|
|
|
1522
1768
|
self.solve_exergoeconomic_analysis(Tamb)
|
|
1523
1769
|
logging.info("Exergoeconomic analysis completed successfully.")
|
|
1524
1770
|
self.check_cost_balance()
|
|
1525
|
-
print("stop")
|
|
1526
1771
|
|
|
1527
1772
|
def print_equations(self):
|
|
1528
1773
|
"""
|
|
@@ -498,8 +498,8 @@ class HeatExchanger(Component):
|
|
|
498
498
|
# Equality equation for mechanical and chemical exergy costs.
|
|
499
499
|
def set_equal(A, row, in_item, out_item, var):
|
|
500
500
|
if in_item["e_" + var] != 0 and out_item["e_" + var] != 0:
|
|
501
|
-
A[row, in_item["CostVar_index"][var]] = 1 / in_item["
|
|
502
|
-
A[row, out_item["CostVar_index"][var]] = -1 / out_item["
|
|
501
|
+
A[row, in_item["CostVar_index"][var]] = 1 / in_item["E_" + var]
|
|
502
|
+
A[row, out_item["CostVar_index"][var]] = -1 / out_item["E_" + var]
|
|
503
503
|
elif in_item["e_" + var] == 0 and out_item["e_" + var] != 0:
|
|
504
504
|
A[row, in_item["CostVar_index"][var]] = 1
|
|
505
505
|
elif in_item["e_" + var] != 0 and out_item["e_" + var] == 0:
|
|
@@ -221,8 +221,8 @@ class Condenser(Component):
|
|
|
221
221
|
# Equality equation for mechanical and chemical exergy costs.
|
|
222
222
|
def set_equal(A, row, in_item, out_item, var):
|
|
223
223
|
if in_item["e_" + var] != 0 and out_item["e_" + var] != 0:
|
|
224
|
-
A[row, in_item["CostVar_index"][var]] = 1 / in_item["
|
|
225
|
-
A[row, out_item["CostVar_index"][var]] = -1 / out_item["
|
|
224
|
+
A[row, in_item["CostVar_index"][var]] = 1 / in_item["E_" + var]
|
|
225
|
+
A[row, out_item["CostVar_index"][var]] = -1 / out_item["E_" + var]
|
|
226
226
|
elif in_item["e_" + var] == 0 and out_item["e_" + var] != 0:
|
|
227
227
|
A[row, in_item["CostVar_index"][var]] = 1
|
|
228
228
|
elif in_item["e_" + var] != 0 and out_item["e_" + var] == 0:
|
|
@@ -269,17 +269,45 @@ class Compressor(Component):
|
|
|
269
269
|
else:
|
|
270
270
|
logging.warning("Case where thermal or mechanical exergy difference is zero is not implemented.")
|
|
271
271
|
elif self.inl[0]["T"] <= T0 and self.outl[0]["T"] > T0:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
"
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
272
|
+
# Case 2: Inlet at/below ambient, outlet above ambient
|
|
273
|
+
# Handle potential zero values for robustness
|
|
274
|
+
if self.outl[0]["e_T"] != 0 and dEM != 0:
|
|
275
|
+
A[row_index, self.outl[0]["CostVar_index"]["T"]] = 1 / self.outl[0]["E_T"]
|
|
276
|
+
A[row_index, self.inl[0]["CostVar_index"]["M"]] = 1 / dEM
|
|
277
|
+
A[row_index, self.outl[0]["CostVar_index"]["M"]] = -1 / dEM
|
|
278
|
+
equations[row_index] = {
|
|
279
|
+
"kind": "aux_p_rule",
|
|
280
|
+
"objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
|
|
281
|
+
"property": "c_T, c_M",
|
|
282
|
+
}
|
|
283
|
+
else:
|
|
284
|
+
logging.warning(
|
|
285
|
+
f"Compressor '{self.name}' Case 2: outlet thermal exergy or mechanical exergy "
|
|
286
|
+
"difference is zero, auxiliary equation may be degenerate."
|
|
287
|
+
)
|
|
288
|
+
# Fallback: set identity equation for thermal cost
|
|
289
|
+
A[row_index, self.outl[0]["CostVar_index"]["T"]] = 1
|
|
290
|
+
equations[row_index] = {
|
|
291
|
+
"kind": "aux_p_rule",
|
|
292
|
+
"objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
|
|
293
|
+
"property": "c_T",
|
|
294
|
+
}
|
|
280
295
|
else:
|
|
281
|
-
|
|
282
|
-
|
|
296
|
+
# Case 3: Both temperatures at or below ambient - apply F-rule for thermal exergy
|
|
297
|
+
# Handle zero thermal exergy cases to avoid division by zero
|
|
298
|
+
if self.inl[0]["e_T"] != 0 and self.outl[0]["e_T"] != 0:
|
|
299
|
+
A[row_index, self.inl[0]["CostVar_index"]["T"]] = -1 / self.inl[0]["E_T"]
|
|
300
|
+
A[row_index, self.outl[0]["CostVar_index"]["T"]] = 1 / self.outl[0]["E_T"]
|
|
301
|
+
elif self.inl[0]["e_T"] == 0 and self.outl[0]["e_T"] != 0:
|
|
302
|
+
# Inlet thermal exergy is zero, constrain C_T_in = 0
|
|
303
|
+
A[row_index, self.inl[0]["CostVar_index"]["T"]] = 1
|
|
304
|
+
elif self.inl[0]["e_T"] != 0 and self.outl[0]["e_T"] == 0:
|
|
305
|
+
# Outlet thermal exergy is zero, constrain C_T_out = 0
|
|
306
|
+
A[row_index, self.outl[0]["CostVar_index"]["T"]] = 1
|
|
307
|
+
else:
|
|
308
|
+
# Both thermal exergies are zero, set identity equation
|
|
309
|
+
A[row_index, self.inl[0]["CostVar_index"]["T"]] = 1
|
|
310
|
+
A[row_index, self.outl[0]["CostVar_index"]["T"]] = -1
|
|
283
311
|
equations[row_index] = {
|
|
284
312
|
"kind": "aux_f_rule",
|
|
285
313
|
"objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
|
|
@@ -262,17 +262,45 @@ class Pump(Component):
|
|
|
262
262
|
else:
|
|
263
263
|
logging.warning("Case where thermal or mechanical exergy difference is zero is not implemented.")
|
|
264
264
|
elif self.inl[0]["T"] <= T0 and self.outl[0]["T"] > T0:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
265
|
+
# Case 2: Inlet at/below ambient, outlet above ambient
|
|
266
|
+
# Handle potential zero values for robustness
|
|
267
|
+
if self.outl[0]["e_T"] != 0 and dEM != 0:
|
|
268
|
+
A[row_index, self.outl[0]["CostVar_index"]["T"]] = 1 / self.outl[0]["E_T"]
|
|
269
|
+
A[row_index, self.inl[0]["CostVar_index"]["M"]] = 1 / dEM
|
|
270
|
+
A[row_index, self.outl[0]["CostVar_index"]["M"]] = -1 / dEM
|
|
271
|
+
equations[row_index] = {
|
|
272
|
+
"kind": "aux_p_rule",
|
|
273
|
+
"objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
|
|
274
|
+
"property": "c_T, c_M",
|
|
275
|
+
}
|
|
276
|
+
else:
|
|
277
|
+
logging.warning(
|
|
278
|
+
f"Pump '{self.name}' Case 2: outlet thermal exergy or mechanical exergy "
|
|
279
|
+
"difference is zero, auxiliary equation may be degenerate."
|
|
280
|
+
)
|
|
281
|
+
# Fallback: set identity equation for thermal cost
|
|
282
|
+
A[row_index, self.outl[0]["CostVar_index"]["T"]] = 1
|
|
283
|
+
equations[row_index] = {
|
|
284
|
+
"kind": "aux_p_rule",
|
|
285
|
+
"objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
|
|
286
|
+
"property": "c_T",
|
|
287
|
+
}
|
|
273
288
|
else:
|
|
274
|
-
|
|
275
|
-
|
|
289
|
+
# Case 3: Both temperatures at or below ambient - apply F-rule for thermal exergy
|
|
290
|
+
# Handle zero thermal exergy cases to avoid division by zero
|
|
291
|
+
if self.inl[0]["e_T"] != 0 and self.outl[0]["e_T"] != 0:
|
|
292
|
+
A[row_index, self.inl[0]["CostVar_index"]["T"]] = -1 / self.inl[0]["E_T"]
|
|
293
|
+
A[row_index, self.outl[0]["CostVar_index"]["T"]] = 1 / self.outl[0]["E_T"]
|
|
294
|
+
elif self.inl[0]["e_T"] == 0 and self.outl[0]["e_T"] != 0:
|
|
295
|
+
# Inlet thermal exergy is zero, constrain C_T_in = 0
|
|
296
|
+
A[row_index, self.inl[0]["CostVar_index"]["T"]] = 1
|
|
297
|
+
elif self.inl[0]["e_T"] != 0 and self.outl[0]["e_T"] == 0:
|
|
298
|
+
# Outlet thermal exergy is zero, constrain C_T_out = 0
|
|
299
|
+
A[row_index, self.outl[0]["CostVar_index"]["T"]] = 1
|
|
300
|
+
else:
|
|
301
|
+
# Both thermal exergies are zero, set identity equation
|
|
302
|
+
A[row_index, self.inl[0]["CostVar_index"]["T"]] = 1
|
|
303
|
+
A[row_index, self.outl[0]["CostVar_index"]["T"]] = -1
|
|
276
304
|
equations[row_index] = {
|
|
277
305
|
"kind": "aux_f_rule",
|
|
278
306
|
"objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cost estimation module for ExerPy.
|
|
3
|
+
|
|
4
|
+
This module provides cost estimation functionality for exergoeconomic analysis.
|
|
5
|
+
Different cost estimation methods can be implemented as separate classes.
|
|
6
|
+
|
|
7
|
+
Available Estimators
|
|
8
|
+
--------------------
|
|
9
|
+
TurtonCostEstimator : Turton 2008 correlations (requires proprietary data files)
|
|
10
|
+
Uses correlations from "Analysis, Synthesis, and Design of Chemical Processes"
|
|
11
|
+
by Turton et al. (2008) with CEPCI cost index adjustment.
|
|
12
|
+
|
|
13
|
+
Notes
|
|
14
|
+
-----
|
|
15
|
+
The TurtonCostEstimator requires proprietary data files that are not distributed
|
|
16
|
+
with ExerPy due to copyright restrictions. Users can:
|
|
17
|
+
|
|
18
|
+
1. Provide their own cost data files in the expected format
|
|
19
|
+
2. Provide costs directly to ExergoeconomicAnalysis.run() without using an estimator
|
|
20
|
+
3. Implement their own cost estimator class
|
|
21
|
+
|
|
22
|
+
Example of providing costs directly::
|
|
23
|
+
|
|
24
|
+
execo = ExergoeconomicAnalysis(exergy_analysis)
|
|
25
|
+
costs = {
|
|
26
|
+
"pump_1_Z": 5.0, # EUR/h
|
|
27
|
+
"hx_1_Z": 12.0, # EUR/h
|
|
28
|
+
"fuel_in_c": 3.5, # EUR/GJ for input stream
|
|
29
|
+
}
|
|
30
|
+
execo.run(costs, Tamb=298.15)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
from .turton import TurtonCostEstimator
|
|
35
|
+
|
|
36
|
+
__all__ = ["TurtonCostEstimator"]
|
|
37
|
+
except ImportError:
|
|
38
|
+
__all__ = []
|
|
39
|
+
|
|
40
|
+
class TurtonCostEstimator:
|
|
41
|
+
"""
|
|
42
|
+
Placeholder for TurtonCostEstimator when data files are not available.
|
|
43
|
+
|
|
44
|
+
The actual TurtonCostEstimator requires proprietary cost correlation data
|
|
45
|
+
files from Turton et al. (2008) which cannot be distributed with ExerPy.
|
|
46
|
+
|
|
47
|
+
To use cost estimation with Turton correlations, you need to:
|
|
48
|
+
1. Obtain the correlation data from the original source
|
|
49
|
+
2. Create the required JSON files in src/exerpy/data/cost_correlations/
|
|
50
|
+
3. Ensure turton.py is present in src/exerpy/cost_estimation/
|
|
51
|
+
|
|
52
|
+
Alternatively, provide component costs directly to ExergoeconomicAnalysis.run().
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, *args, **kwargs):
|
|
56
|
+
raise ImportError(
|
|
57
|
+
"TurtonCostEstimator is not available. "
|
|
58
|
+
"The required cost correlation data files (Turton 2008, CEPCI) "
|
|
59
|
+
"are not included in this distribution due to copyright restrictions.\n\n"
|
|
60
|
+
"Options:\n"
|
|
61
|
+
"1. Provide your own cost data files in src/exerpy/data/cost_correlations/\n"
|
|
62
|
+
"2. Provide costs directly to ExergoeconomicAnalysis.run() method\n"
|
|
63
|
+
"3. Implement your own cost estimator\n\n"
|
|
64
|
+
"See ExergoeconomicAnalysis documentation for the required cost format."
|
|
65
|
+
)
|