プロセス・パラメータの動的変更

バイオ・リファイナリー(再生可能資源であるバイオマスを原料にバイオ燃料や樹脂などを製造するプラントや技術)のシミュレーションソフト"BioSTEAM"で、プロセス・パラメータを動的に変更する方法について説明しています。 オリジナルのページはProcess specificationsです。 ソースコードは以下の実行環境で確認しています。
  • Visual Studio Code バージョン: 1.104.2
  • 拡張機能:Jupyter バージョン 2025.8.0
  • Python 3.12.10
  • biosteam 2.52.13
  • graphviz-14.0.2

プロセス・パラメータの動的変更

仕様を満たすためにプロセスのパラメータを動的に調整することは、生産プロセスを設計する上で極めて重要であり、特に不確実な条件がある場合ではなおさら重要です。 BioSTEAM はプロセス仕様を、分析的仕様と数値解析的仕様の 2 つのカテゴリーに分類しています。 分析的仕様とはシステムの単一ループ内で解くことが出来るもの、 一方、数値解析的仕様は、機器ユニットを繰り返し実行したり、リサイクル系を収束させて解く必要があるものとしています。 以下の実例によって、この点が詳しく説明されます。

分析的に求める仕様

バイオ・エタノール製造プラントでの変性剤投入工程

バイオエタノールの流量に応じて添加する変性剤の量を、出来上がり時に変性剤の比率が質量比で2%になるように調整します。

  1. 仕様を満たすための関数(specification function)
  2. from biosteam import settings, Chemical, Stream, units, main_flowsheet
    import biosteam as bst; bst.nbtutorial()
    # フローシートに名前を付けます
    main_flowsheet.set_flowsheet('mix_ethanol_with_denaturant')
    
    # 使用する成分の熱力学特性を設定します
    # 実際のプロセスではもっと多くの物質を扱うと思いますが、
    # ここでは例として少ない種類で試行します。
    settings.set_thermo(['Water', 'Ethanol', 'Octane'])
    
    # 330日の操業で年間 40 ミリオン ガロンのエタノール生産されているとします。
    dehydrated_ethanol = Stream('dehydrated_ethanol', T=340,
                                Water=0.1, Ethanol=99.9, units='kg/hr')
    operating_days_per_year = 330
    dehydrated_ethanol.set_total_flow(40e6 / operating_days_per_year, 'gal/d')
    denaturant = Stream('denaturant', Octane=1)
    M1 = units.Mixer('M1', ins=(dehydrated_ethanol, denaturant), outs='denatured_ethanol')
    
    # 仕様を満たすための関数(specification function)を設定します。
    # 変性剤が質量比で2%になるように設定します。
    @M1.add_specification(run=True) # 適用後に物質収支およびエネルギー収支を計算します。
    def adjust_denaturant_flow():
        denaturant_over_ethanol_flow = 0.02 / 0.98 # A mass ratio
        denaturant.imass['Octane'] = denaturant_over_ethanol_flow * dehydrated_ethanol.F_mass
    
    # 実行と結果の確認
    M1.simulate()
    M1.show(composition=True, flow='kg/hr')
    Mixer: M1
    ins...
    [0] dehydrated_ethanol  
        phase: 'l', T: 340 K, P: 101325 Pa
        composition (%): Water    0.1
                         Ethanol  99.9
                         -------  1.43e+04 kg/hr
    [1] denaturant  
        phase: 'l', T: 298.15 K, P: 101325 Pa
        composition (%): Octane  100
                         ------  292 kg/hr
    outs...
    [0] denatured_ethanol  
        phase: 'l', T: 339.31 K, P: 101325 Pa
        composition (%): Water    0.098
                         Ethanol  97.9
                         Octane   2
                         -------  1.46e+04 kg/hr
    outs[0]のOctaneの比率が質量比で2%になっていることが確認できます。混合後の体積流量は、
    M1.outs[0].get_total_flow('gal/d') * operating_days_per_year / 1e6
    40.88351940635338
    年間 約40.9 ミリオンガロンになりました。

    ちなみに、この@~Pythonのデコレータ構文[1]というもので、直後に書いた関数を元の関数の引数として実行してくれるもの(?)だそうです。

  3. 仕様リスト
  4. 全ての仕様(specifications)は、機器ユニットの仕様リストを見ることで確認できます。
    M1.specifications
    [ProcessSpecification(f=adjust_denaturant_flow(), args=(), impacted_units=())]
    argsimpacted_initsについては次のcorn slurryの節で説明します。

    従来型乾式粉砕工程におけるコーンスラリーの作成

    従来型の乾式粉砕トウモロコシをエタノール製造プラントに供給する際のコーンスラリーの固形分濃度は、通常約 32 wt.% である。粉砕トウモロコシに混合する水の流量を調整し、スラリーの固形分濃度が 32 wt.% となるようにせよ。

  5. 仕様追加関数に引数を渡す
  6. # 新しいフローシートに名前を付ける
    main_flowsheet.set_flowsheet('corn_slurry_example')
    
    # コーンの代表的な化学成分を設定
    Starch = Chemical.blank('Starch', phase='s')    # でんぷん(固体)
    Fiber = Chemical.blank('Fiber', phase='s')      # 繊維成分(固体)
    Oil = Chemical('Oil', search_ID='Oleic_acid')   # オレイン酸
    Water = Chemical('Water')
    
    # 実際の特性は今回は不要なので、デフォルトの物性値を設定(25℃、1atm)
    Starch.default()
    Fiber.default()
    
    # 熱力学特性もセット。
    # 実際の工程ではもっと多くの成分が関与するが、今回は代表的なもののみとする
    settings.set_thermo([Starch, Oil, Fiber, Water])
    
    # 代表的な乾式粉砕法では年間年間 4,000万ガロン(40,000,000 gal)のエタノールを生産し、
    # トウモロコシ 1 ブッシェル当たり 2.7 ガロンのエタノール収率が得られる
    corn_flow_per_year = 40e6 / 2.7 # ブッシェル/年
    days_per_year = 365
    operating_days_per_year = 330
    corn_flow_per_day = corn_flow_per_year * days_per_year / operating_days_per_year
    
    # トウモロコシの粒の成分は、でんぷん(62%)、タンパク質と繊維(19%)、水分(15%)、
    # 油分(4%)とします
    corn_feed = Stream('corn_feed',
        Starch=62, Fiber=19, Water=15, Oil=4,
        total_flow=corn_flow_per_day,
        units='bu/day', # bushel per day
    )
    T1 = bst.StorageTank('T1', corn_feed)
    
    # 粉砕したトウモロコシの粉をスラリーにするために加える水(希釈水)を設定
    dilution_water = Stream('dilution_water', Water=1)
    M1 = units.Mixer('M1', ins=(dilution_water, T1-0), outs='slurry')
    
    @M1.add_specification(
        run=True, # 仕様追加関数の実行後に物質収支およびエネルギー収支を計算
        args=[0.32], # `adjust_water_flow`関数に引数を渡す
    )
    def adjust_water_flow(solids_content):
        F_mass_moisture = corn_feed.imass['Water']
        F_mass_solids = corn_feed.F_mass - F_mass_moisture
        water_solids_ratio = (1 - solids_content) / solids_content
        dilution_water.imass['Water'] = F_mass_solids * water_solids_ratio - F_mass_moisture
    
    # 実行して結果を確認
    corn_slurry_sys = main_flowsheet.create_system('corn_slurry_sys')
    corn_slurry_sys.simulate()
    M1.show(flow='kg/hr', composition=True)
    Mixer: M1
    ins...
    [0] dilution_water  
        phase: 'l', T: 298.15 K, P: 101325 Pa
        composition (%): Water  100
                         -----  3.97e+07 kg/hr
    [1] s6  from  StorageTank-T1
        phase: 'l', T: 298.15 K, P: 101325 Pa
        composition (%): Starch  62.2
                         Oil     3.72
                         Fiber   19.1
                         Water   15
                         ------  2.4e+07 kg/hr
    outs...
    [0] slurry  
        phase: 'l', T: 298.15 K, P: 101325 Pa
        composition (%): Starch  23.4
                         Oil     1.4
                         Fiber   7.18
                         Water   68
                         ------  6.37e+07 kg/hr
    outs[0] slurry の水分が68%、つまり残りの水分以外の比率が32%となっていることが確認できます。add_specification()関数を使うことで、dilution_waterの流量を動的に変化させていますが、引数も渡すことで、units.Mixer()を変更することなく、比率も状況に応じて変えることが出来ました。

    希釈水を混合する前に加熱したいと思います。希釈水の流量が混合器で指定されると、その上流の熱交換器やポンプにも影響が及びます。流量決定後に直接影響を受けるこれらの機器が再計算されるようにimpacted_unitsで指示することが出来ます。

  7. impacted_unitsで影響を受ける機器を指示
  8. # ポンプと熱交換器を追加します。
    P1 = units.Pump('P1', dilution_water, P=5 * 101325)
    H1 = units.HXutility('H1', P1-0, T=350)
    M1.ins.append(H1-0)
    
    M1.specifications.clear() # 一旦、仕様追加関数を削除
    # "impacted units"を追加した、仕様追加関数を作成
    M1.add_specification(
        adjust_water_flow, # 仕様追加関数
        args=[0.32], # 関数に渡す引数
        run=True, #     run=True, # 仕様追加関数の実行後に物質収支およびエネルギー収支を計算
        impacted_units=[P1], # 希釈水(Dilution water)がポンプP1に接続されている
        # BioSTEAMは、影響を受ける機器P1と仕様を追加した機器(M1)の間にある機器も考慮します
    )
    
    # 以下のような指示の仕方も可能
    # M1.specifications[0].impacted_units = [P1]
    
    corn_slurry_sys = main_flowsheet.create_system('corn_slurry_sys')
    dilution_water.empty() # 計算を実行する前にリセット
    corn_slurry_sys.simulate()
    corn_slurry_sys.diagram(format='png', kind='cluster', number=True)
    corn_slurry_sys.show()
    System: corn_slurry_sys
    ins...
    [0] -  
        phase: 'l', T: 298.15 K, P: 101325 Pa
        flow: 0
    [1] dilution_water  
        phase: 'l', T: 298.15 K, P: 101325 Pa
        flow (kmol/hr): Water  2.2e+06
    [2] corn_feed  
        phase: 'l', T: 298.15 K, P: 101325 Pa
        flow (kmol/hr): Starch  1.49e+07
                        Oil     3.15e+03
                        Fiber   4.57e+06
                        Water   2e+05
    outs...
    [0] slurry  
        phase: 'l', T: 330.74 K, P: 101325 Pa
        flow (kmol/hr): Starch  1.49e+07
                        Oil     3.15e+03
                        Fiber   4.57e+06
                        Water   2.4e+06
    outs[0] slurry の水分が希釈水 dilution_water 分増えて、2.4e+06 kmol/hrになっています。指示通り、熱交換器H1の流量がセットされているか確認します。
  9. 熱交換器H1の流量
  10. H1.show()
    HXutility: H1
    ins...
    [0] s2  from  Pump-P1
        phase: 'l', T: 298.15 K, P: 506625 Pa
        flow (kmol/hr): Water  2.2e+06
    outs...
    [0] s3  to  Mixer-M1
        phase: 'l', T: 350 K, P: 506625 Pa
        flow (kmol/hr): Water  2.2e+06
    希釈水 dilution_water と同じ流量のストリームが流入して流出しています。では、impacted_unitsを設定しないとどうなるか試してみます。
  11. impacted_unitsなし
  12. M1.specifications.clear() # 一旦、仕様追加関数を削除
    # "impacted units"を追加した、仕様追加関数を作成
    M1.add_specification(
        adjust_water_flow, # 仕様追加関数
        args=[0.32], # 関数に渡す引数
        run=True, #     run=True, # 仕様追加関数の実行後に物質収支およびエネルギー収支を計算
        # BioSTEAMは、影響を受ける機器P1と仕様を追加した機器(M1)の間にある機器も考慮します
    )
    
    # 以下のような指示の仕方も可能
    # M1.specifications[0].impacted_units = [P1]
    
    corn_slurry_sys = main_flowsheet.create_system('corn_slurry_sys')
    dilution_water.empty() # 計算を実行する前にリセット
    corn_slurry_sys.simulate()
    corn_slurry_sys.show()
    System: corn_slurry_sys
    ins...
    [0] -  
        phase: 'l', T: 298.15 K, P: 101325 Pa
        flow: 0
    [1] dilution_water  
        phase: 'l', T: 298.15 K, P: 101325 Pa
        flow (kmol/hr): Water  2.2e+06
    [2] corn_feed  
        phase: 'l', T: 298.15 K, P: 101325 Pa
        flow (kmol/hr): Starch  1.49e+07
                        Oil     3.15e+03
                        Fiber   4.57e+06
                        Water   2e+05
    outs...
    [0] slurry  
        phase: 'l', T: 298.15 K, P: 101325 Pa
        flow (kmol/hr): Starch  1.49e+07
                        Oil     3.15e+03
                        Fiber   4.57e+06
                        Water   2e+05
    希釈水dilution_waterの流量は計算されていますが、流出ストリームは流入ストリームのcorn_feedそのままになっています。熱交換器H1の流量を確認します。
  13. 熱交換器H1の流量
  14. H1.show()
    HXutility: H1
    ins...
    [0] s2  from  Pump-P1
        phase: 'l', T: 298.15 K, P: 506625 Pa
        flow: 0
    outs...
    [0] s3  to  Mixer-M1
        phase: 'l', T: 350 K, P: 506625 Pa
        flow: 0
    熱交換器H1を通る経路が再計算されていないため、流量がゼロになっています。impacted_unitsの設定をする、しないでどのような違いが発生するか、確認できました。

    混合器 M1ではなく、貯蔵タンク T1に仕様追加関数を設定してみます。 ポンプ P1 と 熱交換器 H1 は並列であり、上流ではないため、もし P1 が T1 より先に実行されると、シミュレーションの順序に問題が生じる可能性がある、と思われるかもしれませんが、impacted_unitsを設定をすることで、BioSTEAMはシミュレーションの優先順位を判断することが出来ます。

  15. 貯蔵タンク T1に仕様追加関数を設定
  16. M1.specifications.clear() # Remove specifications
    # Create specification with impacted units.
    T1.add_specification(
        adjust_water_flow, # Specification function
        args=[0.32], # Arguments passed to function.
        run=True, # Run mass and energy balance after specification function.
        impacted_units=[P1], # Dilution water is connected to P1.
    )
    
    corn_slurry_sys = main_flowsheet.create_system('corn_slurry_sys')
    corn_slurry_sys.simulate()
    corn_slurry_sys.show()
    System: corn_slurry_sys
    ins...
    [0] -  
        phase: 'l', T: 298.15 K, P: 101325 Pa
        flow: 0
    [1] dilution_water  
        phase: 'l', T: 298.15 K, P: 101325 Pa
        flow (kmol/hr): Water  2.2e+06
    [2] corn_feed  
        phase: 'l', T: 298.15 K, P: 101325 Pa
        flow (kmol/hr): Starch  1.49e+07
                        Oil     3.15e+03
                        Fiber   4.57e+06
                        Water   2e+05
    outs...
    [0] slurry  
        phase: 'l', T: 330.74 K, P: 101325 Pa
        flow (kmol/hr): Starch  1.49e+07
                        Oil     3.15e+03
                        Fiber   4.57e+06
                        Water   2.4e+06
    outs[0] slurry の水分が希釈水 dilution_water 分増えて、2.4e+06 kmol/hrになっていて、問題なさそうです。

    数値解析的に求める仕様

    フラッシュ蒸留器の計算

    エタノールとプロパノールの混合液を、モル分率ではなく質量分率で50%蒸発させます。これは温度を変化させたときの蒸発率を繰り返し計算し、蒸気の質量と液体の質量が50%になる温度を数値解析的に計算することで解くことが出来ます。このとき、目的関数 f(x) = 0 となる x を求める解き方なので、目標が満足する状態で計算した結果がゼロになるような関数を与えます。ここでは、今の蒸発率が0.5のときにゼロになる → 蒸発率V - 0.5 を返す関数をセットします。

  17. 数値解析的仕様変更(add_bounded_numerical_specification)
  18. # 新しいフローシートに名前を付ける
    main_flowsheet.set_flowsheet('flash_specification_example')
    
    # 使用する成分の熱力学特性を設定
    settings.set_thermo(['Water', 'Ethanol', 'Propanol'])
    
    # 供給ストリーム
    mixture = Stream('mixture', T=340,
                     Water=1000, Ethanol=1000, Propanol=1000,
                     units='kg/hr')
    
    # フラッシュ蒸留器を設定
    F1 = units.Flash('F1',
                     ins=mixture,
                     outs=('vapor', 'liquid'),
                     T=373, P=101325)
    
    # 呼び出されたときに目的関数を解く数値解析的仕様変更関数を設定
    @F1.add_bounded_numerical_specification(x0=351.4, x1=373, xtol=1e-9, ytol=1e-3)
    def f(x):
        # 目的関数 f(x) = 0 となる x を求める
        # ここでは蒸発率 50 wt. % となる温度を求める
        F1.T = x
        F1.run() # 重要: 質量およびエネルギーバランスは新しい条件で計算
        feed = F1.ins[0]
        vapor = F1.outs[0]
        V = vapor.F_mass / feed.F_mass
        return V - 0.5
    
    # Now create the system, simulate, and check results.
    system = main_flowsheet.create_system()
    system.simulate()
    system.diagram(format='png')
    system.show('cwt')
    System: SYS2
    ins...
    [0] mixture  
        phase: 'l', T: 340 K, P: 101325 Pa
        composition (%): Water     33.3
                         Ethanol   33.3
                         Propanol  33.3
                         --------  3e+03 kg/hr
    outs...
    [0] vapor  
        phase: 'g', T: 357.66 K, P: 101325 Pa
        composition (%): Water     28.3
                         Ethanol   40.7
                         Propanol  31.1
                         --------  1.5e+03 kg/hr
    [1] liquid  
        phase: 'l', T: 357.66 K, P: 101325 Pa
        composition (%): Water     38.4
                         Ethanol   26
                         Propanol  35.6
                         --------  1.5e+03 kg/hr
    outs[0]の蒸気と、[1]の液体の質量流量が同じ(蒸気分率が50%)になり、温度が 357.66 K(84.51℃)と決まりました。

参考文献

このブログの人気の投稿

さあ、始めよう!

蒸留塔

機器ユニットの計算結果