1. README(理論背景・数式の意味)
物質収支(Mass Balance)の一般化
希釈や混合の問題を解く際、最も確実な「正攻法」は、系(システム)に含まれる溶質の質量保存に注目することです。これを「物質収支」と呼びます。
支配方程式 (Governing Equations)
2つの溶液を混ぜる場合、以下の等式が成立します。
-
全質量の保存 (Total Mass Balance):
-
溶質の質量保存 (Solute Mass Balance):
ここで:
- : 加える溶液1の質量 [g]
- : 溶液1の濃度(比率)
- : 用意した溶液2(または水)の質量 [g]
- : 溶液2の濃度(比率、水なら0)
- : 目標濃度
一般解の導出
上記2式から混合後の質量 を消去し、求める質量 について解くと、以下の「てんびんの公式」が導かれます。
ご提示の式 は、(水)の場合のこの一般式を簡略化したものです。
体積換算と工学的解釈
求まった質量 を、比体積(1gあたりの体積) を用いて体積 に変換します。
このモデルを用いることで、濃度誤差が最終的な体積決定に与える影響(感度)を解析することが可能になります。
# ==========================================================
# 2. Setup Code (import)
# ==========================================================
import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
from IPython.display import display, clear_output
# Set plot style / プロットスタイルの設定
plt.style.use('seaborn-v0_8-muted')
# ==========================================================
# 3. TheoreticalModel
# ==========================================================
class TheoreticalModel:
"""
Handles HCl dilution math using solute mass conservation.
溶質の質量保存則に基づき塩酸希釈の計算を行うクラス。
"""
def __init__(self):
self.m_a = 0.0 # Mass of added acid [g] / 加える塩酸の質量
self.V_a = 0.0 # Volume of added acid [cm^3] / 加える塩酸の体積
def validate(self, C_a, C_t, m_w):
"""Validates physical constraints. / 物理的制約の検証"""
if not (0 < C_t < C_a):
return False, "Error: Target conc (C_t) must be between 0 and Source conc (C_a)."
if m_w <= 0:
return False, "Error: Water mass (m_w) must be positive."
return True, ""
def solve(self, m_w, C_a, C_t, v_s):
"""
Step-by-step solution derivation.
計算過程をステップバイステップで導出。
"""
# Step 1: Calculate Mass m_a = (m_w * C_t) / (C_a - C_t)
numerator = m_w * C_t
denominator = C_a - C_t
self.m_a = numerator / denominator
# Step 2: Convert to Volume V_a = m_a * v_s
self.V_a = self.m_a * v_s
# Transparency Output / 計算の透明性表示
print("--- Calculation Process (計算過程の透明性) ---")
print(f"1. Solute Balance Equation (質量収支式): m_a * C_a = (m_a + m_w) * C_t")
print(f"2. Rearranged for m_a (m_aの導出式): m_a = (m_w * C_t) / (C_a - C_t)")
print(f" Substitution (数値代入): m_a = ({m_w} * {C_t}) / ({C_a} - {C_t})")
print(f" => m_a = {numerator} / {denominator}")
print(f" => m_a = {self.m_a:.4f} g")
print(f"3. Volume Conversion (体積換算): V_a = m_a * v_spec")
print(f" Substitution (数値代入): V_a = {self.m_a:.4f} * {v_s}")
print(f" => V_a = {self.V_a:.4f} cm^3")
print("----------------------------------------------")
return self.m_a, self.V_a
# ==========================================================
# 4. Visualizer
# ==========================================================
class Visualizer:
"""
Plots the Area Diagram for concentration balance.
濃度のつり合いを示す面積図を描画するクラス。
"""
def __init__(self):
self.prev_state = None # To store ghost lines / ゴースト線用の前回データ保存
def plot_area(self, m_w, C_a, C_t, m_a):
fig, ax = plt.subplots(figsize=(10, 6))
# Current result: Area rectangles / 今回の結果:長方形
# Rectangle 1: Water Base (m_w)
ax.add_patch(plt.Rectangle((0, 0), m_w, 0, color='blue', alpha=0.1, label='Water Base (m_w)'))
# Rectangle 2: Added Acid (m_a)
ax.add_patch(plt.Rectangle((m_w, 0), m_a, C_a, color='red', alpha=0.4, label='Added Acid (m_a)'))
# Target Concentration line (C_t)
ax.axhline(y=C_t, color='green', linestyle='-', linewidth=2, label=f'Target Line C_t ({C_t}%)')
# Ghost line for previous result / ゴースト線(前回の結果)
if self.prev_state is not None:
p_mw, p_ma, p_ca, p_ct = self.prev_state
ax.plot([0, p_mw, p_mw, p_mw + p_ma, p_mw + p_ma], [0, 0, p_ca, p_ca, 0],
color='gray', linestyle='--', alpha=0.4, label='Previous Result (Ghost)')
ax.axhline(y=p_ct, color='gray', linestyle=':', alpha=0.4)
# Plot Settings (English Only) / グラフ設定
ax.set_title("Concentration Area Diagram: Solute Mass Conservation")
ax.set_xlabel("Cumulative Solution Mass [g]")
ax.set_ylabel("Concentration [%]")
ax.set_xlim(0, (m_w + m_a) * 1.2)
ax.set_ylim(0, C_a * 1.2)
ax.grid(True, linestyle=':', alpha=0.6)
ax.legend(loc='upper right')
plt.show()
# Update state for sensitivity analysis
self.prev_state = (m_w, m_a, C_a, C_t)
# ==========================================================
# 5. UIBuilder
# ==========================================================
class UIBuilder:
def __init__(self, model, viz):
self.model = model
self.viz = viz
self.output = widgets.Output()
# [Model Parameters: Solution Constants]
self.C_a_box = widgets.BoundedFloatText(value=35.0, min=0.1, max=100.0, description='C_a [%]:')
self.v_s_box = widgets.BoundedFloatText(value=0.85, min=0.01, max=5.0, description='v_spec [cm^3/g]:')
# [Observed / Input Data: Experimental Setup]
self.m_w_box = widgets.BoundedFloatText(value=125.0, min=0.1, max=10000.0, description='m_w [g]:')
self.C_t_box = widgets.BoundedFloatText(value=10.0, min=0.1, max=99.0, description='C_t [%]:')
# [Run Button]
self.run_btn = widgets.Button(description='Run Simulation (実行)', button_style='primary', icon='play')
self.run_btn.on_click(self._on_run)
# Grouped Layout
self.ui_layout = widgets.VBox([
widgets.HTML("<b>[Model Parameters: Physical Properties]</b>"),
widgets.HBox([self.C_a_box, self.v_s_box]),
widgets.HTML("<b>[Input Data: Desired Setup]</b>"),
widgets.HBox([self.m_w_box, self.C_t_box]),
widgets.HTML("<br>"),
self.run_btn
])
def _on_run(self, b):
with self.output:
clear_output(wait=True)
# 1. Validate / 検証
is_valid, msg = self.model.validate(self.C_a_box.value, self.C_t_box.value, self.m_w_box.value)
if not is_valid:
print(f"❌ {msg}")
return
# 2. Calculate / 計算
ma, _ = self.model.solve(
self.m_w_box.value,
self.C_a_box.value,
self.C_t_box.value,
self.v_s_box.value
)
# 3. Visualize / 描画
self.viz.plot_area(self.m_w_box.value, self.C_a_box.value, self.C_t_box.value, ma)
def display(self):
display(self.ui_layout, self.output)
# ==========================================================
# 6. Initialization
# ==========================================================
model_inst = TheoreticalModel()
viz_inst = Visualizer()
ui_inst = UIBuilder(model_inst, viz_inst)
ui_inst.display()