Repository: tarao1006/pyheatintegration (language: python)

Issue:
Move print
結果を出力する場所を `PinchAnalyzer` から `main.py` に移動しました。
最小接近温度差の可能値が負になり得る
- `get_possible_minimum_temp_diff_range` の戻り値に負が含まれる場合がある。

→ `max(minimum_minimum_approch_temp_diff, 0)` とするように変更する。


- `maximum_minimum_approch_temp_diff` が負となる場合、完全な熱交換は不可能である。しかし、外部流体を用意せずにTQ線図を描きたい要請に応える必要もある。

→ `PinchAnalyzer` のコンストラクタの引数に `ignore_maximum` を追加し、最大値のバリデーションを無視できるようにする。
総括伝熱係数を設定可能にする
熱交換器のコスト計算には、総括伝熱係数が必要となる。総括伝熱係数は、流体の状態によって異なる値となる。プロセスデザインコンテストの課題が参考になる。

ただし、利用者が設定できるようにもしたいので、

- デフォルトの総括伝熱係数を設定
- ~~新たに状態と総括伝熱係数の値を設定できるインターフェースの作成~~

を行いたい。

まずは、デフォルトの総括伝熱係数を設定する。
熱交換器のコスト計算を可能にする
対数平均温度差を用いて熱交換器コストを求める。向流と並流両方の場合のコストを求めるようにする。

## 計算式

**伝熱面積**

<img src="https://user-images.githubusercontent.com/32538736/125229011-d2c46400-e310-11eb-82e5-64296b8d6a18.png" height=16 alt="area">
 
- Q: 交換熱量
- U: 総括伝熱係数
- A: 伝熱面積
- deltaT: 対数平均温度差

**コスト計算**

<img src="https://user-images.githubusercontent.com/32538736/125228706-3b5f1100-e310-11eb-91b8-c720030fc753.png" height=24 alt="equation">

Kは係数で、以下の表の値となる

||K|
|:-:|:-:|
|熱交換器|1.0|
|リボイラー/反応器|2.0|
最小接近温度差の最大値
`ignore_maximum` を指定した場合、最小接近温度差の最大値のチェックを無視することができる。便宜的に最大値を `math.inf` としているが、この場合デタラメな値を設定した場合、うまく動作しない。そこで、便宜的な最大値を `与熱流体の最高温度 - 受熱流体の最低温度` に変更する。


Discussion / hints:

Fix patch:
diff --git a/VERSION b/VERSION
index abd4105..0d91a54 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-0.2.4
+0.3.0
diff --git a/docs/conf.py b/docs/conf.py
index aab5e9d..622e4e3 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -22,7 +22,7 @@
 author = 'tarao1006'
 
 # The full version, including alpha/beta/rc tags
-release = '0.2.4'
+release = '0.3.0'
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/docs/index.rst b/docs/index.rst
index 4bcaa39..e997d8f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -29,10 +29,10 @@ Getting Started
 
    # 熱交換を行う流体を準備
    streams = [
-      Stream(40.0, 90.0, 150.0, StreamType(1)),
-      Stream(80.0, 110.0, 180.0, StreamType(1)),
-      Stream(125.0, 80.0, 180.0, StreamType(2)),
-      Stream(100.0, 60.0, 160.0, StreamType(2))
+      Stream(40.0, 90.0, 150.0, StreamType(1), 'cold1'),
+      Stream(80.0, 110.0, 180.0, StreamType(1), 'cold2'),
+      Stream(125.0, 80.0, 180.0, StreamType(2), 'hot1'),
+      Stream(100.0, 60.0, 160.0, StreamType(2), 'hot2')
    ]
    minimum_approach_temperature_difference = 10.0
    analyzer = PinchAnalyzer(streams, minimum_approach_temperature_difference)
@@ -132,10 +132,10 @@ Details
 .. code-block:: python
 
    streams = [
-      Stream(40.0, 90.0, 150.0, StreamType(1)),
-      Stream(80.0, 110.0, 180.0, StreamType(1)),
-      Stream(125.0, 80.0, 180.0, StreamType(2)),
-      Stream(100.0, 60.0, 160.0, StreamType(2))
+      Stream(40.0, 90.0, 150.0, StreamType(1), 'cold1'),
+      Stream(80.0, 110.0, 180.0, StreamType(1), 'cold2'),
+      Stream(125.0, 80.0, 180.0, StreamType(2), 'hot1'),
+      Stream(100.0, 60.0, 160.0, StreamType(2), 'hot2')
    ]
 
 ``Stream`` のコンストラクタ:
diff --git a/examples/simple/data.csv b/examples/simple/data.csv
index fedb069..59103fd 100644
--- a/examples/simple/data.csv
+++ b/examples/simple/data.csv
@@ -1,5 +1,5 @@
-input_temperature,output_temperature,heat_flow,type,cost
-40.0,90.0,150.0,1,0
-80.0,110.0,180.0,1,0
-125.0,80.0,160.0,2,0
-100.0,60.0,160.0,2,0
\ No newline at end of file
+id,input_temperature,output_temperature,heat_flow,type,state,cost,reboiler_or_reactor
+a,40.0,90.0,150.0,1,1,0,
+b,80.0,110.0,180.0,1,1,0,
+A,125.0,80.0,160.0,2,1,0,
+B,100.0,60.0,160.0,2,1,0,
\ No newline at end of file
diff --git a/examples/simple/main.py b/examples/simple/main.py
index b6dcabf..43c8e1c 100644
--- a/examples/simple/main.py
+++ b/examples/simple/main.py
@@ -2,24 +2,56 @@
 import pandas as pd
 from matplotlib.collections import LineCollection
 
-from pyheatintegration import (PinchAnalyzer, Stream, StreamType,
+from pyheatintegration import (PinchAnalyzer, Stream, StreamState, StreamType,
                                extract_x, y_range)
 
 
 def main():
-    df = pd.read_csv("./data.csv")
+    df = pd.read_csv("./data.csv").fillna({'reboiler_or_reactor': ''})
     streams = [
         Stream(
             row.input_temperature,
             row.output_temperature,
             row.heat_flow,
             StreamType(row.type),
-            row.cost
+            StreamState(row.state),
+            cost=row.cost,
+            reboiler_or_reactor=bool(row.reboiler_or_reactor),
+            id_=row.id
         ) for _, row in df.iterrows()]
 
     minimum_approach_temperature_difference = 10.0
     analyzer = PinchAnalyzer(streams, minimum_approach_temperature_difference)
 
+    print(
+        f"設定可能最小接近温度差 [℃]: {analyzer.minimum_approach_temp_diff_range.start:.3f}"
+        f" ~ {analyzer.minimum_approach_temp_diff_range.finish:.3f}"
+    )
+
+    print(f'ピンチポイント [℃]: {analyzer.gcc.minimum_pinch_point_temp}')
+    print(f'必要加熱量[W]: {analyzer.external_heating_demand:.3f}')
+    print(f'必要冷却量[W]: {analyzer.external_cooling_demand:.3f}')
+
+    for stream in analyzer.streams:
+        if stream.is_external() and stream.is_hot():
+            print(
+                f'外部与熱流体 id: {stream.id_} '
+                '\033[1m加熱量\033[0m [W]: '
+                f'{stream.heat():.3f} '
+                '\033[1mコスト\033[0m [円/s]: '
+                f'{stream.heat() * stream.cost:.3f}'
+            )
+
+    for stream in analyzer.streams:
+        if stream.is_external() and stream.is_cold():
+            print(
+                f'外部受熱流体 id: {stream.id_} '
+                '\033[1m冷却量\033[0m [W]: '
+                f'{stream.heat():.3f} '
+                '\033[1mコスト\033[0m [円/s]: '
+                f'{stream.heat() * stream.cost:.3f}'
+            )
+
     # グランドコンポジットカーブ
     heats, temps = analyzer.create_grand_composite_curve()
     fig, ax = plt.subplots(1, 1)
diff --git a/src/pyheatintegration/__init__.py b/src/pyheatintegration/__init__.py
index 6dabc6a..e4cee5d 100644
--- a/src/pyheatintegration/__init__.py
+++ b/src/pyheatintegration/__init__.py
@@ -1,4 +1,4 @@
-from .enums import StreamType  # noqa
+from .enums import StreamState, StreamType  # noqa
 from .grand_composite_curve import GrandCompositeCurve  # noqa
 from .line import extract_x, y_range  # noqa
 from .pinch_analyzer import PinchAnalyzer  # noqa
diff --git a/src/pyheatintegration/enums.py b/src/pyheatintegration/enums.py
index e9eeaa1..b051377 100644
--- a/src/pyheatintegration/enums.py
+++ b/src/pyheatintegration/enums.py
@@ -8,6 +8,7 @@ class StreamType(Enum):
     HOT = 2
     EXTERNAL_COLD = 3
     EXTERNAL_HOT = 4
+    AUTO = 5
 
     def describe(self) -> str:
         return STREAMTYPE_STR[self.name]
@@ -18,4 +19,27 @@ def describe(self) -> str:
     'HOT': 'hot',
     'EXTERNAL_COLD': 'external cold',
     'EXTERNAL_HOT': 'external hot',
+    'AUTO': 'auto'
+}
+
+
+class StreamState(Enum):
+    """流体の状態"""
+
+    LIQUID = 1
+    GAS = 2
+    LIQUID_EVAPORATION = 3
+    GAS_CONDENSATION = 4
+    UNKNOWN = 5
+
+    def describe(self) -> str:
+        return STREAMSTATE_STR[self.name]
+
+
+STREAMSTATE_STR = {
+    'LIQUID': 'liquid',
+    'GAS': 'gas',
+    'LIQUID_EVAPORATION': 'liquid (evaporation)',
+    'GAS_CONDENSATION': 'gas (condensation)',
+    'UNKNOWN': 'unknown'
 }
diff --git a/src/pyheatintegration/grand_composite_curve.py b/src/pyheatintegration/grand_composite_curve.py
index 0b32569..3f4b593 100644
--- a/src/pyheatintegration/grand_composite_curve.py
+++ b/src/pyheatintegration/grand_composite_curve.py
@@ -5,6 +5,49 @@
 from .temperature_range import TemperatureRange, get_temperatures
 
 
+def _get_heats(
+    temp_ranges: list[TemperatureRange],
+    temp_range_streams: defaultdict[TemperatureRange, set[Stream]]
+) -> list[float]:
+    """温度変化領域ごとの熱量変化を求めます。
+
+    Args:
+        temp_ranges: list[TemperatureRange]: 温度領域のリスト。
+        temp_range_lacking_heat: dict[TemperatureRange, float]:
+            温度領域ごとの過不足熱量。
+
+    Returns:
+        list[float]: 熱量のリスト。
+    """
+    temp_range_lacking_heat = _get_lacking_heats(temp_range_streams)
+
+    temp_ranges.sort()
+    heats = [0.0] * (len(temp_ranges) + 1)
+    for i, temp_range in enumerate(temp_ranges):
+        heats[i + 1] = heats[i] - temp_range_lacking_heat[temp_range]
+    min_heat = min(heats)
+
+    return [heat - min_heat for heat in heats]
+
+
+def _get_lacking_heats(
+    temp_range_streams: defaultdict[TemperatureRange, set[Stream]]
+) -> defaultdict[TemperatureRange, float]:
+    """温度領域ごとの不足熱量を求めます.
+
+    Args:
+        temp_range_streams defaultdict[float, set[Stream]]:
+            温度領域ごとの流体のセット。
+
+    Returns:
+        defaultdict[TemperatureRange, float]: 温度領域ごとの過不足熱量。
+    """
+    return defaultdict(int, {
+        temp_range: sum([s.heat() for s in streams if s.is_hot()]) - sum([s.heat() for s in streams if s.is_cold()])
+        for temp_range, streams in temp_range_streams.items()
+    })
+
+
 class GrandCompositeCurve:
     """グランドコンポジットカーブを作成するために必要な情報を得るためのクラス。
 
@@ -21,8 +64,6 @@ class GrandCompositeCurve:
         minimum_pinch_point_index (int): 最も温度が低いピンチポイントのインデックス。
         temps (list[float]): 温度のリスト[℃]。
         heats (list[float]): 熱量のリスト[W]。
-        hot_utility_target (float): 必要加熱量[W]。
-        cold_utility_target (float): 必要冷却熱量[W]。
     """
 
     def __init__(
@@ -48,21 +89,30 @@ def __init__(
             stream for stream in streams if stream.is_internal()
         ])
         self.temps = get_temperatures(temp_ranges)
-        self.heats = self._get_heats(
-            temp_ranges,
-            self._lacking_heats(temp_range_streams)
-        )
+        self.heats = _get_heats(temp_ranges, temp_range_streams)
 
-        pinch_point_info = self._set_pinch_point()
+        pinch_point_info = self._get_pinch_point()
         self.maximum_pinch_point_temp = pinch_point_info[0]
         self.maximum_pinch_point_index = pinch_point_info[1]
         self.minimum_pinch_point_temp = pinch_point_info[2]
         self.minimum_pinch_point_index = pinch_point_info[3]
 
-        self.hot_utility_target = self.heats[-1]
-        self.cold_utility_target = self.heats[0]
+    def _get_pinch_point(self) -> tuple[float, int, float, int]:
+        """ピンチポイントとピンチポイントのインデックスを求めます。
+        """
+        pinch_point_indexes = [
+            i for i, heat in enumerate(self.heats) if heat == 0
+        ]
+        pinch_points = [self.temps[i] for i in pinch_point_indexes]
+
+        return (
+            pinch_points[-1],
+            pinch_point_indexes[-1],
+            pinch_points[0],
+            pinch_point_indexes[0]
+        )
 
-    def solve_external_heat(self) -> dict[int, float]:
+    def solve_external_heat(self) -> dict[str, float]:
         """外部流体による熱交換量を求めます.
 
         Returns:
@@ -93,71 +143,6 @@ def solve_external_heat(self) -> dict[int, float]:
             for stream in updated_external_streams
         }
 
-    @classmethod
-    def _lacking_heats(
-        cls,
-        temp_range_streams: defaultdict[TemperatureRange, set[Stream]]
-    ) -> defaultdict[TemperatureRange, float]:
-        """温度領域ごとの不足熱量を求めます.
-
-        Args:
-            temp_range_streams defaultdict[float, set[Stream]]:
-                温度領域ごとの流体のセット。
-
-        Returns:
-            defaultdict[TemperatureRange, float]: 温度領域ごとの過不足熱量。
-        """
-        return defaultdict(int, {
-            temp_range: sum([s.heat() for s in streams if s.is_hot()]) - sum([s.heat() for s in streams if s.is_cold()])
-            for temp_range, streams in temp_range_streams.items()
-        })
-
-    @classmethod
-    def _get_heats(
-        cls,
-        temp_ranges: list[TemperatureRange],
-        temp_range_lacking_heat: dict[TemperatureRange, float]
-    ) -> list[float]:
-        """温度変化領域ごとの熱量変化を求めます。
-
-        Args:
-            temp_ranges: list[TemperatureRange]: 温度領域のリスト。
-            temp_range_lacking_heat: dict[TemperatureRange, float]:
-                温度領域ごとの過不足熱量。
-
-        Returns:
-            list[float]: 熱量のリスト。
-        """
-        temp_ranges.sort()
-        heats = [0.0] * (len(temp_ranges) + 1)
-        for i, temp_range in enumerate(temp_ranges):
-            heats[i + 1] = heats[i] - temp_range_lacking_heat[temp_range]
-        min_heat = min(heats)
-
-        return [heat - min_heat for heat in heats]
-
-    def _set_pinch_point(self) -> tuple[float, int, float, int]:
-        """ピンチポイントとピンチポイントのインデックスを求めます。
-        """
-        # heat == 0は必ず存在する。
-        if 0 not in self.heats:
-            raise ValueError("heatに0が存在しません。")
-
-        pinch_point_indexes = [
-            i for i, heat in enumerate(self.heats) if heat == 0
-        ]
-        pinch_points = [self.temps[i] for i in pinch_point_indexes]
-
-        if any(pinch_point_indexes) in [0, len(self.heats) - 1]:
-            print("ピンチポイントの値が不正である可能性があります。")
-
-        return (
-            pinch_points[-1],
-            pinch_point_indexes[-1],
-            pinch_points[0],
-            pinch_point_indexes[0]
-        )
-
     def _update_external_streams(
             self,
             external_cold_streams: list[Stream],
diff --git a/src/pyheatintegration/heat_exchanger.py b/src/pyheatintegration/heat_exchanger.py
index a5c8293..95114d6 100644
--- a/src/pyheatintegration/heat_exchanger.py
+++ b/src/pyheatintegration/heat_exchanger.py
@@ -1,30 +1,101 @@
 import math
-from typing import Optional
 
+from .enums import StreamState
 from .heat_range import HeatRange
 from .plot_segment import PlotSegment
 
+OVERALL_HEAT_TRANSFER_COEFFICIENT = {
+    StreamState.LIQUID: {
+        StreamState.LIQUID: 300.0,
+        StreamState.GAS: 200.0,
+        StreamState.LIQUID_EVAPORATION: 1000.0
+    },
+    StreamState.GAS: {
+        StreamState.LIQUID: 200.0,
+        StreamState.GAS: 150.0,
+        StreamState.LIQUID_EVAPORATION: 500.0
+    },
+    StreamState.GAS_CONDENSATION: {
+        StreamState.LIQUID: 1000.0,
+        StreamState.GAS: 500.0,
+        StreamState.LIQUID_EVAPORATION: 1500.0
+    },
+}
+
+
+def get_overall_heat_transfer_coefficient(
+    hot_stream_state: StreamState,
+    cold_stream_state: StreamState
+) -> float:
+    """対応する総括伝熱係数を返します。
+
+    Args:
+        hot_stream_state (StreamState): 与熱流体の状態。
+        cold_stream_state (StreamState): 受熱流体の状態。
+
+    Returns:
+        float: 総括伝熱係数 [W/m2.K]
+
+    Exapmles:
+        >>> get_overall_heat_transfer_coefficient(StreamState.LIQUID, StreamState.LIQUID)
+        300.0
+    """
+    if hot_stream_state not in [
+        StreamState.LIQUID, StreamState.GAS, StreamState.GAS_CONDENSATION
+    ]:
+        raise ValueError(
+            '与熱流体の状態が不正です。対応する総括伝熱係数が設定されていません。'
+            f'状態: {hot_stream_state} '
+            '設定可能な状態: '
+            f'{StreamState.LIQUID}, '
+            f'{StreamState.GAS}, '
+            f'{StreamState.GAS_CONDENSATION}'
+        )
+
+    if cold_stream_state not in [
+        StreamState.LIQUID, StreamState.GAS, StreamState.LIQUID_EVAPORATION
+    ]:
+        raise ValueError(
+            '受熱流体の状態が不正です。対応する総括伝熱係数が設定されていません。'
+            f'状態: {cold_stream_state} '
+            '設定可能な状態: '
+            f'{StreamState.LIQUID}, '
+            f'{StreamState.GAS}, '
+            f'{StreamState.LIQUID_EVAPORATION}'
+        )
+
+    return OVERALL_HEAT_TRANSFER_COEFFICIENT[hot_stream_state][cold_stream_state]
+
 
 class HeatExchanger:
     """熱交換器を表すクラス。
 
+    Args:
+        heat_range (HeatRange): 熱量領域。
+        hot_plot_segment (PlotSegment): 与熱流体のプロットセグメント。
+        cold_plot_segment (PlotSegment): 受熱流体のプロットセグメント。
+
     Attributes:
         heat_range (HeatRange): 熱交換の範囲。
         hot_stream_uuid (str): 与熱流体のid。
         cold_stream_uuid (str): 受熱流体のid。
+        hot_stream_state (StreamState): 与熱流体の状態。
+        cold_stream_state (StreamState): 受熱流体の状態。
         hot_temperature_range (TemperatureRange): 与熱流体の温度領域。
         cold_temperature_range (TemperatureRange): 受熱流体の温度領域。
-        lmtd_parallel_flow (Optional[float]): 並流の場合の対数平均温度差。
-        lmtd_counterflow (float): 向流の場合の対数平均温度差。
+        lmtd (float): 対数平均温度差。
+        area (float): 向流の場合の必要面積 [m2]。
         hot_plot_segment (PlotSegment): 与熱流体のプロットセグメント。
         cold_plot_segment (PlotSegment): 受熱流体のプロットセグメント。
+        reboiler_or_reactor (bool): リボイラーもしくは反応器で用いるか。
     """
 
     def __init__(
         self,
         heat_range: HeatRange,
         hot_plot_segment: PlotSegment,
-        cold_plot_segment: PlotSegment
+        cold_plot_segment: PlotSegment,
+        counterflow: bool = True
     ):
         self.heat_range = heat_range
         self.hot_plot_segment = hot_plot_segment
@@ -33,16 +104,28 @@ def __init__(
         self.cold_temperature_range = self.cold_plot_segment.temperature_range
         self.hot_stream_uuid = self.hot_plot_segment.uuid
         self.cold_stream_uuid = self.cold_plot_segment.uuid
+        self.hot_stream_state = self.hot_plot_segment.state
+        self.cold_stream_state = self.cold_plot_segment.state
+        self.reboiler_or_reactor = self.hot_plot_segment.reboiler_or_reactor | self.cold_plot_segment.reboiler_or_reactor
+
+        self.overall_heat_transfer_coefficient = get_overall_heat_transfer_coefficient(
+            self.hot_stream_state,
+            self.cold_stream_state
+        )
 
-        self.lmtd_parallel_flow = self.init_lmtd_pararell_flow()
-        self.lmtd_counterflow = self.init_lmtd_counterflow()
+        if counterflow:
+            self.lmtd = self.init_lmtd_counterflow()
+        else:
+            self.lmtd = self.init_lmtd_pararell_flow()
+
+        self.area = self.heat_range.delta / self.lmtd / self.overall_heat_transfer_coefficient
 
     def __repr__(self) -> str:
         return (
-            "HeatExchanger("
-            f"{self.heat_range}, "
-            f"{self.hot_plot_segment}, "
-            f"{self.cold_plot_segment})"
+            'HeatExchanger('
+            f'{self.heat_range}, '
+            f'{self.hot_plot_segment}, '
+            f'{self.cold_plot_segment})'
         )
 
     def __str__(self) -> str:
@@ -56,7 +139,14 @@ def __str__(self) -> str:
     def __lt__(self, other) -> bool:
         return self.heat_range < other.heat_range
 
-    def init_lmtd_pararell_flow(self) -> Optional[float]:
+    def init_lmtd_pararell_flow(self) -> float:
+        """並流の場合の対数平均温度差を返します。
+
+        並流が不可能な場合はNoneを返します。
+
+        Returns:
+            Optional[float]: 並流の場合の対数平均温度差。並流が不可能な場合はNone。
+        """
         hot_low_temp, hot_high_temp = self.hot_temperature_range()
         cold_low_temp, cold_high_temp = self.cold_temperature_range()
 
@@ -64,7 +154,10 @@ def init_lmtd_pararell_flow(self) -> Optional[float]:
         finish_temp_diff = hot_low_temp - cold_high_temp
 
         if finish_temp_diff <= 0:
-            return None
+            raise RuntimeError(
+                '出口温度差が0以下となるため、並流にすることができません。'
+                f'出口温度差: {finish_temp_diff} ℃'
+            )
 
         if start_temp_diff == finish_temp_diff:
             return start_temp_diff
@@ -72,6 +165,11 @@ def init_lmtd_pararell_flow(self) -> Optional[float]:
         return (start_temp_diff - finish_temp_diff) / math.log(start_temp_diff / finish_temp_diff)
 
     def init_lmtd_counterflow(self) -> float:
+        """向流の場合の対数平均温度差を返します。
+
+        Returns:
+            float: 向流の場合の対数平均温度差。
+        """
         hot_low_temp, hot_high_temp = self.hot_temperature_range()
         cold_low_temp, cold_high_temp = self.cold_temperature_range()
 
@@ -85,6 +183,15 @@ def init_lmtd_counterflow(self) -> float:
 
 
 def merge_heat_exchangers(heat_exchanger: HeatExchanger, other: HeatExchanger) -> HeatExchanger:
+    """熱交換器を結合します。
+
+    Args:
+        heat_exchanger (HeatExchange): 熱交換器。
+        other (HeatExchanger): 熱交換器(結合対象)。
+
+    Returns:
+        HeatExchanger: 結合後の熱交換器。
+    """
     heat_range = heat_exchanger.heat_range.merge(other.heat_range)
     hot_plot_segment = heat_exchanger.hot_plot_segment.merge(other.hot_plot_segment)
     cold_plot_segment = heat_exchanger.cold_plot_segment.merge(other.cold_plot_segment)
diff --git a/src/pyheatintegration/line.py b/src/pyheatintegration/line.py
index b53aa0e..c1f2b02 100644
--- a/src/pyheatintegration/line.py
+++ b/src/pyheatintegration/line.py
@@ -8,12 +8,23 @@
 def y_range(lines: list[Line]) -> tuple[float, float]:
     """xy座標系における複数の直線からyの最小値と最大値を返します。
 
+    直線は広義単調増加であることを期待しています。
+
     Args:
         lines list[Line]: 直線。
 
     Returns:
         tuple[float, float]:
             最小値と最大値。
+
+    Examples:
+        >>> y_range([
+                ((0, 0), (1, 1)),
+                ((1, 1), (2, 2)),
+                ((2, 2), (3, 5)),
+                ((3, 3), (5, 8))
+            ])
+        >>> (0, 8)
     """
     return (
         min(line[0][1] for line in lines),
@@ -29,5 +40,14 @@ def extract_x(lines: list[Line]) -> list[float]:
 
     Returns:
         list[float]: x座標の値。
+
+    Examples:
+        >>> extract_x([
+                ((0, 0), (1, 1)),
+                ((1, 1), (2, 2)),
+                ((2, 2), (3, 5)),
+                ((3, 3), (5, 8))
+            ])
+        >>> [0, 1, 2, 3, 5]
     """
     return sorted(list(set(point[0] for point in chain(*lines))))
diff --git a/src/pyheatintegration/pinch_analyzer.py b/src/pyheatintegration/pinch_analyzer.py
index c7d9794..298972f 100644
--- a/src/pyheatintegration/pinch_analyzer.py
+++ b/src/pyheatintegration/pinch_analyzer.py
@@ -1,51 +1,100 @@
+import math
 from copy import deepcopy
 
 from .grand_composite_curve import GrandCompositeCurve
+from .heat_exchanger import HeatExchanger
+from .heat_range import HeatRange, get_detailed_heat_ranges
 from .line import Line
-from .stream import Stream
+from .plot_segment import PlotSegment, get_plot_segments
+from .stream import Stream, is_valid_streams
 from .tq_diagram import TQDiagram, get_possible_minimum_temp_diff_range
 
 
+def calculate_heat_exchanger_cost(
+    area: float,
+    reboiler_or_reactor: bool = False
+) -> float:
+    """熱交換器にかかるコストを返します。
+
+    Args:
+        area (float): 熱交換器の面積。
+        k (float): 係数。リボイラーまたは反応器の場合は2
+
+    Returns:
+        float: コスト[円]。
+    """
+    if reboiler_or_reactor:
+        k = 2.0
+    else:
+        k = 1.0
+    return 1_500_000 * math.pow(area, 0.65) * k
+
+
 class PinchAnalyzer:
-    """エントリーポイント。
+    """流体のリストと最小接近温度差を設定し、グランドコンポジットカーブおよびTQ線図を作成します。
+
+    解析を行う場合はこのクラス経由で扱います。このクラスを経由することで、流体のidが重複してい
+    ないことや、最小接近温度差が指定可能な値であるかを検証したのちに図を作成するため、予想外の
+    エラーが生じることを回避することができます。
+
+    Args:
+        streams_ (list[Stream]): 流体のリスト。
+        minimum_approach_temp_diff (float): 最小接近温度差 [℃]。
+        ignore_maximum (bool): 最小接近温度差の最大値のチェックを無視するかどうか。
+
+    Attributes:
+        gcc (GrandCompositeCurve): グランドコンポジットカーブ。
+        tq (TQDiagram): TQ線図。
+        streams (list[Stream]): 流体のリスト。
+        minimum_approach_temp_diff_range (TemperatureRange): 最小接近温度差の指定可能範囲。
+        pinch_point_temp (float): ピンチポイントの温度 [℃]。
+        heat_exchangers (list[HeatExchanger]): 熱交換器のリスト。
+        heat_exchanger_cost (float): 熱交換器のコスト。
+        external_heating_demand (float): 必要加熱量[W]。
+        external_cooling_demand (float): 必要冷却熱量[W]。
+
+    Raises:
+        ValueError: 流体のidが重複している場合。また、最小接近温度差の値が不正な場合。
+        RuntimeError: 受熱流体、与熱流体が一つも指定されていない場合。
     """
 
     def __init__(
         self,
         streams_: list[Stream],
-        minimum_approach_temp_diff: float
+        minimum_approach_temp_diff: float,
+        ignore_maximum: bool = False
     ):
-        hot_streams = sorted(
-            [stream for stream in streams_ if stream.is_hot()],
-            key=lambda s: s.output_temperature()
-        )
-        cold_streams = sorted(
-            [stream for stream in streams_ if stream.is_cold()],
-            key=lambda s: s.input_temperature()
-        )
-
-        if not hot_streams:
-            raise RuntimeError('与熱流体は少なくとも1つは指定する必要があります。')
-        if not cold_streams:
-            raise RuntimeError('受熱流体は少なくとも1つは指定する必要があります。')
-
-        streams = hot_streams + cold_streams
-        for i, s in enumerate(streams):
-            s.set_id(i + 1)
-
-        streams = deepcopy(streams)
+        streams = deepcopy(streams_)
 
-        minimum_approach_temp_diff_range = get_possible_minimum_temp_diff_range(streams)
+        id_set: set[str] = set()
+        for stream in streams:
+            if stream.id_ in id_set:
+                raise ValueError(
+                    '流体のidは一意である必要があります。'
+                    f'重複しているid: {stream.id_}'
+                )
+            id_set.add(stream.id_)
+
+        if not is_valid_streams(streams):
+            raise ValueError('与熱流体および受熱流体は少なくとも1つは指定する必要があります。')
+
+        self.minimum_approach_temp_diff_range = get_possible_minimum_temp_diff_range(
+            streams,
+            ignore_maximum
+        )
 
-        if minimum_approach_temp_diff not in minimum_approach_temp_diff_range:
+        if minimum_approach_temp_diff not in self.minimum_approach_temp_diff_range:
             raise ValueError(
                 "最小接近温度差が不正です。"
                 f"指定最小接近温度差 [℃]: {minimum_approach_temp_diff}, "
-                f"設定可能最小接近温度差 [℃]: {minimum_approach_temp_diff_range.start:.3f}"
-                f" - {minimum_approach_temp_diff_range.finish:.3f}"
+                f"設定可能最小接近温度差 [℃]: {self.minimum_approach_temp_diff_range.start:.3f}"
+                f" ~ {self.minimum_approach_temp_diff_range.finish:.3f}"
             )
 
         self.gcc = GrandCompositeCurve(streams, minimum_approach_temp_diff)
+        self.external_heating_demand = self.gcc.heats[-1]
+        self.external_cooling_demand = self.gcc.heats[0]
+
         id_heats = self.gcc.solve_external_heat()
         for stream in streams:
             for id_, heat in id_heats.items():
@@ -60,13 +109,46 @@ def __init__(
             self.pinch_point_temp
         )
 
+        all_heat_ranges = get_detailed_heat_ranges(
+            [
+                [plot_segment.heat_range for plot_segment in self.tq.hcc_merged],
+                [plot_segment.heat_range for plot_segment in self.tq.ccc_merged]
+            ]
+        )
+        hot_heat_range_plot_segment: dict[HeatRange, PlotSegment] = {
+            s.heat_range: s for s in get_plot_segments(all_heat_ranges, self.tq.hcc_merged)
+        }
+        cold_heat_range_plot_segment: dict[HeatRange, PlotSegment] = {
+            s.heat_range: s for s in get_plot_segments(all_heat_ranges, self.tq.ccc_merged)
+        }
+
+        self.heat_exchangers: list[HeatExchanger] = []
+        for heat_range in all_heat_ranges:
+            hot_plot_segment = hot_heat_range_plot_segment.get(heat_range, None)
+            cold_plot_segment = cold_heat_range_plot_segment.get(heat_range, None)
+
+            if hot_plot_segment is None or cold_plot_segment is None:
+                continue
+
+            self.heat_exchangers.append(
+                HeatExchanger(heat_range, hot_plot_segment, cold_plot_segment)
+            )
+
+        self.heat_exchanger_cost = sum(
+            calculate_heat_exchanger_cost(
+                heat_exchanger.area,
+                heat_exchanger.reboiler_or_reactor
+            )
+            for heat_exchanger in self.heat_exchangers
+        )
+
     def create_grand_composite_curve(self) -> tuple[list[float], list[float]]:
         """グランドコンポジットカーブを描くために必要な熱量と温度を返します。
         """
         return self.gcc.heats, self.gcc.temps
 
     def create_tq(self) -> tuple[list[Line], list[Line]]:
-        """tq線図をを描くために必要な直線と熱量変化帯を返します。
+        """tq線図をを描くために必要な与熱複合線および受熱複合線を返します。
         """
         return (
             self.tq.hot_lines,
@@ -74,7 +156,7 @@ def create_tq(self) -> tuple[list[Line], list[Line]]:
         )
 
     def create_tq_separated(self) -> tuple[list[Line], list[Line]]:
-        """流体ごとに分割したtq線図をを描くために必要な直線と熱量変化帯を返します。
+        """流体ごとに分割したtq線図をを描くために必要な与熱複合線および受熱複合線を返します。
         """
         return (
             self.tq.hot_lines_separated,
@@ -82,7 +164,7 @@ def create_tq_separated(self) -> tuple[list[Line], list[Line]]:
         )
 
     def create_tq_splitted(self) -> tuple[list[Line], list[Line]]:
-        """流体ごとに分割し、最小接近温度差の条件を満たしたtq線図をを描くために必要な直線を返します。
+        """流体ごとに分割し、最小接近温度差の条件を満たしたtq線図をを描くために必要な与熱複合線および受熱複合線を返します。
         """
         return (
             self.tq.hot_lines_splitted,
@@ -90,7 +172,7 @@ def create_tq_splitted(self) -> tuple[list[Line], list[Line]]:
         )
 
     def create_tq_merged(self) -> tuple[list[Line], list[Line]]:
-        """流体ごとに分割し、最小接近温度差の条件を満たしたtq線図をを描くために必要な直線を返します。
+        """結合可能な熱交換器を結合したtq線図をを描くために必要な与熱複合線および受熱複合線を返します。
         """
         return (
             self.tq.hot_lines_merged,
diff --git a/src/pyheatintegration/plot_segment.py b/src/pyheatintegration/plot_segment.py
index 492970d..bb48ce2 100644
--- a/src/pyheatintegration/plot_segment.py
+++ b/src/pyheatintegration/plot_segment.py
@@ -4,15 +4,32 @@
 from collections.abc import Iterable
 from typing import Optional
 
+from .enums import StreamState
 from .heat_range import HeatRange
 from .heat_range import is_continuous as is_continuous_heat_ranges
+from .line import Line
 from .temperature_range import TemperatureRange
 
 
 class PlotSegment:
-    uuid: str
-    heat_range: HeatRange
-    temperature_range: TemperatureRange
+    """プロットの一部を表すクラスです。
+
+    Args:
+        start_heat (float): 熱量の開始値。
+        finish_heat (float): 熱量の終了値。
+        start_temperature (float): 温度の開始値。
+        finish_temperature (float): 温度の終了値。
+        uuid_ (Optional[str]): uuid。対応する流体がある場合はそのid。
+        state (StreamState): 対応する流体の状態。
+        reboiler_or_reactor (bool): 対応する流体がリボイラーもしくは反応器で用いられるか。
+
+    Attributes:
+        heat_range (HeatRange): 熱量領域。
+        temperature_range (TemperatureRange): 温度領域。
+        uuid (str): uuid。対応する流体がある場合はそのid。
+        state (StreamState): 対応する流体の状態。
+        reboiler_or_reactor (bool): 対応する流体がリボイラーもしくは反応器で用いられるか。
+    """
 
     def __init__(
         self,
@@ -20,7 +37,9 @@ def __init__(
         finish_heat: float = 0.0,
         start_temperature: float = 0.0,
         finish_temperature: float = 0.0,
-        uuid_: Optional[str] = None
+        uuid_: Optional[str] = None,
+        state: StreamState = StreamState.UNKNOWN,
+        reboiler_or_reactor: bool = False
     ):
         self.heat_range = HeatRange(start_heat, finish_heat)
         self.temperature_range = TemperatureRange(start_temperature, finish_temperature)
@@ -29,6 +48,9 @@ def __init__(
         else:
             self.uuid = uuid_
 
+        self.state = state
+        self.reboiler_or_reactor = reboiler_or_reactor
+
     def __str__(self) -> str:
         return (
             f"heat: ({self.heat_range.start}; "
@@ -81,43 +103,118 @@ def __ge__(self, other: object) -> bool:
             return NotImplemented
         return self.heat_range >= other.heat_range
 
-    def line(self) -> tuple[tuple[float, float], tuple[float, float]]:
+    def line(self) -> Line:
+        """直線の始点と終点を返します。
+
+        Returns:
+            Line: 直線。
+        """
         return (
             (self.heats()[0], self.temperatures()[0]),
             (self.heats()[1], self.temperatures()[1])
         )
 
     def heats(self) -> tuple[float, float]:
+        """熱量の開始値と終了値を返します。
+
+        Returns:
+            tuple[float, float]: 熱量の開始値と終了値。
+        """
         return self.heat_range()
 
     def start_heat(self) -> float:
+        """熱量の開始値を返します。
+
+        Returns:
+            float: 熱量の開始値。
+        """
         return self.heat_range.start
 
     def finish_heat(self) -> float:
+        """熱量の終了値を返します。
+
+        Returns:
+            float: 熱量の終了値。
+        """
         return self.heat_range.finish
 
     def temperatures(self) -> tuple[float, float]:
+        """温度の開始値と終了値を返します。
+
+        Returns:
+            tuple[float, float]: 温度の開始値と終了値。
+        """
         return self.temperature_range()
 
     def start_temperature(self) -> float:
+        """温度の開始値を返します。
+
+        Returns:
+            float: 温度の開始値。
+        """
         return self.temperature_range.start
 
     def finish_temperature(self) -> float:
+        """温度の終了値を返します。
+
+        Returns:
+            float: 温度の終了値。
+        """
         return self.temperature_range.finish
 
     def contain_heat(self, heat: float) -> bool:
+        """熱量を含むかを返します。
+
+        Args:
+            heat (float): 含むかを検証する熱量。
+
+        Returns:
+            bool: 熱量を含むかどうか。
+        """
         return heat in self.heat_range
 
     def contain_heats(self, heats: Iterable[float]) -> bool:
+        """複数の熱量を含むかを返します。
+
+        Args:
+            heats (Iterable[float]): 含むかを検証する熱量。
+
+        Returns:
+            bool: 複数の熱量を含むかどうか。
+        """
         return all(self.contain_heat(heat) for heat in heats)
 
     def contain_temperature(self, temperature: float) -> bool:
+        """温度を含むかを返します。
+
+        Args:
+            temperature (float): 含むかを検証する温度。
+
+        Returns:
+            bool: 温度を含むかどうか。
+        """
         return temperature in self.temperature_range
 
     def temperatures_at_heats(self, heats: tuple[float, float]) -> tuple[float, float]:
+        """ある複数の熱量をとる温度を返します。
+
+        Args:
+            heats (tuple[float, float]): 温度を求めたい熱量。
+
+        Returns:
+            tuple[float, float]: ある熱量をとる温度。
+        """
         return self.temperature_at_heat(heats[0]), self.temperature_at_heat(heats[1])
 
     def temperature_at_heat(self, heat: float) -> float:
+        """ある熱量をとる温度を返します。
+
+        Args:
+            heat (float): 温度を求めたい熱量。
+
+        Returns:
+            float: ある熱量をとる温度。
+        """
         if not self.contain_heat(heat):
             raise ValueError('heatを含んでいる必要があります。')
 
@@ -128,8 +225,16 @@ def temperature_at_heat(self, heat: float) -> float:
         return slope * (heat - heat_left) + temp_left
 
     def heat_at_temperature(self, temperature: float) -> float:
+        """ある温度をとる熱量を返します。
+
+        Args:
+            temperature (float): 熱量を求めたい温度。
+
+        Returns:
+            float: ある温度をとる熱量。
+        """
         if not self.contain_temperature(temperature):
-            raise ValueError('heatを含んでいる必要があります。')
+            raise ValueError('temperatureを含んでいる必要があります。')
 
         heat_left, heat_right = self.heat_range()
         temp_left, temp_right = self.temperature_range()
@@ -138,14 +243,35 @@ def heat_at_temperature(self, temperature: float) -> float:
         return 1 / slope * (temperature - temp_left) + heat_left
 
     def shift_heat(self, delta: float) -> None:
+        """熱量をずらします。
+
+        Args:
+            delta (float): ずらす値。
+        """
         self.heat_range.shift(delta)
 
-    def mergiable(self, other) -> bool:
+    def mergiable(self, other: PlotSegment) -> bool:
+        """プロットセグメントを結合可能かを返します。
+
+        Args:
+            other (PlotSegment): 結合できるかを検証したいプロットセグメント。
+
+        Returns:
+            bool: 結合可能かどうか。
+        """
         return (self.uuid == other.uuid) \
             and (self.finish_heat() == other.start_heat()) \
             and (self.finish_temperature() == other.start_temperature())
 
     def merge(self, other: PlotSegment) -> PlotSegment:
+        """プロットセグメントを結合します。
+
+        Args:
+            other (PlotSegment): 結合したいプロットセグメント。
+
+        Returns:
+            PlotSegment: 結合後のプロットセグメント。
+        """
         if self.finish_heat() != other.start_heat():
             raise ValueError("other's start_heat must be self's finish_heat.")
         if self.finish_temperature() != other.start_temperature():
@@ -158,11 +284,22 @@ def merge(self, other: PlotSegment) -> PlotSegment:
             other.finish_heat(),
             self.start_temperature(),
             other.finish_temperature(),
-            self.uuid
+            self.uuid,
+            self.state,
+            self.reboiler_or_reactor
         )
 
 
 def temp_diff(segment: PlotSegment, other: PlotSegment) -> tuple[float, float]:
+    """同じ熱量領域のプロットセグメントの入り口温度の差と出口温度の差を求める。
+
+    Args:
+        segment (PlotSegment): プロットセグメント。
+        ohter (PlotSegment): プロットセグメント。
+
+    Returns:
+        tuple[float, float]: 入り口温度差、出口温度差。
+    """
     hot_temp_start, hot_temp_finish = segment.temperatures()
     cold_temp_start, cold_temp_finish = other.temperatures()
 
@@ -194,7 +331,9 @@ def get_plot_segments(
                     PlotSegment(
                         *heat_range(),
                         *plot_segment.temperatures_at_heats(heat_range()),
-                        plot_segment.uuid
+                        plot_segment.uuid,
+                        plot_segment.state,
+                        plot_segment.reboiler_or_reactor
                     )
                 )
     return res
diff --git a/src/pyheatintegration/segment.py b/src/pyheatintegration/segment.py
index 05fcd78..e18bf6d 100644
--- a/src/pyheatintegration/segment.py
+++ b/src/pyheatintegration/segment.py
@@ -130,7 +130,9 @@ def init_plot_segments_separated_streams(
                 start_heat,
                 finish_heat,
                 *temperature_range(),
-                str(streams[i].id_)
+                str(streams[i].id_),
+                streams[i].state,
+                streams[i].reboiler_or_reactor
             ))
             start_heat += heat
         return res
@@ -175,12 +177,16 @@ def split(self, minimum_approach_temp_diff: float) -> None:
                 hot_heat_range_plot_segment[heat_range] = PlotSegment(
                     *heat_range(),
                     *self.hot_temperature_range(),
-                    hot_plot_segment.uuid
+                    hot_plot_segment.uuid,
+                    hot_plot_segment.state,
+                    hot_plot_segment.reboiler_or_reactor
                 )
                 cold_heat_range_plot_segment[heat_range] = PlotSegment(
                     *heat_range(),
                     *self.cold_temperature_range(),
-                    cold_plot_segment.uuid
+                    cold_plot_segment.uuid,
+                    cold_plot_segment.state,
+                    cold_plot_segment.reboiler_or_reactor
                 )
 
                 for heat_range_ in self.heat_ranges:
@@ -199,14 +205,18 @@ def split(self, minimum_approach_temp_diff: float) -> None:
                         hot_heat_range_plot_segment[heat_range_] = PlotSegment(
                             *heat_range_(),
                             *self.hot_temperature_range(),
-                            hot_plot_segment.uuid
+                            hot_plot_segment.uuid,
+                            hot_plot_segment.state,
+                            hot_plot_segment.reboiler_or_reactor
                         )
 
                     if cold_plot_segment_ is not None and cold_plot_segment_.uuid == cold_plot_segment.uuid:
                         cold_heat_range_plot_segment[heat_range_] = PlotSegment(
                             *heat_range_(),
                             *self.cold_temperature_range(),
-                            cold_plot_segment.uuid
+                            cold_plot_segment.uuid,
+                            cold_plot_segment.state,
+                            cold_plot_segment.reboiler_or_reactor
                         )
 
         self.hot_plot_segments_splitted = sorted(list(hot_heat_range_plot_segment.values()))
diff --git a/src/pyheatintegration/stream.py b/src/pyheatintegration/stream.py
index ded7a38..66b3c55 100644
--- a/src/pyheatintegration/stream.py
+++ b/src/pyheatintegration/stream.py
@@ -1,40 +1,81 @@
 import math
+import uuid
 from collections import defaultdict
 from collections.abc import Iterable
 from copy import copy
 
-from .enums import StreamType
+from .enums import StreamState, StreamType
 from .errors import InvalidStreamError
 from .temperature_range import (TemperatureRange, get_temperature_ranges,
                                 get_temperature_transition)
 
 
 class Stream:
+    r"""熱交換を行う流体を表すクラス。
+
+    Args:
+        input_temperature (float): 入り口温度 [℃]。
+        output_temperature (float): 出口温度 [℃]。
+        heat_flow (float): 熱量 [W]。
+        type\_ (StreamType): 流体の種類。
+        state (StreamState): 流体の状態。
+        cost (float): 流体のコスト [円/J]。外部流体の場合のみ設定可能。
+        reboiler_or_reactor (bool): 流体がリボイラーまたは反応器で用いられるかどうか。
+        id\_ (str): 流体を区別する識別子。
+
+    Attributes:
+        id_ (str): 流体を区別する識別子。
+        temperature_range　(TemperatureRange): 温度範囲。
+        heat_flow (float): 熱量 [W]。
+        cost (float): コスト [円/J]。
+        type_ (StreamType): 流体の種類。
+        state　(StreamState): 流体の状態。
+        reboiler_or_reactor (bool): 流体がリボイラーまたは反応器で用いられるかどうか。
+
+    Raises:
+        InvalidStreamError:
+            入り口温度と出口温度の大小関係と流体種の関係が不正である場合。また、外部流体の熱
+            量が0以外の場合および外部流体以外の流体の熱量が0である場合。外部流体以外にコスト
+            を設定した場合。
+
+    Examples:
+        >>> Stream(0, 20, 300)
+        Stream(0, 20, 300, type_=StreamType.COLD, state=StreamState.UNKNOWN, cost=0.0, reboiler_or_reactor=False, id_="e0c1facb-538b-417f-862c-5cf8043ec075")
+        >>> Stream(20, 0, 300)
+        Stream(20, 0, 300, type_=StreamType.HOT, state=StreamState.UNKNOWN, cost=0.0, reboiler_or_reactor=False, id_="1a193fc7-9f34-4e6a-8e99-615d40be1b20")
+        >>> Stream(0, 0, 0)
+        Traceback (most recent call last):
+        ...
+        InvalidStreamError: 入り口温度と出口温度が同じ流体の種類は明示的に指定する必要があります。
+    """
 
     def __init__(
         self,
         input_temperature: float,
         output_temperature: float,
         heat_flow: float,
-        type_: StreamType,
-        cost: float = 0.0
+        type_: StreamType = StreamType.AUTO,
+        state: StreamState = StreamState.UNKNOWN,
+        cost: float = 0.0,
+        reboiler_or_reactor: bool = False,
+        id_: str = ''
     ):
-        """流体を表すクラス。
-
-        Args:
-            input_temperature (float): 入り口温度
-            output_temperature (float): 出口温度
-            heat_flow (float): 熱量
-            type_ (StreamType): 流体種
-            cost (float, optional): 流体のコスト。外部流体の場合のみ設定できる。
-
-        Raises:
-            InvalidStreamError:
-                入り口温度と出口温度の大小関係と流体種の関係が不正である場合。また、外部流体
-                の熱量が0以外の場合、および外部流体以外の流体の熱量が0である場合。
-        """
-        self.id_ = 0
-        self.type_ = type_
+        if id_:
+            self.id_ = id_
+        else:
+            self.id_ = str(uuid.uuid4())
+
+        if type_ == StreamType.AUTO:
+            if input_temperature < output_temperature:
+                self.type_ = StreamType.COLD
+            elif input_temperature > output_temperature:
+                self.type_ = StreamType.HOT
+            else:
+                raise InvalidStreamError(
+                    '入り口温度と出口温度が同じ流体の種類は明示的に指定する必要があります。'
+                )
+        else:
+            self.type_ = type_
 
         if self.is_internal() and heat_flow == 0:
             raise InvalidStreamError(
@@ -52,23 +93,23 @@ def __init__(
             raise InvalidStreamError(
                 "受熱流体は入り口温度が出口温度より低い必要があります。"
                 f"流体の種類: {self.type_.describe()} "
-                f"入り口温度: {input_temperature} "
-                f"出口温度: {output_temperature}"
+                f"入り口温度 [℃]: {input_temperature} "
+                f"出口温度 [℃]: {output_temperature}"
             )
 
         if self.is_hot() and input_temperature < output_temperature:
             raise InvalidStreamError(
                 "与熱流体は入り口温度が出口温度より高い必要があります。"
                 f"流体の種類: {self.type_.describe()} "
-                f"入り口温度: {input_temperature} "
-                f"出口温度: {output_temperature}"
+                f"入り口温度 [℃]: {input_temperature} "
+                f"出口温度 [℃]: {output_temperature}"
             )
 
         if self.is_internal() and cost != 0:
             raise InvalidStreamError(
                 "外部流体以外にはコストを設定できません。"
                 f"流体の種類: {self.type_.describe()} "
-                f"コスト: {cost} "
+                f"コスト [円]: {cost} "
             )
 
         self.cost = cost
@@ -78,72 +119,107 @@ def __init__(
         )
         self.heat_flow = heat_flow
 
+        self.state = state
+
+        if self.state in [StreamState.LIQUID_EVAPORATION, StreamState.GAS_CONDENSATION] \
+           and not self.is_isothermal():
+            raise InvalidStreamError(
+                '相変化によって熱交換を行う流体は等温である必要があります。'
+                f'流体の状態: {self.state.describe()} '
+                f'入口温度: {input_temperature} '
+                f'出口温度: {output_temperature}'
+            )
+
+        self.reboiler_or_reactor = reboiler_or_reactor
+
     def __repr__(self) -> str:
         return (
-            f"Stream({self.input_temperature()}, "
-            f"{self.output_temperature()}, "
-            f"{self.heat_flow}, "
-            f"{self.type_}, "
-            f"{self.cost})"
+            f'Stream('
+            f'{self.input_temperature()}, '
+            f'{self.output_temperature()}, '
+            f'{self.heat_flow}, '
+            f'type_={self.type_}, '
+            f'state={self.state}, '
+            f'cost={self.cost}, '
+            f'reboiler_or_reactor={self.reboiler_or_reactor}, '
+            f'id_="{self.id_}"'
+            ')'
         )
 
     def __str__(self) -> str:
         return (
-            f"{self.type_.describe()}, "
-            f"input [℃]: {self.input_temperature()}, "
-            f"output [℃]: {self.output_temperature()}, "
-            f"heat flow [W]: {self.heat_flow}"
+            f'{self.type_.describe()}, '
+            f'input [℃]: {self.input_temperature()}, '
+            f'output [℃]: {self.output_temperature()}, '
+            f'heat flow [W]: {self.heat_flow}'
         )
 
     def __format__(self, format_spec: str) -> str:
         description = self.type_.describe()
         return (
-            f"{description},{'':{14 - len(description)}s}"
-            f"input [℃]: {self.input_temperature().__format__(format_spec)}, "
-            f"output [℃]: {self.output_temperature().__format__(format_spec)}, "
-            f"heat flow [W]: {self.heat_flow.__format__(format_spec)}"
+            f'{description},{"":{14 - len(description)}s}'
+            f'input [℃]: {self.input_temperature().__format__(format_spec)}, '
+            f'output [℃]: {self.output_temperature().__format__(format_spec)}, '
+            f'heat flow [W]: {self.heat_flow.__format__(format_spec)}'
         )
 
-    def set_id(self, id_: int) -> None:
-        """流体にidを設定する。
+    def sort_key(self) -> float:
+        """ソートの際に用いるキーを返します。
 
-        Args:
-            id_ (int): 設定するid。
+        与熱流体は出口温度、受熱温度は入口温度を返します。
 
-        Raises:
-            ValueError: idが0以下であった場合。
+        Returns:
+            float: ソート時にキーとなる値。
         """
-        if id_ <= 0:
-            raise ValueError('idは0より大きい必要があります。')
-        self.id_ = id_
+        if self.is_hot():
+            return self.output_temperature()
+        return self.input_temperature()
 
     def is_external(self) -> bool:
         """外部流体であるかを返します。
+
+        Returns:
+            bool: 外部流体であるかどうか。
         """
         return self.type_ in [StreamType.EXTERNAL_HOT, StreamType.EXTERNAL_COLD]
 
     def is_internal(self) -> bool:
         """外部流体以外であるかを返します。
+
+        Returns:
+            bool: 外部流体以外であるかどうか。
         """
         return not self.is_external()
 
     def is_hot(self) -> bool:
         """与熱流体であるかを返します。
+
+        Returns:
+            bool: 与熱流体であるかどうか。
         """
         return self.type_ in [StreamType.HOT, StreamType.EXTERNAL_HOT]
 
     def is_cold(self) -> bool:
         """受熱流体であるかを返します。
+
+        Returns:
+            bool: 受熱流体であるかどうか。
         """
         return not self.is_hot()
 
     def is_isothermal(self) -> bool:
-        """温度変化がない流体かを返します。
+        """等温流体かを返します。
+
+        Returns:
+            bool: 等温流体であるかどうか。
         """
         return math.isclose(self.temperature_range.delta, 0.0)
 
     def input_temperature(self) -> float:
         """入り口温度を返します。
+
+        Returns:
+            float: 入り口温度。
         """
         if self.is_hot():
             return self.temperature_range.finish
@@ -151,6 +227,9 @@ def input_temperature(self) -> float:
 
     def output_temperature(self) -> float:
         """出口温度を返します。
+
+        Returns:
+            float: 出口温度。
         """
         if self.is_hot():
             return self.temperature_range.start
@@ -158,16 +237,25 @@ def output_temperature(self) -> float:
 
     def temperature(self) -> float:
         """温度変化を返します。
+
+        Returns:
+            float: 温度変化 [℃]。
         """
         return self.temperature_range.delta
 
     def heat(self) -> float:
         """熱量を返します。
+
+        Returns:
+            float: 熱量 [W]。
         """
         return self.heat_flow
 
     def temperatures(self) -> tuple[float, float]:
         """入り口温度と出口温度を返します。
+
+        Returns:
+            tuple[float, float]: 温度範囲。
         """
         return self.temperature_range()
 
@@ -184,6 +272,9 @@ def contain_temperature(self, temperature: float) -> bool:
 
         Args:
             temperature (float): 検証したい温度。
+
+        Returns:
+            bool: 与えられた温度をとるかどうか。
         """
         return temperature in self.temperature_range
 
@@ -194,6 +285,9 @@ def contain_temperatures(self, temperatures: Iterable[float]) -> bool:
 
         Args:
             temperatures (list[float]): 検証したい温度のリスト。
+
+        Returns:
+            bool: 与えられた複数の温度をとるかどうか。
         """
         return all(map(lambda t: t in self.temperature_range, temperatures))
 
@@ -208,8 +302,8 @@ def update_temperature(
         度変化の比に従って更新します。
 
         Args:
-            input_temperature (float): 更新する入り口温度。
-            output_temperature (float): 更新する出口温度。
+            input_temperature (float): 更新する入り口温度 [℃]。
+            output_temperature (float): 更新する出口温度 [℃]。
 
         Raises:
             ValueError: 等温流体に対して温度を更新しようとした場合。
@@ -239,16 +333,48 @@ def update_heat(self, heat_flow: float) -> None:
         self.heat_flow = heat_flow
 
 
+def is_valid_streams(streams: list[Stream]) -> bool:
+    """与熱流体および受熱流体がそれぞれ1つ以上含まれるかどうかを返します。
+
+    Args:
+        streams (list[Stream]): 流体のリスト。
+
+    Returns:
+        bool: 流体のリストが不正かどうか。
+
+    Example:
+        >>> is_valid_streams([
+                Stream(40, 90, 150),
+                Stream(80, 110, 180),
+                Stream(125, 80, 160),
+                Stream(100, 60, 160)
+            ])
+        True
+        >>> is_valid_streams([
+                Stream(40, 90, 150),
+                Stream(80, 110, 180)
+            ])
+        False
+    """
+
+    hot_streams = [stream for stream in streams if stream.is_hot()]
+    cold_streams = [stream for stream in streams if stream.is_cold()]
+
+    if not hot_streams or not cold_streams:
+        return False
+    return True
+
+
 def get_temperature_range_streams(
     streams: list[Stream]
 ) -> tuple[
     list[TemperatureRange],
     defaultdict[TemperatureRange, set[Stream]]
 ]:
-    """温度領域と、温度領域に属する流体を返します。
+    """温度領域に属する流体を返します。
 
     Args:
-        streams (list[Stream]): 流体。
+        streams (list[Stream]): 流体のリスト。
 
     Returns:
             list[list[TemperatureRange], defaultdict[TemperatureRange, set[Stream]]:
@@ -285,6 +411,15 @@ def get_temperature_range_streams(
 def get_temperature_range_heats(
     streams: list[Stream]
 ) -> tuple[list[TemperatureRange], dict[TemperatureRange, float]]:
+    """温度領域ごとに必要な熱量を返します。
+
+    Args:
+        streams (list[Stream]): 流体のリスト。
+
+    Returns:
+        tuple[list[TemperatureRange], dict[TemperatureRange, float]]:
+            温度領域、温度領域ごとの必要熱量。
+    """
     temp_ranges, temp_range_streams = get_temperature_range_streams(streams)
     tem_range_heats = {
         temp_range: sum(stream.heat() for stream in temp_range_streams[temp_range])
diff --git a/src/pyheatintegration/temperature_range.py b/src/pyheatintegration/temperature_range.py
index 3914510..8d571de 100644
--- a/src/pyheatintegration/temperature_range.py
+++ b/src/pyheatintegration/temperature_range.py
@@ -56,6 +56,11 @@ def is_continuous(temp_ranges_: list[TemperatureRange]) -> Optional[tuple[float,
 
     Returns:
         Optional[tuple[float, float]]: 領域が連続であるか。
+
+    Examples:
+        >>> is_continuous([TemperatureRange(0, 10), TemperatureRange(10, 20)])
+        >>> is_continuous([TemperatureRange(0, 10), TemperatureRange(20, 40)])
+        (10, 20)
     """
     temp_ranges = sorted(temp_ranges_)
     for i in range(len(temp_ranges)):
@@ -74,27 +79,27 @@ def get_temperatures(temp_ranges_: list[TemperatureRange]) -> list[float]:
     Returns:
         list[float]: 温度のリスト。
 
+    Raises:
+        ValueError: 温度領域が連続でない場合。
+
     Examples:
         >>> get_temperatures([TemperatureRange(0, 10), TemperatureRange(10, 20)])
         [0, 10, 20]
+        >>> get_temperatures([TemperatureRange(0, 10), TemperatureRange(30, 40)])
+        Traceback (most recent call last):
+        ...
+        ValueError: 終了値と開始値が異なります。終了値: 10.000 開始値: 30.000
     """
     temp_ranges = sorted(temp_ranges_)
     if (values := is_continuous(temp_ranges)) is not None:
         raise ValueError(
             f'終了値と開始値が異なります。'
-            f'finish: {values[0]}, '
-            f'start: {values[1]}'
+            f'終了値: {values[0]:.3f} '
+            f'開始値: {values[1]:.3f}'
         )
+
     res: list[float] = []
     for i in range(len(temp_ranges)):
-        if i != len(temp_ranges) - 1:
-            if temp_ranges[i].finish != temp_ranges[i + 1].start:
-                raise ValueError(
-                    f'終了値と開始値が異なります。'
-                    f'finish: {temp_ranges[i].finish}, '
-                    f'start: {temp_ranges[i + 1].start}'
-                )
-
         res.append(temp_ranges[i].start)
         if i == len(temp_ranges) - 1:
             res.append(temp_ranges[i].finish)
diff --git a/src/pyheatintegration/tq_diagram.py b/src/pyheatintegration/tq_diagram.py
index 8c23686..9f169ad 100644
--- a/src/pyheatintegration/tq_diagram.py
+++ b/src/pyheatintegration/tq_diagram.py
@@ -1,8 +1,8 @@
 import math
+from collections.abc import Callable
 from copy import copy, deepcopy
-from typing import Callable, Optional
+from typing import Optional
 
-from .heat_exchanger import HeatExchanger
 from .heat_range import (REL_TOL_DIGIT, HeatRange, get_detailed_heat_ranges,
                          get_heat_ranges, get_heats)
 from .plot_segment import PlotSegment, get_plot_segments, is_continuous
@@ -267,12 +267,16 @@ def _merge_segments(
             merged_hot_plot_segments.append(PlotSegment(
                 *merged_heat_range(),
                 *merged_hot_temp_range(),
-                hot_plot_segment.uuid
+                hot_plot_segment.uuid,
+                hot_plot_segment.state,
+                hot_plot_segment.reboiler_or_reactor
             ))
             merged_cold_plot_segments.append(PlotSegment(
                 *merged_heat_range(),
                 *merged_cold_temp_range(),
-                cold_plot_segment.uuid
+                cold_plot_segment.uuid,
+                cold_plot_segment.state,
+                cold_plot_segment.reboiler_or_reactor
             ))
             merged_heat_ranges.extend([heat_ranges[i], heat_ranges[i + 1]])
 
@@ -286,30 +290,18 @@ def _merge_segments(
 
 
 def get_possible_minimum_temp_diff_range(
-    streams: list[Stream]
+    streams: list[Stream],
+    ignore_maximum: bool = False
 ) -> TemperatureRange:
     """設定可能な最小接近温度差を返します。
 
     Args:
         streams (list[Stream]): 流体のリスト。
+        ignore_maximum (bool): 最大値のチェックを無視するか。
 
     Returns:
         float: 可能な最小接近温度差[℃]。
     """
-    cold_streams = sorted(
-        [stream for stream in streams if stream.is_internal() and stream.is_cold()],
-        key=lambda stream: stream.input_temperature()
-    )
-    hot_streams = sorted(
-        [stream for stream in streams if stream.is_internal() and stream.is_hot()],
-        key=lambda stream: stream.output_temperature()
-    )
-
-    if not hot_streams:
-        raise RuntimeError('与熱流体は少なくとも1つは指定する必要があります。')
-    if not cold_streams:
-        raise RuntimeError('受熱流体は少なくとも1つは指定する必要があります。')
-
     hot_maximum_temp = max(
         stream.input_temperature() for stream in streams if stream.is_hot()
     )
@@ -326,14 +318,35 @@ def get_possible_minimum_temp_diff_range(
         stream.input_temperature() for stream in streams if stream.is_cold()
     )
 
-    maximum_minimum_approch_temp_diff = min(
-        hot_maximum_temp - cold_maximum_temp,
-        hot_minimum_temp - cold_minimum_temp
-    )
+    if ignore_maximum:
+        maximum_minimum_approch_temp_diff = hot_maximum_temp - cold_minimum_temp
+    else:
+        if hot_minimum_temp - cold_minimum_temp < 0:
+            raise ValueError(
+                '与熱流体の最低温度が受熱流体の最低温度を下回っています。'
+                f'与熱流体最低温度: {hot_minimum_temp:.3f} ℃ '
+                f'受熱流体最低温度: {cold_minimum_temp:.3f} ℃'
+            )
+
+        if hot_maximum_temp - cold_maximum_temp < 0:
+            raise ValueError(
+                '与熱流体の最高温度が受熱流体の最高温度を下回っています。'
+                f'与熱流体最高温度: {hot_maximum_temp:.3f} ℃ '
+                f'受熱流体最高温度: {cold_maximum_temp:.3f} ℃'
+            )
+
+        maximum_minimum_approch_temp_diff = min(
+            hot_maximum_temp - cold_maximum_temp,
+            hot_minimum_temp - cold_minimum_temp
+        )
 
     # 与熱流体と受熱流体のセグメントを得る。
-    initial_hcc = _create_composite_curve(hot_streams)
-    initial_ccc = _create_composite_curve(cold_streams)
+    initial_hcc = _create_composite_curve([
+        stream for stream in streams if stream.is_internal() and stream.is_hot()
+    ])
+    initial_ccc = _create_composite_curve([
+        stream for stream in streams if stream.is_internal() and stream.is_cold()
+    ])
 
     initial_heat_ranges = get_detailed_heat_ranges(
         [
@@ -390,6 +403,8 @@ def get_possible_minimum_temp_diff_range(
             hot_finish_temp - cold_finish_temp
         )
 
+    minimum_minimum_approch_temp_diff = max(0, minimum_minimum_approch_temp_diff)
+
     return TemperatureRange(
         maximum_minimum_approch_temp_diff,
         minimum_minimum_approch_temp_diff
@@ -405,15 +420,16 @@ class TQDiagram:
         pinch_point_temp (float): ピンチポイントの温度[℃]。
 
     Attributes:
-        heat_exchangers (list[HeatExchanger]): 熱交換器のリスト。
-        hot_lines (list[Line]): TQ線図の与熱複合線。
-        cold_linse (list[Line]): TQ線図の受熱複合線。
-        hot_lines_separated (list[Line]): 流体ごとに分割した与熱複合線。
-        cold_lines_separated (list[Line]): 流体ごとに分割した受熱複合線。
-        hot_lines_splitted (list[Line]): 流体ごとに分割し、最小接近温度差を満たした与熱複合線。
-        cold_lines_splitted (list[Line]): 流体ごとに分割し、最小接近温度差を満たした受熱複合線。
-        hot_lines_merged (list[Line]): 熱交換器を結合した与熱複合線。
-        cold_lines_merged (list[Line]): 熱交換器を結合した受熱複合線。
+        hot_lines (list[Line]): TQ線図の与熱複合線(プロット用の直線のリスト)。
+        cold_linse (list[Line]): TQ線図の受熱複合線(プロット用の直線のリスト)。
+        hot_lines_separated (list[Line]): 流体ごとに分割した与熱複合線(プロット用の直線のリスト)。
+        cold_lines_separated (list[Line]): 流体ごとに分割した受熱複合線(プロット用の直線のリスト)。
+        hot_lines_splitted (list[Line]): 流体ごとに分割し、最小接近温度差を満たした与熱複合線(プロット用の直線のリスト)。
+        cold_lines_splitted (list[Line]): 流体ごとに分割し、最小接近温度差を満たした受熱複合線(プロット用の直線のリスト)。
+        hot_lines_merged (list[Line]): 熱交換器を結合した与熱複合線(プロット用の直線のリスト)。
+        cold_lines_merged (list[Line]): 熱交換器を結合した受熱複合線(プロット用の直線のリスト)。
+        hcc_merged (list[PlotSegment]): 熱交換器を結合した与熱複合線。
+        ccc_merged (list[PlotSegment]): 熱交換器を結合した受熱複合線。
     """
 
     def __init__(
@@ -456,31 +472,6 @@ def __init__(
         self.cold_lines_splitted = segments.cold_lines_splitted()
 
         # 結合可能なセグメント同士を結合する。
-        hcc_merged, ccc_merged = _merge_segments(segments)
-        self.hot_lines_merged = [plot_segment.line() for plot_segment in hcc_merged]
-        self.cold_lines_merged = [plot_segment.line() for plot_segment in ccc_merged]
-
-        all_heat_ranges = get_detailed_heat_ranges(
-            [
-                [plot_segment.heat_range for plot_segment in hcc_merged],
-                [plot_segment.heat_range for plot_segment in ccc_merged]
-            ]
-        )
-        hot_heat_range_plot_segment: dict[HeatRange, PlotSegment] = {
-            s.heat_range: s for s in get_plot_segments(all_heat_ranges, hcc_merged)
-        }
-        cold_heat_range_plot_segment: dict[HeatRange, PlotSegment] = {
-            s.heat_range: s for s in get_plot_segments(all_heat_ranges, ccc_merged)
-        }
-
-        self.heat_exchangers: list[HeatExchanger] = []
-        for heat_range in all_heat_ranges:
-            hot_plot_segment = hot_heat_range_plot_segment.get(heat_range, None)
-            cold_plot_segment = cold_heat_range_plot_segment.get(heat_range, None)
-
-            if hot_plot_segment is None or cold_plot_segment is None:
-                continue
-
-            self.heat_exchangers.append(
-                HeatExchanger(heat_range, hot_plot_segment, cold_plot_segment)
-            )
+        self.hcc_merged, self.ccc_merged = _merge_segments(segments)
+        self.hot_lines_merged = [plot_segment.line() for plot_segment in self.hcc_merged]
+        self.cold_lines_merged = [plot_segment.line() for plot_segment in self.ccc_merged]


Test patch:
diff --git a/tests/base_range_test.py b/tests/base_range_test.py
index b9c05b6..09aae0c 100644
--- a/tests/base_range_test.py
+++ b/tests/base_range_test.py
@@ -20,9 +20,17 @@ class TestBaseRange(unittest.TestCase):
 
     def test_base_range(self):
         base_range = BaseRange(0.0, 100.0)
+        other = BaseRange(200.0, 300.0)
 
         self.assertEqual(base_range.__repr__(), "BaseRange(0.0, 100.0)")
         self.assertEqual(base_range.__str__(), "0.0->100.0")
+        self.assertEqual(f"{base_range:.2f}", "0.00->100.00")
+
+        self.assertTrue(base_range != other)
+        self.assertTrue(base_range < other)
+        self.assertTrue(base_range <= other)
+        self.assertTrue(other > base_range)
+        self.assertTrue(other >= base_range)
 
         self.assertEqual(base_range.delta, 100)
         self.assertEqual(base_range.start, 0)
diff --git a/tests/heat_exchanger_test.py b/tests/heat_exchanger_test.py
new file mode 100644
index 0000000..fba245a
--- /dev/null
+++ b/tests/heat_exchanger_test.py
@@ -0,0 +1,30 @@
+import unittest
+
+from src.pyheatintegration.enums import StreamState
+from src.pyheatintegration.heat_exchanger import \
+    get_overall_heat_transfer_coefficient
+
+
+class TestGetOverallHeatTransferCoefficient(unittest.TestCase):
+
+    def test_success(self):
+        self.assertEqual(
+            get_overall_heat_transfer_coefficient(
+                StreamState.LIQUID, StreamState.LIQUID
+            ),
+            300.0
+        )
+
+    def test_failure(self):
+        with self.assertRaises(ValueError):
+            get_overall_heat_transfer_coefficient(
+                StreamState.LIQUID, StreamState.GAS_CONDENSATION
+            )
+
+            get_overall_heat_transfer_coefficient(
+                StreamState.LIQUID_EVAPORATION, StreamState.LIQUID
+            )
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/heat_range_test.py b/tests/heat_range_test.py
index 0ad5584..c365a6e 100644
--- a/tests/heat_range_test.py
+++ b/tests/heat_range_test.py
@@ -1,7 +1,9 @@
 import unittest
 
-from src.pyheatintegration.heat_range import (HeatRange, get_heat_ranges,
-                                              get_heats, is_continuous, get_detailed_heat_ranges)
+from src.pyheatintegration.heat_range import (HeatRange,
+                                              get_detailed_heat_ranges,
+                                              get_heat_ranges, get_heats,
+                                              is_continuous)
 
 
 class TestHeatRange(unittest.TestCase):
diff --git a/tests/line_test.py b/tests/line_test.py
new file mode 100644
index 0000000..1ae3e2f
--- /dev/null
+++ b/tests/line_test.py
@@ -0,0 +1,32 @@
+import unittest
+
+from src.pyheatintegration.line import extract_x, y_range
+
+
+class TestLine(unittest.TestCase):
+
+    def test_y_range(self):
+        self.assertEqual(
+            y_range([
+                ((0, 0), (1, 1)),
+                ((1, 1), (2, 2)),
+                ((2, 2), (3, 5)),
+                ((3, 3), (5, 8))
+            ]),
+            (0, 8)
+        )
+
+    def test_extract_x(self):
+        self.assertEqual(
+            extract_x([
+                ((0, 0), (1, 1)),
+                ((1, 1), (2, 2)),
+                ((2, 2), (3, 5)),
+                ((3, 3), (5, 8))
+            ]),
+            [0, 1, 2, 3, 5]
+        )
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/pinch_analyzer_test.py b/tests/pinch_analyzer_test.py
index d0c8883..0f4a500 100644
--- a/tests/pinch_analyzer_test.py
+++ b/tests/pinch_analyzer_test.py
@@ -6,7 +6,7 @@
 class TestPinchAnalyzer(unittest.TestCase):
 
     def test(self):
-        with self.assertRaises(RuntimeError):
+        with self.assertRaises(ValueError):
             PinchAnalyzer([], 10.0)
 
 
diff --git a/tests/segment_test.py b/tests/segment_test.py
index 454a8b2..318f9ef 100644
--- a/tests/segment_test.py
+++ b/tests/segment_test.py
@@ -1,10 +1,10 @@
 import unittest
 
+from src.pyheatintegration.enums import StreamType
 from src.pyheatintegration.heat_range import HeatRange
+from src.pyheatintegration.plot_segment import PlotSegment
 from src.pyheatintegration.segment import Segment
 from src.pyheatintegration.stream import Stream
-from src.pyheatintegration.enums import StreamType
-from src.pyheatintegration.plot_segment import PlotSegment
 
 
 class TeseSegment(unittest.TestCase):
diff --git a/tests/stream_test.py b/tests/stream_test.py
index 9cedb8e..20908ca 100644
--- a/tests/stream_test.py
+++ b/tests/stream_test.py
@@ -3,7 +3,9 @@
 
 from src.pyheatintegration.enums import StreamType
 from src.pyheatintegration.errors import InvalidStreamError
-from src.pyheatintegration.stream import Stream, get_temperature_range_streams
+from src.pyheatintegration.stream import (Stream,
+                                          get_temperature_range_streams,
+                                          is_valid_streams)
 from src.pyheatintegration.temperature_range import TemperatureRange
 
 
@@ -67,5 +69,21 @@ def test_get_temperature_range_streams(self):
                 self.assertEqual(result_stream.type_, expected_stream.type_)
 
 
+class TestIsValidStreams(unittest.TestCase):
+
+    def test_is_valid_streams(self):
+        self.assertTrue(is_valid_streams([
+            Stream(40, 90, 150),
+            Stream(80, 110, 180),
+            Stream(125, 80, 160),
+            Stream(100, 60, 160)
+        ]))
+
+        self.assertFalse(is_valid_streams([
+            Stream(40, 90, 150),
+            Stream(80, 110, 180)
+        ]))
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/temperature_range_test.py b/tests/temperature_range_test.py
index 84fb5b9..aad1d9d 100644
--- a/tests/temperature_range_test.py
+++ b/tests/temperature_range_test.py
@@ -1,8 +1,8 @@
 import unittest
 
 from src.pyheatintegration.temperature_range import (
-    TemperatureRange, get_temperature_ranges, get_temperature_transition,
-    is_continuous, accumulate_heats)
+    TemperatureRange, accumulate_heats, get_temperature_ranges,
+    get_temperature_transition, is_continuous)
 
 
 class TestGetTemperatureRange(unittest.TestCase):