第4章:プロセスフローシートシミュレーション

複数ユニットの統合、リサイクルストリームの収束、最適化

📖 読了時間: 35-40分 📊 難易度: 中級 💻 コード例: 8個

学習目標

この章を読むことで、以下を習得できます:


4.1 フローシートのトポロジー表現

グラフ理論によるフローシート表現

化学プロセスのフローシートは有向グラフ(Directed Graph)として表現できます。各ユニットはノード、物質・エネルギーストリームはエッジに対応します。

"""
Example 1: Flowsheet Topology Representation
フローシートトポロジーの表現(有向グラフ)
"""
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
from typing import Dict, List, Tuple

class FlowsheetTopology:
    """フローシートのトポロジーを管理するクラス"""

    def __init__(self):
        self.graph = nx.DiGraph()
        self.streams = {}  # ストリーム情報

    def add_unit(self, unit_name: str, unit_type: str):
        """ユニットを追加"""
        self.graph.add_node(unit_name, type=unit_type)

    def add_stream(self, from_unit: str, to_unit: str,
                   stream_name: str, stream_data: Dict = None):
        """ストリームを追加"""
        self.graph.add_edge(from_unit, to_unit, name=stream_name)
        if stream_data:
            self.streams[stream_name] = stream_data

    def get_calculation_order(self) -> List[str]:
        """トポロジカルソートで計算順序を決定"""
        try:
            # リサイクルがない場合は単純なトポロジカルソート
            return list(nx.topological_sort(self.graph))
        except nx.NetworkXError:
            # サイクル(リサイクル)がある場合
            return self._handle_recycle()

    def _handle_recycle(self) -> List[str]:
        """リサイクルストリームを含む計算順序の決定"""
        # 最も影響の小さいエッジを切断してトポロジカルソート
        cycles = list(nx.simple_cycles(self.graph))
        print(f"Recycle loops detected: {cycles}")

        # リサイクルストリームを特定(簡易版:最初のサイクルのみ)
        if cycles:
            recycle_edge = (cycles[0][-1], cycles[0][0])
            temp_graph = self.graph.copy()
            temp_graph.remove_edge(*recycle_edge)
            order = list(nx.topological_sort(temp_graph))
            print(f"Calculation order (with recycle): {order}")
            return order
        return []

    def visualize(self):
        """フローシートを可視化"""
        plt.figure(figsize=(10, 6))
        pos = nx.spring_layout(self.graph, seed=42)

        # ノードの色分け(ユニットタイプ別)
        node_colors = []
        for node in self.graph.nodes():
            unit_type = self.graph.nodes[node].get('type', 'unknown')
            colors = {'reactor': '#ff6b6b', 'separator': '#4ecdc4',
                     'heater': '#ffe66d', 'cooler': '#95e1d3'}
            node_colors.append(colors.get(unit_type, '#gray'))

        nx.draw(self.graph, pos, with_labels=True,
               node_color=node_colors, node_size=2000,
               font_size=10, font_weight='bold',
               arrows=True, arrowsize=20, edge_color='#666')

        # エッジラベル(ストリーム名)
        edge_labels = nx.get_edge_attributes(self.graph, 'name')
        nx.draw_networkx_edge_labels(self.graph, pos, edge_labels)

        plt.title("Process Flowsheet Topology")
        plt.axis('off')
        plt.tight_layout()
        plt.show()

# 使用例
flowsheet = FlowsheetTopology()

# ユニットを追加
flowsheet.add_unit("FEED", "feed")
flowsheet.add_unit("R-101", "reactor")
flowsheet.add_unit("T-101", "separator")
flowsheet.add_unit("PRODUCT", "product")

# ストリームを追加
flowsheet.add_stream("FEED", "R-101", "S-01")
flowsheet.add_stream("R-101", "T-101", "S-02")
flowsheet.add_stream("T-101", "PRODUCT", "S-03")
flowsheet.add_stream("T-101", "R-101", "S-04")  # Recycle

# 計算順序を取得
calc_order = flowsheet.get_calculation_order()
print(f"Calculation order: {calc_order}")

# 可視化
flowsheet.visualize()

計算順序の決定

トポロジカルソート(Topological Sort)により、上流から下流への計算順序が自動的に決定されます。リサイクルストリームがある場合は反復計算が必要です。


4.2 リサイクルストリームの収束計算

逐次代入法(Successive Substitution)

リサイクルストリームを含むフローシートでは、仮定値から開始して収束するまで反復計算を行います。

"""
Example 2: Recycle Stream Convergence
リサイクルストリームの収束計算(逐次代入法)
"""
import numpy as np
from scipy.optimize import fsolve

class RecycleConvergence:
    """リサイクルストリームの収束計算"""

    def __init__(self, max_iter=100, tol=1e-6):
        self.max_iter = max_iter
        self.tol = tol
        self.history = []

    def successive_substitution(self, flowsheet_func,
                                initial_guess, damping=0.5):
        """
        逐次代入法による収束計算

        Args:
            flowsheet_func: フローシート計算関数(入力→出力)
            initial_guess: リサイクルストリームの初期推定値
            damping: 減衰係数(0-1、収束安定性向上)

        Returns:
            converged_value: 収束値
            iterations: 反復回数
        """
        x_old = np.array(initial_guess)

        for iteration in range(self.max_iter):
            # フローシート計算
            x_new = flowsheet_func(x_old)

            # 減衰(収束性向上)
            x_damped = damping * x_new + (1 - damping) * x_old

            # 収束判定
            error = np.linalg.norm(x_damped - x_old)
            self.history.append({'iter': iteration, 'error': error,
                               'value': x_damped.copy()})

            print(f"Iter {iteration}: error = {error:.2e}")

            if error < self.tol:
                print(f"Converged in {iteration} iterations")
                return x_damped, iteration

            x_old = x_damped

        print("WARNING: Did not converge")
        return x_old, self.max_iter

    def wegstein_acceleration(self, flowsheet_func, initial_guess):
        """
        Wegstein加速法(収束性向上)

        逐次代入法より高速に収束する場合が多い
        """
        x0 = np.array(initial_guess)
        x1 = flowsheet_func(x0)

        for iteration in range(self.max_iter):
            x2 = flowsheet_func(x1)

            # Wegstein加速パラメータ
            q = (x2 - x1) / (x1 - x0 + 1e-10)
            q_safe = np.clip(q, -5, 0)  # 安定化

            # 加速
            x_new = (q_safe * x1 - x0) / (q_safe - 1)

            # 収束判定
            error = np.linalg.norm(x_new - x1)
            self.history.append({'iter': iteration, 'error': error})

            if error < self.tol:
                print(f"Wegstein converged in {iteration} iterations")
                return x_new, iteration

            x0, x1 = x1, x_new

        return x1, self.max_iter

# 使用例:簡単なリサイクルループ
def simple_recycle_flowsheet(recycle_flow):
    """
    簡単なリサイクルフローシート
    recycle_flow: リサイクル流量 [kmol/h]
    returns: 新しいリサイクル流量
    """
    feed_flow = 100.0  # kmol/h
    conversion = 0.8

    total_feed = feed_flow + recycle_flow
    product = total_feed * conversion
    recycle_new = total_feed * (1 - conversion)

    return np.array([recycle_new])

# 収束計算
solver = RecycleConvergence(tol=1e-6)
initial = np.array([10.0])  # 初期推定値

# 逐次代入法
recycle_conv, iters = solver.successive_substitution(
    simple_recycle_flowsheet, initial, damping=0.7
)
print(f"Converged recycle flow: {recycle_conv[0]:.2f} kmol/h")

4.3 完全フローシートシミュレーション

蒸留塔+反応器+分離器の統合

実用的なフローシートでは、複数のユニットが相互作用します。ここでは蒸留塔で精製した原料を反応器で反応させ、生成物を分離するプロセスを構築します。

"""
Example 3: Complete Flowsheet Simulation
完全フローシートシミュレーション(蒸留+反応+分離+リサイクル)
"""
import numpy as np
from dataclasses import dataclass
from typing import Dict

@dataclass
class Stream:
    """ストリームのデータ構造"""
    flow: float  # kmol/h
    composition: Dict[str, float]  # mol fraction
    temperature: float  # K
    pressure: float  # bar

    def __repr__(self):
        return f"Stream(F={self.flow:.1f}, T={self.temperature:.1f}K)"

class CompleteFlowsheet:
    """完全なプロセスフローシート"""

    def __init__(self):
        self.streams = {}
        self.convergence_history = []

    def reactor(self, feed: Stream, conversion=0.75) -> Stream:
        """反応器: A → B"""
        product = Stream(
            flow=feed.flow,
            composition={
                'A': feed.composition['A'] * (1 - conversion),
                'B': feed.composition['A'] * conversion + feed.composition.get('B', 0)
            },
            temperature=feed.temperature + 50,  # 発熱反応
            pressure=feed.pressure
        )
        return product

    def distillation(self, feed: Stream, recovery_A=0.95) -> Tuple[Stream, Stream]:
        """蒸留塔: AとBを分離"""
        # 塔頂(A rich)
        distillate_flow = feed.flow * feed.composition['A'] * recovery_A
        distillate = Stream(
            flow=distillate_flow,
            composition={'A': 0.98, 'B': 0.02},
            temperature=350,
            pressure=1.5
        )

        # 塔底(B rich)
        bottoms_flow = feed.flow - distillate_flow
        bottoms = Stream(
            flow=bottoms_flow,
            composition={'A': 0.05, 'B': 0.95},
            temperature=400,
            pressure=1.5
        )

        return distillate, bottoms

    def mixer(self, streams: List[Stream]) -> Stream:
        """ミキサー:複数ストリームの混合"""
        total_flow = sum(s.flow for s in streams)

        # 物質収支
        comp_A = sum(s.flow * s.composition['A'] for s in streams) / total_flow
        comp_B = sum(s.flow * s.composition['B'] for s in streams) / total_flow

        mixed = Stream(
            flow=total_flow,
            composition={'A': comp_A, 'B': comp_B},
            temperature=sum(s.flow * s.temperature for s in streams) / total_flow,
            pressure=min(s.pressure for s in streams)
        )
        return mixed

    def simulate(self, feed: Stream, recycle_ratio=0.5,
                max_iter=50, tol=1e-4):
        """
        完全フローシートシミュレーション

        フロー: Feed → Mixer → Reactor → Distillation
                             ↑              ↓
                             ← Recycle ←─── Distillate
        """
        # 初期リサイクル推定
        recycle = Stream(
            flow=feed.flow * recycle_ratio,
            composition={'A': 0.98, 'B': 0.02},
            temperature=350,
            pressure=1.5
        )

        for iteration in range(max_iter):
            # 1. ミキサー
            mixed = self.mixer([feed, recycle])

            # 2. 反応器
            reactor_out = self.reactor(mixed, conversion=0.75)

            # 3. 蒸留塔
            distillate, bottoms = self.distillation(reactor_out, recovery_A=0.95)

            # 4. リサイクル更新
            recycle_new = distillate

            # 収束判定
            error = abs(recycle_new.flow - recycle.flow)
            self.convergence_history.append(error)

            print(f"Iter {iteration}: Recycle flow error = {error:.2e} kmol/h")

            if error < tol:
                print(f"✓ Converged in {iteration} iterations")

                # 結果保存
                self.streams = {
                    'feed': feed,
                    'mixed': mixed,
                    'reactor_out': reactor_out,
                    'distillate': distillate,
                    'bottoms': bottoms,
                    'recycle': recycle_new
                }

                return self.streams

            recycle = recycle_new

        print("⚠ Did not converge")
        return self.streams

    def print_results(self):
        """結果の表示"""
        print("\n=== Flowsheet Simulation Results ===")
        for name, stream in self.streams.items():
            print(f"{name:15s}: {stream}")
            print(f"  Composition: A={stream.composition['A']:.3f}, "
                  f"B={stream.composition['B']:.3f}")

# 使用例
flowsheet = CompleteFlowsheet()

# フィード条件
feed = Stream(
    flow=100.0,
    composition={'A': 1.0, 'B': 0.0},
    temperature=300,
    pressure=2.0
)

# シミュレーション実行
results = flowsheet.simulate(feed, recycle_ratio=0.3)
flowsheet.print_results()

# 収束履歴をプロット
import matplotlib.pyplot as plt
plt.figure(figsize=(8, 5))
plt.semilogy(flowsheet.convergence_history, 'o-')
plt.xlabel('Iteration')
plt.ylabel('Recycle Flow Error [kmol/h]')
plt.title('Convergence History')
plt.grid(True, alpha=0.3)
plt.show()

4.4 感度分析(Sensitivity Analysis)

パラメータ変化の影響評価

操作条件の変化が製品収率やエネルギー消費に与える影響を定量化します。

"""
Example 4: Sensitivity Analysis
感度分析(パラメータ変化の影響評価)
"""
import numpy as np
import matplotlib.pyplot as plt

class SensitivityAnalyzer:
    """感度分析ツール"""

    def __init__(self, flowsheet):
        self.flowsheet = flowsheet

    def analyze_parameter(self, param_name, param_range,
                         output_metrics):
        """
        単一パラメータの感度分析

        Args:
            param_name: パラメータ名
            param_range: パラメータの範囲(array)
            output_metrics: 出力指標のリスト
        """
        results = {metric: [] for metric in output_metrics}

        for param_value in param_range:
            # パラメータ設定してシミュレーション
            # (実装は省略:flowsheet内のパラメータを変更)

            # 例:反応器転化率の影響
            conversion = param_value
            # ここでflowsheet.simulateを実行

            # 出力指標を計算
            yield_B = 0.75 * conversion  # 簡略化
            energy = 1000 + 500 * conversion  # kW

            results['yield'].append(yield_B)
            results['energy'].append(energy)

        return results

    def plot_sensitivity(self, param_name, param_range, results):
        """感度分析結果のプロット"""
        fig, axes = plt.subplots(1, len(results), figsize=(12, 4))

        if len(results) == 1:
            axes = [axes]

        for ax, (metric, values) in zip(axes, results.items()):
            ax.plot(param_range, values, 'o-', linewidth=2)
            ax.set_xlabel(param_name)
            ax.set_ylabel(metric)
            ax.grid(True, alpha=0.3)
            ax.set_title(f'{metric} vs {param_name}')

        plt.tight_layout()
        plt.show()

    def tornado_plot(self, params_impact):
        """
        トルネード図(複数パラメータの影響度比較)

        Args:
            params_impact: {param_name: (min_value, max_value), ...}
        """
        params = list(params_impact.keys())
        impacts = [abs(max_val - min_val)
                  for min_val, max_val in params_impact.values()]

        # 影響度順にソート
        sorted_idx = np.argsort(impacts)
        params_sorted = [params[i] for i in sorted_idx]
        impacts_sorted = [impacts[i] for i in sorted_idx]

        plt.figure(figsize=(8, 6))
        plt.barh(params_sorted, impacts_sorted, color='steelblue')
        plt.xlabel('Impact on Product Yield')
        plt.title('Tornado Diagram - Parameter Sensitivity')
        plt.grid(True, alpha=0.3, axis='x')
        plt.tight_layout()
        plt.show()

# 使用例
analyzer = SensitivityAnalyzer(flowsheet)

# 反応器転化率の感度分析
conversion_range = np.linspace(0.5, 0.95, 10)
results = {
    'yield': 100 * conversion_range * 0.75,
    'energy': 1000 + 500 * conversion_range
}

# プロット
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

ax1.plot(conversion_range, results['yield'], 'o-', linewidth=2)
ax1.set_xlabel('Reactor Conversion')
ax1.set_ylabel('Product Yield [kmol/h]')
ax1.grid(True, alpha=0.3)

ax2.plot(conversion_range, results['energy'], 'o-',
        linewidth=2, color='orangered')
ax2.set_xlabel('Reactor Conversion')
ax2.set_ylabel('Energy Consumption [kW]')
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

4.5 最適化(Optimization)

経済性と環境負荷のバランス

収益最大化、コスト最小化、環境負荷低減などの目的関数を最適化します。

"""
Example 5: Flowsheet Optimization
フローシート最適化(収益最大化)
"""
from scipy.optimize import minimize, differential_evolution
import numpy as np

class FlowsheetOptimizer:
    """フローシート最適化"""

    def __init__(self, flowsheet):
        self.flowsheet = flowsheet

    def objective_profit(self, x):
        """
        目的関数:利益最大化 = 収益 - コスト

        Args:
            x: 決定変数 [conversion, recycle_ratio]

        Returns:
            -profit (最小化問題に変換)
        """
        conversion, recycle_ratio = x

        # フローシートシミュレーション(簡略化)
        feed_flow = 100.0
        product_flow = feed_flow * conversion * 0.9

        # 収益
        product_price = 1000  # $/kmol
        revenue = product_flow * product_price

        # コスト
        raw_material_cost = feed_flow * 500  # $/kmol
        energy_cost = (1000 + 500 * conversion) * 0.1  # $/kW
        recycle_cost = recycle_ratio * 1000

        total_cost = raw_material_cost + energy_cost + recycle_cost

        profit = revenue - total_cost

        return -profit  # 最大化 → 最小化

    def constraints(self, x):
        """制約条件"""
        conversion, recycle_ratio = x

        # 制約:転化率は0.5-0.95、リサイクル比は0-0.8
        constraints = [
            conversion - 0.5,      # conversion >= 0.5
            0.95 - conversion,     # conversion <= 0.95
            recycle_ratio,         # recycle_ratio >= 0
            0.8 - recycle_ratio    # recycle_ratio <= 0.8
        ]

        return constraints

    def optimize_local(self, x0):
        """局所最適化(勾配法)"""
        bounds = [(0.5, 0.95), (0, 0.8)]

        result = minimize(
            self.objective_profit,
            x0,
            method='SLSQP',
            bounds=bounds,
            options={'disp': True}
        )

        return result

    def optimize_global(self):
        """大域最適化(Differential Evolution)"""
        bounds = [(0.5, 0.95), (0, 0.8)]

        result = differential_evolution(
            self.objective_profit,
            bounds,
            maxiter=100,
            popsize=15,
            disp=True
        )

        return result

# 使用例
optimizer = FlowsheetOptimizer(flowsheet)

# 初期推定値
x0 = np.array([0.75, 0.3])

# 局所最適化
print("=== Local Optimization ===")
result_local = optimizer.optimize_local(x0)
print(f"Optimal conversion: {result_local.x[0]:.4f}")
print(f"Optimal recycle ratio: {result_local.x[1]:.4f}")
print(f"Maximum profit: ${-result_local.fun:.2f}/h")

# 大域最適化
print("\n=== Global Optimization ===")
result_global = optimizer.optimize_global()
print(f"Optimal conversion: {result_global.x[0]:.4f}")
print(f"Optimal recycle ratio: {result_global.x[1]:.4f}")
print(f"Maximum profit: ${-result_global.fun:.2f}/h")

4.6 ヒートインテグレーション基礎

Pinch解析の導入

プロセス内の熱交換を最適化し、エネルギー消費を削減します。

"""
Example 6: Heat Integration Basics
ヒートインテグレーション基礎(ピンチ解析)
"""
import numpy as np
import matplotlib.pyplot as plt
from dataclasses import dataclass
from typing import List

@dataclass
class HeatStream:
    """熱ストリーム"""
    name: str
    type: str  # 'hot' or 'cold'
    T_supply: float  # K
    T_target: float  # K
    heat_capacity_flow: float  # kW/K

    @property
    def heat_load(self):
        """熱負荷 [kW]"""
        return abs(self.heat_capacity_flow * (self.T_target - self.T_supply))

class PinchAnalyzer:
    """Pinch解析ツール"""

    def __init__(self, min_approach_temp=10):
        """
        Args:
            min_approach_temp: 最小接近温度差 [K]
        """
        self.dT_min = min_approach_temp
        self.hot_streams = []
        self.cold_streams = []

    def add_stream(self, stream: HeatStream):
        """ストリームを追加"""
        if stream.type == 'hot':
            self.hot_streams.append(stream)
        else:
            self.cold_streams.append(stream)

    def calculate_composite_curves(self):
        """複合曲線を計算"""
        # 温度間隔の作成
        temps = []
        for stream in self.hot_streams + self.cold_streams:
            temps.extend([stream.T_supply, stream.T_target])
        temps = sorted(set(temps))

        # Hot composite curve
        hot_curve = []
        Q_hot = 0
        for T in sorted(temps, reverse=True):
            for stream in self.hot_streams:
                if stream.T_target <= T <= stream.T_supply:
                    dT = T - max(T - 1, stream.T_target)
                    Q_hot += stream.heat_capacity_flow * abs(dT)
            hot_curve.append((T, Q_hot))

        # Cold composite curve (shifted by dT_min)
        cold_curve = []
        Q_cold = 0
        for T in sorted(temps):
            for stream in self.cold_streams:
                if stream.T_supply <= T <= stream.T_target:
                    dT = min(T + 1, stream.T_target) - T
                    Q_cold += stream.heat_capacity_flow * abs(dT)
            cold_curve.append((T + self.dT_min, Q_cold))

        return hot_curve, cold_curve

    def find_pinch_point(self, hot_curve, cold_curve):
        """ピンチポイントを特定"""
        # 最小距離を探索(簡略化)
        min_distance = float('inf')
        pinch_temp = None

        for T_hot, Q_hot in hot_curve:
            for T_cold, Q_cold in cold_curve:
                if abs(T_hot - T_cold) < min_distance:
                    min_distance = abs(T_hot - T_cold)
                    pinch_temp = T_hot

        return pinch_temp, min_distance

    def calculate_utilities(self):
        """必要なユーティリティ(加熱・冷却)を計算"""
        total_hot_load = sum(s.heat_load for s in self.hot_streams)
        total_cold_load = sum(s.heat_load for s in self.cold_streams)

        if total_hot_load > total_cold_load:
            heating_utility = 0
            cooling_utility = total_hot_load - total_cold_load
        else:
            heating_utility = total_cold_load - total_hot_load
            cooling_utility = 0

        return heating_utility, cooling_utility

    def plot_composite_curves(self):
        """複合曲線をプロット"""
        hot_curve, cold_curve = self.calculate_composite_curves()

        plt.figure(figsize=(10, 6))

        # Hot composite curve
        T_hot, Q_hot = zip(*hot_curve)
        plt.plot(Q_hot, T_hot, 'r-', linewidth=2, label='Hot Composite')

        # Cold composite curve
        T_cold, Q_cold = zip(*cold_curve)
        plt.plot(Q_cold, T_cold, 'b-', linewidth=2, label='Cold Composite')

        plt.xlabel('Heat Load [kW]')
        plt.ylabel('Temperature [K]')
        plt.title('Composite Curves (Pinch Analysis)')
        plt.legend()
        plt.grid(True, alpha=0.3)
        plt.tight_layout()
        plt.show()

# 使用例
analyzer = PinchAnalyzer(min_approach_temp=10)

# 熱ストリームを追加
analyzer.add_stream(HeatStream('H1', 'hot', 450, 350, 20))
analyzer.add_stream(HeatStream('H2', 'hot', 400, 320, 15))
analyzer.add_stream(HeatStream('C1', 'cold', 300, 420, 18))
analyzer.add_stream(HeatStream('C2', 'cold', 330, 390, 12))

# ユーティリティ計算
heating, cooling = analyzer.calculate_utilities()
print(f"Heating utility required: {heating:.1f} kW")
print(f"Cooling utility required: {cooling:.1f} kW")

# 複合曲線をプロット
analyzer.plot_composite_curves()

4.7 ケーススタディ:化学プラント全体のシミュレーション

統合プロセス設計

実際の化学プラント設計では、反応、分離、熱交換、リサイクルを全て統合します。

"""
Example 7: Complete Chemical Plant Simulation
化学プラント全体のシミュレーション(統合ケーススタディ)
"""
import numpy as np
from dataclasses import dataclass, field
from typing import Dict, List

@dataclass
class PlantPerformance:
    """プラント性能指標"""
    production_rate: float  # kmol/h
    yield_overall: float  # %
    energy_consumption: float  # kW
    raw_material_usage: float  # kmol/h
    profit_per_hour: float  # $/h

    def display(self):
        print("\n=== Plant Performance ===")
        print(f"Production rate:    {self.production_rate:.2f} kmol/h")
        print(f"Overall yield:      {self.yield_overall:.2f} %")
        print(f"Energy consumption: {self.energy_consumption:.1f} kW")
        print(f"Raw material usage: {self.raw_material_usage:.2f} kmol/h")
        print(f"Profit:            ${self.profit_per_hour:.2f}/h")

class ChemicalPlant:
    """化学プラント全体のシミュレーター"""

    def __init__(self, config: Dict):
        self.config = config
        self.streams = {}
        self.unit_operations = {}

    def simulate_full_plant(self, feed_rate=100.0,
                           optimization_mode=False):
        """
        プラント全体のシミュレーション

        Process Flow:
        Feed → Preheater → Reactor → Flash → Distillation → Product
                  ↑           ↓                    ↓
              Heat Recovery   ←──── Recycle ───────┘
        """
        # 1. フィード予熱器
        feed_temp_in = 300  # K
        feed_temp_out = 400  # K
        preheat_duty = feed_rate * 2.5 * (feed_temp_out - feed_temp_in)

        # 2. 反応器
        conversion = self.config.get('conversion', 0.80)
        reaction_temp = 450  # K
        reaction_heat = feed_rate * conversion * 50  # kJ/kmol

        product_from_reactor = feed_rate * conversion
        unreacted = feed_rate * (1 - conversion)

        # 3. フラッシュ分離器
        vapor_frac = 0.3
        flash_vapor = (product_from_reactor + unreacted) * vapor_frac
        flash_liquid = (product_from_reactor + unreacted) * (1 - vapor_frac)

        # 4. 蒸留塔
        distillate_purity = 0.98
        distillate_recovery = 0.95

        distillate = flash_liquid * distillate_recovery * distillate_purity
        bottoms = flash_liquid - distillate

        # 5. リサイクル
        recycle_ratio = self.config.get('recycle_ratio', 0.5)
        recycle_flow = distillate * recycle_ratio

        # 6. エネルギー統合
        heat_recovery = 0.7 * reaction_heat  # 70%回収
        heating_utility = preheat_duty - heat_recovery
        cooling_utility = reaction_heat - heat_recovery

        total_energy = heating_utility + cooling_utility

        # 性能指標計算
        performance = PlantPerformance(
            production_rate=distillate * (1 - recycle_ratio),
            yield_overall=100 * distillate * (1 - recycle_ratio) / feed_rate,
            energy_consumption=total_energy,
            raw_material_usage=feed_rate,
            profit_per_hour=self._calculate_profit(
                distillate * (1 - recycle_ratio),
                feed_rate,
                total_energy
            )
        )

        return performance

    def _calculate_profit(self, product, feed, energy):
        """利益計算"""
        revenue = product * 1000  # $/kmol
        feed_cost = feed * 500
        energy_cost = energy * 0.1  # $/kW
        return revenue - feed_cost - energy_cost

    def optimize_operation(self):
        """運転最適化"""
        from scipy.optimize import minimize

        def objective(x):
            conversion, recycle_ratio = x
            self.config['conversion'] = conversion
            self.config['recycle_ratio'] = recycle_ratio

            perf = self.simulate_full_plant()
            return -perf.profit_per_hour  # 最大化→最小化

        result = minimize(
            objective,
            x0=[0.75, 0.4],
            bounds=[(0.6, 0.95), (0.2, 0.7)],
            method='SLSQP'
        )

        return result

# 使用例
config = {
    'conversion': 0.80,
    'recycle_ratio': 0.5,
    'min_approach_temp': 10
}

plant = ChemicalPlant(config)

# 通常運転
print("=== Normal Operation ===")
perf_normal = plant.simulate_full_plant(feed_rate=100.0)
perf_normal.display()

# 最適化運転
print("\n=== Optimized Operation ===")
result = plant.optimize_operation()
print(f"Optimal conversion: {result.x[0]:.4f}")
print(f"Optimal recycle ratio: {result.x[1]:.4f}")

perf_optimized = plant.simulate_full_plant(feed_rate=100.0)
perf_optimized.display()

# 改善率
improvement = (perf_optimized.profit_per_hour - perf_normal.profit_per_hour) / perf_normal.profit_per_hour * 100
print(f"\nProfit improvement: {improvement:.2f}%")

4.8 動的シミュレーション入門

時間変化を考慮したシミュレーション

起動・停止、外乱応答などの動的挙動を解析します。

"""
Example 8: Dynamic Simulation Introduction
動的シミュレーション入門(時間変化の考慮)
"""
import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt

class DynamicCSTR:
    """動的連続槽型反応器(CSTR)"""

    def __init__(self, volume=10.0, k=0.5, rho=1000, Cp=4.18):
        """
        Args:
            volume: 反応器容積 [m³]
            k: 反応速度定数 [1/min]
            rho: 密度 [kg/m³]
            Cp: 比熱 [kJ/kg/K]
        """
        self.V = volume
        self.k = k
        self.rho = rho
        self.Cp = Cp

    def model(self, y, t, F_in, C_in, T_in, Q_heat):
        """
        動的モデル(微分方程式)

        状態変数: y = [C, T]
        C: 濃度 [kmol/m³]
        T: 温度 [K]
        """
        C, T = y

        # 物質収支
        r = self.k * C  # 反応速度 [kmol/m³/min]
        dC_dt = (F_in / self.V) * (C_in - C) - r

        # エネルギー収支
        dH_r = -50  # 反応熱 [kJ/kmol]
        Q_reaction = r * dH_r * self.V

        dT_dt = (F_in / self.V) * (T_in - T) + \
                (Q_heat + Q_reaction) / (self.rho * self.V * self.Cp)

        return [dC_dt, dT_dt]

    def simulate(self, t_span, y0, F_in, C_in, T_in, Q_heat):
        """
        動的シミュレーション

        Args:
            t_span: 時間範囲 [min]
            y0: 初期状態 [C0, T0]
            F_in: 入口流量 [m³/min](時間の関数も可)
            C_in: 入口濃度 [kmol/m³]
            T_in: 入口温度 [K]
            Q_heat: 加熱量 [kW]
        """
        solution = odeint(self.model, y0, t_span,
                         args=(F_in, C_in, T_in, Q_heat))

        return solution

# 使用例:起動シミュレーション
reactor = DynamicCSTR(volume=10.0, k=0.5)

# 時間範囲
t = np.linspace(0, 60, 300)  # 0-60分

# 初期条件(空の反応器)
y0 = [0.0, 300.0]  # [C0=0, T0=300K]

# 運転条件
F_in = 1.0  # m³/min
C_in = 2.0  # kmol/m³
T_in = 320.0  # K
Q_heat = 50.0  # kW

# シミュレーション実行
solution = reactor.simulate(t, y0, F_in, C_in, T_in, Q_heat)

# 結果プロット
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))

# 濃度の時間変化
ax1.plot(t, solution[:, 0], 'b-', linewidth=2)
ax1.set_xlabel('Time [min]')
ax1.set_ylabel('Concentration [kmol/m³]')
ax1.set_title('CSTR Startup: Concentration Profile')
ax1.grid(True, alpha=0.3)

# 温度の時間変化
ax2.plot(t, solution[:, 1], 'r-', linewidth=2)
ax2.set_xlabel('Time [min]')
ax2.set_ylabel('Temperature [K]')
ax2.set_title('CSTR Startup: Temperature Profile')
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# 定常状態到達時間
C_steady = solution[-1, 0]
idx_90 = np.where(solution[:, 0] >= 0.9 * C_steady)[0][0]
print(f"Time to reach 90% of steady state: {t[idx_90]:.2f} min")

学習目標の確認

この章を完了すると、以下ができるようになります:

基本理解

実践スキル

応用力


まとめ

この章では、プロセスフローシートシミュレーションの実践的手法を習得しました:

次章の予告:第5章では、PythonとDWSIM等の商用シミュレーターを連携させ、より高度なプロセス設計を実現します。


免責事項