Pythonコード
import flopy
import numpy as np
import matplotlib.pyplot as plt
import os
import shutil
import sys
import time
import csv
from phreeqpy.iphreeqc.phreeqc_dll import IPhreeqc
# ==========================================
# 0. 準備
# ==========================================
def get_exe_path():
try:
base_dir = os.path.dirname(os.path.abspath(__file__))
except NameError:
base_dir = os.getcwd()
exe_name = "mf6.exe"
exe_path = os.path.join(base_dir, exe_name)
if not os.path.exists(exe_path):
print(f"エラー: {exe_path} が見つかりません。")
sys.exit(1)
db_name = "llnl.dat"
db_path = os.path.join(base_dir, db_name)
if not os.path.exists(db_path):
print(f"エラー: {db_name} が見つかりません。")
sys.exit(1)
print(f"Using database: {db_path}")
return exe_path, db_path, base_dir
# ==========================================
# 1. Flopyによる流動モデル
# ==========================================
def setup_and_run_flow(exe_path, length, radius, n_cells, flow_rate_m3s, k_perms):
name = "column_flow"
ws = "workspace_column"
base_dir = os.path.dirname(exe_path)
ws_path = os.path.join(base_dir, ws)
if os.path.exists(ws_path): shutil.rmtree(ws_path)
os.makedirs(ws_path, exist_ok=True)
area = np.pi * radius**2
delr = length / n_cells
sim = flopy.mf6.MFSimulation(sim_name=name, exe_name=exe_path, sim_ws=ws_path)
tdis = flopy.mf6.ModflowTdis(sim, time_units='SECONDS', nper=1, perioddata=[(1.0, 1, 1.0)])
ims = flopy.mf6.ModflowIms(sim)
gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True)
sim.register_ims_package(ims, [gwf.name])
dis = flopy.mf6.ModflowGwfdis(gwf, nlay=1, nrow=1, ncol=n_cells,
delr=delr, delc=np.sqrt(area), top=length, botm=0.0)
npf = flopy.mf6.ModflowGwfnpf(gwf, k=k_perms, save_specific_discharge=True)
ic = flopy.mf6.ModflowGwfic(gwf, strt=1.0)
wel_spd = {0: [[(0, 0, 0), flow_rate_m3s]]}
wel = flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel_spd)
chd_spd = {0: [[(0, 0, n_cells-1), 1.0]]}
chd = flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd)
budget_file = f"{name}.cbc"
oc = flopy.mf6.ModflowGwfoc(gwf,
budget_filerecord=budget_file,
head_filerecord=f"{name}.hds",
saverecord=[('HEAD', 'ALL'), ('BUDGET', 'ALL')])
sim.write_simulation()
sim.run_simulation(silent=True)
cbc_path = os.path.join(ws_path, budget_file)
try:
cbb = flopy.utils.CellBudgetFile(cbc_path, precision='double')
spdis = cbb.get_data(text='DATA-SPDIS')[0]
qx = spdis['qx']
avg_q = np.mean(np.abs(qx))
return avg_q
except Exception as e:
print(f"Error reading budget: {e}")
return None
# ==========================================
# 2. PhreeqPyによる反応輸送モデル
# ==========================================
def run_reactive_transport_kinetics(darcy_velocity, length, n_cells, porosity, flow_rate_ml_min, db_path):
print("--- Step 2: Running PHREEQC Transport ---")
velocity = darcy_velocity / porosity
delr = length / n_cells
dt = delr / velocity
# ---------------------------------------------------------
# 時間設定の変更
# ---------------------------------------------------------
target_hours = 24.0 # 目標時間 [時間]
target_seconds = target_hours * 3600.0
total_steps = int(target_seconds / dt)
print(f"Target Time: {target_hours} hours")
print(f"Flow Rate : {flow_rate_ml_min} mL/min")
print(f"Delta t : {dt:.2f} sec (Time per step)")
print(f"Total Steps: {total_steps}")
print(f"Conditions : 150 C, 10 MPa")
phreeqc = IPhreeqc()
phreeqc.load_database(db_path)
# PHASES
phases_def = """
PHASES
Amor.Si
SiO2 = SiO2(aq)
log_k -2.71
K-Feldspar
KAlSi3O8 + 4H+ = K+ + Al+++ + 3SiO2(aq) + 2H2O
log_k -0.275
delta_h -53.53 kJ
Magnetite
Fe2SiO4 + 4H+ = 2Fe++ + SiO2(aq) + 2H2O
log_k 3.0
Fe(OH)3
Fe(OH)3 + 3H+ = Fe+3 + 3H2O
log_k 3.0
Boehmite
AlOOH + 3H+ = Al+3 + 2H2O
log_k 8.5
"""
phreeqc.run_string(phases_def)
# RATES
rates_def = """
RATES
Albite
-start
10 k_acid = 10^-10.2 * EXP(-65.0 / 0.008314 * (1/TK - 1/298.15))
20 k_neut = 10^-12.6 * EXP(-69.8 / 0.008314 * (1/TK - 1/298.15))
30 k_base = 10^-15.6 * EXP(-71.0 / 0.008314 * (1/TK - 1/298.15))
40 rate = (k_acid * ACT("H+")^0.46 + k_neut + k_base * ACT("H+")^-0.57) * (1 - SR("Albite"))
50 moles = rate * TIME * PARM(1)
60 SAVE moles
-end
Anorthite
-start
40 rate = (10^-10.2 * ACT("H+")^0.46 + 10^-12.6) * (1 - SR("Anorthite"))
50 moles = rate * TIME * PARM(1)
60 SAVE moles
-end
Enstatite
-start
40 rate = (10^-10.2 * ACT("H+")^0.5) * (1 - SR("Enstatite"))
50 moles = rate * TIME * PARM(1)
60 SAVE moles
-end
Magnetite
-start
40 rate = 10^-11.0 * (1 - SR("Magnetite"))
50 moles = rate * TIME * PARM(1)
60 SAVE moles
-end
Diopside
-start
40 rate = (10^-10.2 * ACT("H+")^0.5) * (1 - SR("Diopside"))
50 moles = rate * TIME * PARM(1)
60 SAVE moles
-end
K-Feldspar
-start
40 rate = (10^-10.2 * ACT("H+")^0.46) * (1 - SR("K-Feldspar"))
50 moles = rate * TIME * PARM(1)
60 SAVE moles
-end
Amor.Si
-start
40 rate = 10^-13.0 * (1 - SR("Amor.Si"))
50 moles = rate * TIME * PARM(1)
60 SAVE moles
-end
"""
phreeqc.run_string(rates_def)
# ---------------------------------------------------------
# 初期状態 & KINETICS
# ---------------------------------------------------------
# 【修正】 parms (表面積) を大幅に大きくしました (1e2 ~ 1e4)
# これにより反応速度が上がり、濃度が数桁上昇します。
# 計算負荷を下げるため -step_divide をさらに強化しています。
initial_sol_def = f"""
SOLUTION 1-{n_cells} Initial Pore Water
units mmol/kgw
pH 7.0
temp 150
pressure 100
EQUILIBRIUM_PHASES 1-{n_cells}
Calcite 0 0
Fe(OH)3 0 0
Boehmite 0 0
Dolomite 0 0
Magnesite 0 0
KINETICS 1-{n_cells}
-step_divide 1e7 # 分割数を増やして収束しやすくする
-bad_step_max 1000
Albite
-formula NaAlSi3O8
-m 0.1285
-parms 1e3 # 0.1 -> 1000
Anorthite
-formula CaAl2Si2O8
-m 0.1285
-parms 1e2 # 0.1 -> 100
Enstatite
-formula MgSiO3
-m 0.072
-parms 1e2 # 0.1 -> 100
Magnetite
-formula Fe2SiO4
-m 0.054
-parms 1e2 # 0.1 -> 100
Diopside
-formula CaMg(SiO3)2
-m 0.052
-parms 1e3 # 0.1 -> 1000
K-Feldspar
-formula KAlSi3O8
-m 0.03
-parms 1e2 # 0.1 -> 100
Amor.Si
-formula SiO2
-m 0.417
-parms 1e4 # 0.1 -> 10000
SAVE solution 1-{n_cells}
END
"""
phreeqc.run_string(initial_sol_def)
# 流入水
injection_sol_def = """
SOLUTION 0 Inflow Water
units mmol/kgw
pH 7.0 charge
temp 150
pressure 100
EQUILIBRIUM_PHASES 0
CO2(g) 1.3 10.0
SAVE solution 0
END
"""
phreeqc.run_string(injection_sol_def)
# データ格納用辞書
results = {
'time': [], 'pH': [],
'Na': [], 'Ca': [], 'K': [], 'Mg': [], 'Fe': [], 'Al': [], 'Si': [],
'Calcite': [], 'Fe(OH)3': [], 'Boehmite': [], 'Magnetite': []
}
phreeqc.run_string("""
SELECTED_OUTPUT
-reset false
-pH
-totals Na Ca K Mg Fe Al Si
-equilibrium_phases Calcite Fe(OH)3 Boehmite
-kinetic_reactants Magnetite
END
""")
start_time = time.time()
col_map = {}
# ループ開始
for step in range(1, total_steps + 1):
transport_step = f"""
TRANSPORT
-cells {n_cells}
-shifts 1
-time_step {dt}
-punch_cells {n_cells}
-punch_frequency 1
END
"""
try:
phreeqc.run_string(transport_step)
except Exception as e:
print(f"\nError at step {step}: {e}")
break
elapsed = time.time() - start_time
# 進捗表示(ステップ数が多いので見やすく)
sys.stdout.write(f"\rProgress: [{step}/{total_steps}] ({elapsed:.1f}s) - {(step/total_steps)*100:.1f}%")
sys.stdout.flush()
res = phreeqc.get_selected_output_array()
if res and len(res) > 1:
header = res[0]
data = res[-1]
if not col_map:
h_up = [str(x).upper() for x in header]
def find_idx(key):
for i, h in enumerate(h_up):
if key.upper() == h: return i
for i, h in enumerate(h_up):
if key.upper() in h: return i
return None
col_map['pH'] = find_idx('pH')
for elem in ['Na', 'Ca', 'K', 'Mg', 'Fe', 'Al', 'Si']:
col_map[elem] = find_idx(elem)
for min_name in ['Calcite', 'Fe(OH)3', 'Boehmite', 'Magnetite']:
col_map[min_name] = find_idx(min_name)
# 時間 (時間単位)
current_time_hr = step * dt / 3600.0
results['time'].append(current_time_hr)
results['pH'].append(data[col_map['pH']])
for elem in ['Na', 'Ca', 'K', 'Mg', 'Fe', 'Al', 'Si']:
idx = col_map[elem]
val = data[idx] if idx is not None else 0.0
results[elem].append(val * 1000) # mmol/kgw
for min_name in ['Calcite', 'Fe(OH)3', 'Boehmite', 'Magnetite']:
idx = col_map[min_name]
val = data[idx] if idx is not None else 0.0
results[min_name].append(val)
print("\nCalculation finished.")
return results
# ==========================================
# メイン実行部
# ==========================================
if __name__ == "__main__":
L = 0.15
r = 0.01
N = 15
Q_ml_min = 1.0
phi = 0.35
K = 1e-4
Q_m3s = (Q_ml_min * 1e-6) / 60.0
mf6_path, db_path, base_dir = get_exe_path()
q_val = setup_and_run_flow(mf6_path, L, r, N, Q_m3s, K)
if q_val is not None:
print(f"Darcy Velocity: {q_val:.2e} m/s")
res = run_reactive_transport_kinetics(q_val, L, N, phi, Q_ml_min, db_path)
times = res['time']
if len(times) > 0:
# 保存
output_file = os.path.join(base_dir, "simulation_results_24h.dat")
print(f"Saving results to: {output_file}")
try:
with open(output_file, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
header_row = ['Time(hr)', 'pH'] + \
[f"{e}(mmol/kgw)" for e in ['Na', 'Ca', 'K', 'Mg', 'Fe', 'Al', 'Si']] + \
[f"{m}(mol)" for m in ['Calcite', 'Fe(OH)3', 'Boehmite', 'Magnetite']]
writer.writerow(header_row)
for i in range(len(times)):
row = [times[i], res['pH'][i]]
row += [res[e][i] for e in ['Na', 'Ca', 'K', 'Mg', 'Fe', 'Al', 'Si']]
row += [res[m][i] for m in ['Calcite', 'Fe(OH)3', 'Boehmite', 'Magnetite']]
writer.writerow(row)
print("File saved successfully.")
except Exception as e:
print(f"Error saving file: {e}")
# グラフ描画
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 15), sharex=True)
# 1. 溶存濃度 (対数軸)
elements = ['Na', 'Ca', 'K', 'Mg', 'Fe', 'Al', 'Si']
colors = plt.cm.tab10(np.linspace(0, 1, len(elements)))
for i, elem in enumerate(elements):
ax1.plot(times, res[elem], label=elem, color=colors[i], linewidth=2)
ax1.set_yscale('log')
ax1.set_ylabel('Concentration (mmol/kgw)')
ax1.set_title('1. Aqueous Species Concentrations (Log Scale)')
ax1.legend(loc='upper right', bbox_to_anchor=(1.15, 1))
ax1.grid(True, which="both", alpha=0.3)
# 2. pH
ax2.plot(times, res['pH'], 'k-', linewidth=2, label='pH')
ax2.set_ylabel('pH')
ax2.set_title('2. pH Change')
ax2.grid(True, alpha=0.3)
ax2.legend()
# 3. 鉱物量
minerals = ['Calcite', 'Fe(OH)3', 'Boehmite', 'Magnetite']
colors_min = ['blue', 'brown', 'cyan', 'black']
for i, mn in enumerate(minerals):
ax3.plot(times, res[mn], label=mn, color=colors_min[i], linewidth=2)
ax3.set_ylabel('Mineral Amount (mol)')
ax3.set_xlabel('Time (hours)')
ax3.set_title('3. Mineral Phase Changes')
ax3.legend()
ax3.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
else:
print("No results to plot.")