updated version with forward, reverse, and empirical mode
This commit is contained in:
parent
9c8dc6053e
commit
301a38da80
20 changed files with 1140 additions and 0 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"python-envs.defaultEnvManager": "ms-python.python:system"
|
||||
}
|
||||
10
empirical_wf_dodecanedioic_run.yaml
Normal file
10
empirical_wf_dodecanedioic_run.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
solvent_properties:
|
||||
name: WF2 + Dodecanedioic
|
||||
water_solubility:
|
||||
t_low_mass_fraction: 0.4299
|
||||
t_high_mass_fraction: 0.0
|
||||
ion_partition_coefficients:
|
||||
t_low:
|
||||
default: 0.63511
|
||||
t_high:
|
||||
default: 0.579491
|
||||
10
empirical_wf_hcl_run.yaml
Normal file
10
empirical_wf_hcl_run.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
solvent_properties:
|
||||
name: WF2 + HCl
|
||||
water_solubility:
|
||||
t_low_mass_fraction: 0.04
|
||||
t_high_mass_fraction: 0.0
|
||||
ion_partition_coefficients:
|
||||
t_low:
|
||||
default: 0.0
|
||||
t_high:
|
||||
default: 0.0
|
||||
10
empirical_wf_hexanoic_run.yaml
Normal file
10
empirical_wf_hexanoic_run.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
solvent_properties:
|
||||
name: WF2 + Hexanoic
|
||||
water_solubility:
|
||||
t_low_mass_fraction: 0.5063
|
||||
t_high_mass_fraction: 0.138
|
||||
ion_partition_coefficients:
|
||||
t_low:
|
||||
default: 0.854573
|
||||
t_high:
|
||||
default: 0.682616
|
||||
10
empirical_wf_octanoic_run.yaml
Normal file
10
empirical_wf_octanoic_run.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
solvent_properties:
|
||||
name: WF2 + Octanoic
|
||||
water_solubility:
|
||||
t_low_mass_fraction: 0.4556
|
||||
t_high_mass_fraction: 0.264
|
||||
ion_partition_coefficients:
|
||||
t_low:
|
||||
default: 0.640262
|
||||
t_high:
|
||||
default: 0.423737
|
||||
10
empirical_wf_suberic_run.yaml
Normal file
10
empirical_wf_suberic_run.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
solvent_properties:
|
||||
name: WF2 + Suberic
|
||||
water_solubility:
|
||||
t_low_mass_fraction: 0.1884
|
||||
t_high_mass_fraction: 0.0
|
||||
ion_partition_coefficients:
|
||||
t_low:
|
||||
default: 0.158771
|
||||
t_high:
|
||||
default: -0.138553
|
||||
10
empirical_wf_succinic_run.yaml
Normal file
10
empirical_wf_succinic_run.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
solvent_properties:
|
||||
name: WF2 + Succinic
|
||||
water_solubility:
|
||||
t_low_mass_fraction: 0.4446
|
||||
t_high_mass_fraction: 0.1484
|
||||
ion_partition_coefficients:
|
||||
t_low:
|
||||
default: 1.499923
|
||||
t_high:
|
||||
default: 0.77172
|
||||
109
experimental_runs.yaml
Normal file
109
experimental_runs.yaml
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# experimental_runs.yaml
|
||||
# Feed TDS for all runs is approximately 238 mg/kg
|
||||
|
||||
experiments:
|
||||
hcl_run:
|
||||
acid_used: "HCl"
|
||||
inputs:
|
||||
wf_density_kg_L: 0.80
|
||||
feed_water_g: 18.0030
|
||||
feed_tds_mg_kg: 238.0
|
||||
wf2_base_g: 8.1189
|
||||
acid_g: 0.1675
|
||||
measured_outputs:
|
||||
hydrated_wf_g: 8.6320
|
||||
reject_water_g: null # Not measured/reported
|
||||
yield_water_g: 0.4645
|
||||
dried_wf_g: 8.1675
|
||||
reject_tds_mg_kg: null # Not measured/reported
|
||||
yield_tds_mg_kg: null # Not measured/reported
|
||||
|
||||
hexanoic_run:
|
||||
acid_used: "Hexanoic"
|
||||
inputs:
|
||||
feed_water_g: 18.0531
|
||||
feed_tds_mg_kg: 238.0
|
||||
wf2_base_g: 8.5090
|
||||
acid_g: 0.2473
|
||||
measured_outputs:
|
||||
hydrated_wf_g: 17.7369
|
||||
reject_water_g: 10.4738
|
||||
yield_water_g: 7.5793
|
||||
dried_wf_g: 10.1576
|
||||
reject_tds_mg_kg: 164.6
|
||||
yield_tds_mg_kg: 175.11
|
||||
|
||||
octanoic_run:
|
||||
acid_used: "Octanoic"
|
||||
inputs:
|
||||
feed_water_g: 18.1049
|
||||
feed_tds_mg_kg: 238.0
|
||||
wf2_base_g: 8.5037
|
||||
acid_g: 0.3008
|
||||
measured_outputs:
|
||||
hydrated_wf_g: 16.1714
|
||||
reject_water_g: 13.8967
|
||||
yield_water_g: 4.2082
|
||||
dried_wf_g: 11.9632
|
||||
reject_tds_mg_kg: 191.48
|
||||
yield_tds_mg_kg: 220.62
|
||||
|
||||
palmitoyl_ascorbic_run:
|
||||
acid_used: "6-O-Palmitoyl-L-ascorbic"
|
||||
inputs:
|
||||
feed_water_g: 18.0727
|
||||
feed_tds_mg_kg: 238.0
|
||||
wf2_base_g: 8.5056
|
||||
acid_g: 0.8617
|
||||
measured_outputs:
|
||||
hydrated_wf_g: null # Did not form a phase barrier
|
||||
reject_water_g: null
|
||||
yield_water_g: null
|
||||
dried_wf_g: null
|
||||
reject_tds_mg_kg: null
|
||||
yield_tds_mg_kg: null
|
||||
|
||||
succinic_run:
|
||||
acid_used: "Succinic"
|
||||
inputs:
|
||||
feed_water_g: 18.0481
|
||||
feed_tds_mg_kg: 238.0
|
||||
wf2_base_g: 8.5479
|
||||
acid_g: 0.2764
|
||||
measured_outputs:
|
||||
hydrated_wf_g: 15.8887
|
||||
reject_water_g: 12.5214
|
||||
yield_water_g: 5.5267
|
||||
dried_wf_g: 10.3620
|
||||
reject_tds_mg_kg: 112.66
|
||||
yield_tds_mg_kg: 200.82
|
||||
|
||||
suberic_run:
|
||||
acid_used: "Suberic"
|
||||
inputs:
|
||||
feed_water_g: 18.2303
|
||||
feed_tds_mg_kg: 238.0
|
||||
wf2_base_g: 8.5123
|
||||
acid_g: 0.3638
|
||||
measured_outputs:
|
||||
hydrated_wf_g: 10.9361
|
||||
reject_water_g: 14.8307
|
||||
yield_water_g: 3.3996
|
||||
dried_wf_g: 7.5365
|
||||
reject_tds_mg_kg: 237.62
|
||||
yield_tds_mg_kg: 242.44
|
||||
|
||||
dodecanedioic_run:
|
||||
acid_used: "Dodecanedioic"
|
||||
inputs:
|
||||
feed_water_g: 18.0421
|
||||
feed_tds_mg_kg: 238.0
|
||||
wf2_base_g: 8.5123
|
||||
acid_g: 0.4817
|
||||
measured_outputs:
|
||||
hydrated_wf_g: 15.7753
|
||||
reject_water_g: 10.4636
|
||||
yield_water_g: 7.5785
|
||||
dried_wf_g: 8.1968
|
||||
reject_tds_mg_kg: 189.08
|
||||
yield_tds_mg_kg: 158.80
|
||||
101
tradeoff_data.csv
Normal file
101
tradeoff_data.csv
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
S_water_low_mass_fraction,S_water_high_mass_fraction
|
||||
0.5566,0.0050
|
||||
0.5593,0.0190
|
||||
0.5621,0.0327
|
||||
0.5649,0.0463
|
||||
0.5677,0.0596
|
||||
0.5705,0.0727
|
||||
0.5733,0.0856
|
||||
0.5761,0.0983
|
||||
0.5789,0.1109
|
||||
0.5817,0.1232
|
||||
0.5845,0.1354
|
||||
0.5873,0.1474
|
||||
0.5901,0.1592
|
||||
0.5929,0.1709
|
||||
0.5956,0.1824
|
||||
0.5984,0.1937
|
||||
0.6012,0.2049
|
||||
0.6040,0.2159
|
||||
0.6068,0.2268
|
||||
0.6096,0.2375
|
||||
0.6124,0.2481
|
||||
0.6152,0.2586
|
||||
0.6180,0.2689
|
||||
0.6208,0.2790
|
||||
0.6236,0.2890
|
||||
0.6264,0.2989
|
||||
0.6292,0.3087
|
||||
0.6319,0.3183
|
||||
0.6347,0.3279
|
||||
0.6375,0.3373
|
||||
0.6403,0.3465
|
||||
0.6431,0.3557
|
||||
0.6459,0.3647
|
||||
0.6487,0.3737
|
||||
0.6515,0.3825
|
||||
0.6543,0.3912
|
||||
0.6571,0.3998
|
||||
0.6599,0.4083
|
||||
0.6627,0.4167
|
||||
0.6655,0.4250
|
||||
0.6683,0.4332
|
||||
0.6710,0.4413
|
||||
0.6738,0.4493
|
||||
0.6766,0.4572
|
||||
0.6794,0.4651
|
||||
0.6822,0.4728
|
||||
0.6850,0.4804
|
||||
0.6878,0.4880
|
||||
0.6906,0.4954
|
||||
0.6934,0.5028
|
||||
0.6962,0.5101
|
||||
0.6990,0.5173
|
||||
0.7018,0.5245
|
||||
0.7046,0.5315
|
||||
0.7073,0.5385
|
||||
0.7101,0.5454
|
||||
0.7129,0.5523
|
||||
0.7157,0.5590
|
||||
0.7185,0.5657
|
||||
0.7213,0.5723
|
||||
0.7241,0.5789
|
||||
0.7269,0.5853
|
||||
0.7297,0.5917
|
||||
0.7325,0.5981
|
||||
0.7353,0.6043
|
||||
0.7381,0.6105
|
||||
0.7409,0.6167
|
||||
0.7436,0.6228
|
||||
0.7464,0.6288
|
||||
0.7492,0.6347
|
||||
0.7520,0.6406
|
||||
0.7548,0.6465
|
||||
0.7576,0.6522
|
||||
0.7604,0.6580
|
||||
0.7632,0.6636
|
||||
0.7660,0.6692
|
||||
0.7688,0.6748
|
||||
0.7716,0.6803
|
||||
0.7744,0.6857
|
||||
0.7772,0.6911
|
||||
0.7799,0.6964
|
||||
0.7827,0.7017
|
||||
0.7855,0.7070
|
||||
0.7883,0.7122
|
||||
0.7911,0.7173
|
||||
0.7939,0.7224
|
||||
0.7967,0.7274
|
||||
0.7995,0.7324
|
||||
0.8023,0.7374
|
||||
0.8051,0.7423
|
||||
0.8079,0.7471
|
||||
0.8107,0.7520
|
||||
0.8135,0.7567
|
||||
0.8162,0.7615
|
||||
0.8190,0.7661
|
||||
0.8218,0.7708
|
||||
0.8246,0.7754
|
||||
0.8274,0.7799
|
||||
0.8302,0.7845
|
||||
0.8330,0.7889
|
||||
|
BIN
tradeoff_plot.png
Normal file
BIN
tradeoff_plot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
536
tsse_engine.py
536
tsse_engine.py
|
|
@ -0,0 +1,536 @@
|
|||
import yaml
|
||||
import os
|
||||
import numpy as np
|
||||
from scipy.optimize import root
|
||||
import csv
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
def load_yaml(filepath):
|
||||
"""Loads a YAML file and returns the dictionary."""
|
||||
if not os.path.exists(filepath):
|
||||
raise FileNotFoundError(f"Missing {filepath}! Please check your paths.")
|
||||
with open(filepath, 'r') as file:
|
||||
return yaml.safe_load(file)
|
||||
|
||||
def calculate_brine_density(mass_ions_g, mass_total_g):
|
||||
"""
|
||||
Calculates brine density (kg/L) based on the mass fraction of salt.
|
||||
"""
|
||||
if mass_total_g == 0:
|
||||
return 1.0
|
||||
mass_fraction = mass_ions_g / mass_total_g
|
||||
# Simple empirical approximation: water density + salt mass fraction
|
||||
return 1.0 + mass_fraction
|
||||
|
||||
def initialize_feed_mass(ions_dict, feed_volume_L):
|
||||
"""Converts feed concentrations (mg/L) into absolute masses (grams)."""
|
||||
masses_g = {}
|
||||
|
||||
total_tds_mg_L = sum(details.get('value', 0.0) for details in ions_dict.values())
|
||||
|
||||
for ion, details in ions_dict.items():
|
||||
conc_mg_L = details.get('value', 0.0)
|
||||
masses_g[ion] = (conc_mg_L * feed_volume_L) / 1000.0
|
||||
|
||||
# We estimate initial mass using our density function
|
||||
initial_mass_fraction = total_tds_mg_L / 1_000_000.0
|
||||
density_kg_L = 1.0 + initial_mass_fraction
|
||||
|
||||
total_solution_mass_g = feed_volume_L * density_kg_L * 1000.0
|
||||
total_ion_mass_g = sum(masses_g.values())
|
||||
|
||||
masses_g['H2O'] = total_solution_mass_g - total_ion_mass_g
|
||||
return masses_g
|
||||
|
||||
def run_forward_mode(params, ions_dict, feed_volume, solvent_mass_g):
|
||||
"""Runs the two-stage thermodynamic extraction simulation."""
|
||||
# 1. Initialize Feed Mass
|
||||
feed_masses_g = initialize_feed_mass(ions_dict, feed_volume)
|
||||
|
||||
# We need a fixed list of species to keep our arrays ordered
|
||||
species_list = ['H2O'] + sorted(ions_dict.keys())
|
||||
|
||||
# Create an initial guess: assume 0 grams of ions transfer, and 100g of water transfers
|
||||
initial_guesses = [100.0 if sp == 'H2O' else 0.0 for sp in species_list]
|
||||
|
||||
# ==========================================
|
||||
# STAGE 1: EXTRACTION
|
||||
# ==========================================
|
||||
print("\n⚙️ Running Stage 1 Extraction (T_low)...")
|
||||
solution_1 = root(
|
||||
stage_1_residuals,
|
||||
x0=initial_guesses,
|
||||
args=(species_list, feed_masses_g, solvent_mass_g, params),
|
||||
method='hybr'
|
||||
)
|
||||
|
||||
if not solution_1.success:
|
||||
print("❌ Stage 1 Solver failed to converge:", solution_1.message)
|
||||
return
|
||||
|
||||
print("✅ Stage 1 Equilibrium Reached!")
|
||||
|
||||
# Unpack Stage 1 solution
|
||||
loaded_org_masses = {species: solution_1.x[i] for i, species in enumerate(species_list)}
|
||||
reject_aq_masses = {species: feed_masses_g[species] - solution_1.x[i] for i, species in enumerate(species_list)}
|
||||
|
||||
print(f" -> Reject Brine left behind: {sum(reject_aq_masses.values()):.2f} g")
|
||||
print(f" -> Water/Salts pulled into solvent: {sum(loaded_org_masses.values()):.2f} g")
|
||||
|
||||
# ==========================================
|
||||
# STAGE 2: RECOVERY
|
||||
# ==========================================
|
||||
print("\n⚙️ Running Stage 2 Recovery (T_high)...")
|
||||
initial_guesses_2 = [10.0 if sp == 'H2O' else 0.0 for sp in species_list]
|
||||
|
||||
solution_2 = root(
|
||||
stage_2_residuals,
|
||||
x0=initial_guesses_2,
|
||||
args=(species_list, loaded_org_masses, solvent_mass_g, params),
|
||||
method='hybr'
|
||||
)
|
||||
|
||||
if not solution_2.success:
|
||||
print("❌ Stage 2 Solver failed to converge:", solution_2.message)
|
||||
return
|
||||
|
||||
print("✅ Stage 2 Equilibrium Reached!")
|
||||
|
||||
# Unpack Stage 2 solution
|
||||
final_org_masses = {species: solution_2.x[i] for i, species in enumerate(species_list)}
|
||||
yield_aq_masses = {species: loaded_org_masses[species] - solution_2.x[i] for i, species in enumerate(species_list)}
|
||||
|
||||
# Convert masses back to volume and mg/L
|
||||
reject_vol_L, reject_conc = convert_mass_to_concentration(reject_aq_masses)
|
||||
yield_vol_L, yield_conc = convert_mass_to_concentration(yield_aq_masses)
|
||||
yield_tds = sum(yield_conc.values())
|
||||
|
||||
# Output Results
|
||||
print("\n==========================================")
|
||||
print("🏆 FINAL PROCESS OUTPUT")
|
||||
print("==========================================")
|
||||
|
||||
print("\n--- Reject Brine ---")
|
||||
print(f" Volume: {reject_vol_L:.3f} L")
|
||||
print(f" Na: {reject_conc.get('Na', 0):.1f} mg/L")
|
||||
print(f" Cl: {reject_conc.get('Cl', 0):.1f} mg/L")
|
||||
|
||||
print("\n--- Yield Water (Agricultural Product) ---")
|
||||
print(f" Volume: {yield_vol_L:.3f} L")
|
||||
print(f" Total TDS: {yield_tds:.1f} mg/L")
|
||||
for sp, conc in sorted(yield_conc.items()):
|
||||
print(f" {sp}: {conc:.2f} mg/L")
|
||||
|
||||
print("\n--- Recycled Working Fluid (Dry) ---")
|
||||
print(f" Solvent (Base): {solvent_mass_g:.2f} g")
|
||||
print(f" Residual H2O: {final_org_masses['H2O']:.2f} g")
|
||||
|
||||
def run_reverse_mode(target_yield_g, solvent_mass_g, s_low_max=0.833, steps=100):
|
||||
"""
|
||||
Sweeps S_low up to the 400% volumetric expansion limit (0.833 mass fraction)
|
||||
and calculates the required S_high to hit the target yield.
|
||||
Exports a .csv and saves a .png plot.
|
||||
"""
|
||||
print(f"\n⚙️ Running Reverse Mode Parameter Sweep...")
|
||||
print(f"Targeting {target_yield_g:.1f} g of water yield from {solvent_mass_g:.1f} g of solvent.")
|
||||
|
||||
target_ratio = target_yield_g / solvent_mass_g
|
||||
|
||||
# Calculate the absolute minimum S_low required to even hold the target yield
|
||||
# (If S_high was 0, what must S_low be?)
|
||||
s_low_min = target_ratio / (1.0 + target_ratio)
|
||||
|
||||
if s_low_max <= s_low_min:
|
||||
print("❌ Error: The target yield requires more water capacity than your maximum limit allows.")
|
||||
return
|
||||
|
||||
s_low_values = np.linspace(s_low_min + 0.001, s_low_max, steps)
|
||||
valid_s_low = []
|
||||
s_high_values = []
|
||||
|
||||
for s_low in s_low_values:
|
||||
# Water held per gram of dry solvent at T_low
|
||||
water_ratio_low = s_low / (1.0 - s_low)
|
||||
|
||||
# Water that MUST be held at T_high to expel the target amount
|
||||
water_ratio_high = water_ratio_low - target_ratio
|
||||
|
||||
if water_ratio_high >= 0:
|
||||
s_high = water_ratio_high / (1.0 + water_ratio_high)
|
||||
valid_s_low.append(s_low)
|
||||
s_high_values.append(s_high)
|
||||
|
||||
# 1. Export to CSV
|
||||
csv_filename = "tradeoff_data.csv"
|
||||
with open(csv_filename, mode='w', newline='') as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerow(["S_water_low_mass_fraction", "S_water_high_mass_fraction"])
|
||||
for low, high in zip(valid_s_low, s_high_values):
|
||||
writer.writerow([f"{low:.4f}", f"{high:.4f}"])
|
||||
print(f"✅ Data exported to {csv_filename}")
|
||||
|
||||
# 2. Generate and Save Plot (Headless mode)
|
||||
plt.figure(figsize=(8, 6))
|
||||
plt.plot(valid_s_low, s_high_values, 'b-', linewidth=2, label="Required Thermodynamic Path")
|
||||
|
||||
# Shade the physically impossible region (below zero)
|
||||
plt.axhline(0, color='black', linewidth=1)
|
||||
plt.fill_between(valid_s_low, s_high_values, 0, alpha=0.1, color='blue')
|
||||
|
||||
plt.title(f"Working Fluid Design Space\n(Target: {target_yield_g:.0f}g Yield per {solvent_mass_g:.0f}g Solvent)")
|
||||
plt.xlabel("Water Uptake at Extraction ($T_{low}$) [Mass Fraction]")
|
||||
plt.ylabel("Water Uptake at Recovery ($T_{high}$) [Mass Fraction]")
|
||||
plt.xlim([0.5, 0.85])
|
||||
plt.ylim([0, max(s_high_values) * 1.1])
|
||||
plt.grid(True, linestyle='--', alpha=0.7)
|
||||
plt.legend()
|
||||
|
||||
png_filename = "tradeoff_plot.png"
|
||||
plt.savefig(png_filename, dpi=300, bbox_inches='tight')
|
||||
print(f"✅ Plot saved to {png_filename}")
|
||||
plt.close() # Prevents the plot from popping up and halting the script
|
||||
|
||||
def run_empirical_mode(experiment_name, run_data):
|
||||
"""
|
||||
Parses lab data from a physical experiment, runs integrity checks,
|
||||
and calculates empirical water solubilities and volumetric salt partition coefficients.
|
||||
"""
|
||||
print(f"\n--------------------------------------------------")
|
||||
print(f"🔬 Analyzing: {experiment_name}")
|
||||
print(f"--------------------------------------------------")
|
||||
|
||||
inputs = run_data['inputs']
|
||||
outputs = run_data['measured_outputs']
|
||||
|
||||
if outputs['hydrated_wf_g'] is None:
|
||||
print(f"❌ Incomplete data for extraction stage. Cannot calculate thermodynamics.")
|
||||
return
|
||||
|
||||
# ==========================================
|
||||
# 1. ABSOLUTE WATER UPTAKE
|
||||
# ==========================================
|
||||
dry_wf_in = inputs['wf2_base_g'] + inputs['acid_g']
|
||||
|
||||
water_extracted_g = outputs['hydrated_wf_g'] - dry_wf_in
|
||||
s_low = water_extracted_g / outputs['hydrated_wf_g']
|
||||
|
||||
water_trapped_g = outputs['dried_wf_g'] - dry_wf_in
|
||||
s_high = max(0.0, water_trapped_g / outputs['dried_wf_g'])
|
||||
|
||||
print("\n--- Empirical Water Solubility (Absolute) ---")
|
||||
print(f" Dry WF Input: {dry_wf_in:.4f} g")
|
||||
print(f" S_water at T_low: {s_low*100:.1f}% by mass ({water_extracted_g:.4f} g extracted)")
|
||||
print(f" S_water at T_high: {s_high*100:.1f}% by mass ({max(0.0, water_trapped_g):.4f} g trapped)")
|
||||
|
||||
# ==========================================
|
||||
# 2. VOLUMETRIC SALT PARTITIONING (mg/L)
|
||||
# ==========================================
|
||||
k_low, k_high = 0.0, 0.0
|
||||
true_reject_mass_g = inputs['feed_water_g'] - water_extracted_g
|
||||
|
||||
if outputs['reject_tds_mg_kg'] is not None and outputs['yield_tds_mg_kg'] is not None:
|
||||
# Calculate absolute salt (mg)
|
||||
feed_salt_mg = inputs['feed_water_g'] * (inputs['feed_tds_mg_kg'] / 1000.0)
|
||||
reject_salt_mg = true_reject_mass_g * (outputs['reject_tds_mg_kg'] / 1000.0)
|
||||
yield_salt_mg = outputs['yield_water_g'] * (outputs['yield_tds_mg_kg'] / 1000.0)
|
||||
|
||||
# --- Density & Volume Conversions ---
|
||||
wf_density_kg_L = inputs.get('wf_density_kg_L', 0.80)
|
||||
|
||||
# Aqueous Volumes (using empirical density approximation)
|
||||
reject_density = 1.0 + (outputs['reject_tds_mg_kg'] / 1_000_000.0)
|
||||
v_aq_reject_L = (true_reject_mass_g / 1000.0) / reject_density
|
||||
|
||||
yield_density = 1.0 + (outputs['yield_tds_mg_kg'] / 1_000_000.0)
|
||||
v_aq_yield_L = (outputs['yield_water_g'] / 1000.0) / yield_density
|
||||
|
||||
# Organic Volumes (Additive assumption: V_total = V_dry_wf + V_water)
|
||||
v_dry_wf_L = (dry_wf_in / 1000.0) / wf_density_kg_L
|
||||
v_org_hydrated_L = v_dry_wf_L + (water_extracted_g / 1000.0) / 1.0 # water density ~1.0 kg/L
|
||||
v_org_dried_L = v_dry_wf_L + (water_trapped_g / 1000.0) / 1.0
|
||||
|
||||
# --- K_low (Volumetric) ---
|
||||
salt_transferred_mg = feed_salt_mg - reject_salt_mg
|
||||
c_org_hydrated_vol = salt_transferred_mg / v_org_hydrated_L if v_org_hydrated_L > 0 else 0
|
||||
c_aq_reject_vol = reject_salt_mg / v_aq_reject_L if v_aq_reject_L > 0 else 0
|
||||
k_low = c_org_hydrated_vol / c_aq_reject_vol if c_aq_reject_vol > 0 else 0
|
||||
|
||||
# --- K_high (Volumetric) ---
|
||||
salt_trapped_mg = salt_transferred_mg - yield_salt_mg
|
||||
c_org_dried_vol = salt_trapped_mg / v_org_dried_L if v_org_dried_L > 0 else 0
|
||||
c_aq_yield_vol = yield_salt_mg / v_aq_yield_L if v_aq_yield_L > 0 else 0
|
||||
k_high = c_org_dried_vol / c_aq_yield_vol if c_aq_yield_vol > 0 else 0
|
||||
|
||||
print("\n--- Empirical Salt Partitioning (Volumetric mg/L) ---")
|
||||
print(f" Calculated K_salt at T_low: {k_low:.5f}")
|
||||
print(f" Calculated K_salt at T_high: {k_high:.5f}")
|
||||
|
||||
# ==========================================
|
||||
# 3. MASS CLOSURE & DATA INTEGRITY REPORT
|
||||
# ==========================================
|
||||
print("\n--- Mass Closure & Data Integrity Report ---")
|
||||
|
||||
stage2_input = outputs['hydrated_wf_g']
|
||||
stage2_output = outputs['yield_water_g'] + outputs['dried_wf_g']
|
||||
stage2_diff = stage2_input - stage2_output
|
||||
|
||||
if abs(stage2_diff) < 0.001:
|
||||
print(" ✅ Stage 2 Mass Balance: Perfect closure.")
|
||||
else:
|
||||
print(f" ❌ Stage 2 Mass Balance Error: {stage2_diff:+.4f} g missing during separation.")
|
||||
|
||||
if water_trapped_g < -0.01:
|
||||
organic_loss = abs(water_trapped_g)
|
||||
loss_pct = (organic_loss / dry_wf_in) * 100
|
||||
print(f" ⚠️ Organic Phase Loss: {organic_loss:.4f} g ({loss_pct:.1f}%) of the working fluid was lost.")
|
||||
else:
|
||||
print(" ✅ Organic Phase Mass: No obvious solvent loss detected.")
|
||||
|
||||
reported_reject = outputs.get('reject_water_g')
|
||||
if reported_reject is not None:
|
||||
table_artifact = inputs['feed_water_g'] - outputs['yield_water_g']
|
||||
if abs(reported_reject - table_artifact) < 0.001:
|
||||
print(f" ⚠️ Reject Mass Artifact: The reported Reject water mass appears to be a spreadsheet calculation.")
|
||||
print(f" -> Script automatically corrected Reject to: {true_reject_mass_g:.4f} g")
|
||||
|
||||
# ==========================================
|
||||
# 4. EXPORT TO SPECIFIC YAML
|
||||
# ==========================================
|
||||
acid_name = run_data.get('acid_used', 'Unknown_Acid')
|
||||
export_data = {
|
||||
"solvent_properties": {
|
||||
"name": f"WF2 + {acid_name}",
|
||||
"water_solubility": {
|
||||
"t_low_mass_fraction": float(round(s_low, 4)),
|
||||
"t_high_mass_fraction": float(round(s_high, 4))
|
||||
},
|
||||
"ion_partition_coefficients": {
|
||||
"t_low": {"default": float(round(k_low, 6))},
|
||||
"t_high": {"default": float(round(k_high, 6))}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output_filename = f"empirical_wf_{experiment_name}.yaml"
|
||||
with open(output_filename, 'w') as file:
|
||||
yaml.safe_dump(export_data, file, sort_keys=False)
|
||||
|
||||
def stage_1_residuals(x, species_list, feed_masses_g, solvent_mass_g, params):
|
||||
"""
|
||||
The physics engine core. Evaluates the mass balances and equilibrium equations.
|
||||
x = array of guesses for the mass of each species in the organic phase.
|
||||
"""
|
||||
k_dict = params['solvent_properties']['ion_partition_coefficients']['t_low']
|
||||
default_k = k_dict['default']
|
||||
water_solubility = params['solvent_properties']['water_solubility']['t_low_mass_fraction']
|
||||
|
||||
# 1. Distribute the mass based on Python's current guesses (x)
|
||||
org_masses = {species: x[i] for i, species in enumerate(species_list)}
|
||||
aq_masses = {species: feed_masses_g[species] - x[i] for i, species in enumerate(species_list)}
|
||||
|
||||
# 2. Calculate dynamic totals and volumes
|
||||
total_org_mass = solvent_mass_g + sum(org_masses.values())
|
||||
# Convert density from kg/L to g/L for the math
|
||||
solvent_density_g_L = params['solvent_properties'].get('density_kg_L', 0.8) * 1000.0
|
||||
org_vol_L = total_org_mass / solvent_density_g_L
|
||||
|
||||
total_aq_mass = sum(aq_masses.values())
|
||||
aq_ion_mass = total_aq_mass - aq_masses['H2O']
|
||||
aq_density = calculate_brine_density(aq_ion_mass, total_aq_mass)
|
||||
aq_vol_L = total_aq_mass / (aq_density * 1000.0)
|
||||
|
||||
# 3. Calculate the residuals (how far off the physics are from perfect equilibrium)
|
||||
residuals = []
|
||||
for species in species_list:
|
||||
m_org = org_masses[species]
|
||||
m_aq = aq_masses[species]
|
||||
|
||||
if species == 'H2O':
|
||||
# Is the solvent saturated with water?
|
||||
actual_fraction = m_org / total_org_mass
|
||||
residuals.append(water_solubility - actual_fraction)
|
||||
else:
|
||||
# Did the ions partition correctly?
|
||||
c_org = m_org / org_vol_L
|
||||
c_aq = m_aq / aq_vol_L if aq_vol_L > 0 else 0
|
||||
k_val = k_dict.get(species, default_k)
|
||||
|
||||
residuals.append((k_val * c_aq) - c_org)
|
||||
|
||||
return residuals
|
||||
|
||||
def stage_2_residuals(x, species_list, loaded_org_masses_g, solvent_mass_g, params):
|
||||
"""
|
||||
Evaluates the recovery stage at T_high.
|
||||
x = array of guesses for the mass of each species STAYING in the organic phase.
|
||||
"""
|
||||
k_dict = params['solvent_properties']['ion_partition_coefficients']['t_high']
|
||||
default_k = k_dict['default']
|
||||
water_solubility = params['solvent_properties']['water_solubility']['t_high_mass_fraction']
|
||||
|
||||
# 1. Distribute the mass based on guesses
|
||||
# x is what stays in the organic phase. The rest is expelled as our aqueous yield.
|
||||
org_masses = {species: x[i] for i, species in enumerate(species_list)}
|
||||
aq_masses = {species: loaded_org_masses_g[species] - x[i] for i, species in enumerate(species_list)}
|
||||
|
||||
# 2. Calculate dynamic totals and volumes
|
||||
total_org_mass = solvent_mass_g + sum(org_masses.values())
|
||||
# Convert density from kg/L to g/L for the math
|
||||
solvent_density_g_L = params['solvent_properties'].get('density_kg_L', 0.8) * 1000.0
|
||||
org_vol_L = total_org_mass / solvent_density_g_L
|
||||
|
||||
total_aq_mass = sum(aq_masses.values())
|
||||
|
||||
# Prevent division by zero if the solver guesses 0 aqueous mass
|
||||
if total_aq_mass > 0:
|
||||
aq_ion_mass = total_aq_mass - aq_masses['H2O']
|
||||
aq_density = calculate_brine_density(aq_ion_mass, total_aq_mass)
|
||||
aq_vol_L = total_aq_mass / (aq_density * 1000.0)
|
||||
else:
|
||||
aq_vol_L = 0.0001 # tiny buffer
|
||||
|
||||
# 3. Calculate the residuals
|
||||
residuals = []
|
||||
for species in species_list:
|
||||
m_org = org_masses[species]
|
||||
m_aq = aq_masses[species]
|
||||
|
||||
if species == 'H2O':
|
||||
actual_fraction = m_org / total_org_mass if total_org_mass > 0 else 0
|
||||
residuals.append(water_solubility - actual_fraction)
|
||||
else:
|
||||
c_org = m_org / org_vol_L if org_vol_L > 0 else 0
|
||||
c_aq = m_aq / aq_vol_L if aq_vol_L > 0 else 0
|
||||
k_val = k_dict.get(species, default_k)
|
||||
|
||||
residuals.append((k_val * c_aq) - c_org)
|
||||
|
||||
return residuals
|
||||
|
||||
def convert_mass_to_concentration(masses_g):
|
||||
"""
|
||||
Converts an array of absolute masses (grams) back into
|
||||
Total Volume (L) and Concentrations (mg/L).
|
||||
"""
|
||||
total_mass_g = sum(masses_g.values())
|
||||
|
||||
# Safety check: if the phase is virtually empty, return zeros
|
||||
if total_mass_g < 0.001:
|
||||
return 0.0, {}
|
||||
|
||||
ion_mass_g = total_mass_g - masses_g.get('H2O', 0.0)
|
||||
|
||||
# Calculate Density and Volume
|
||||
density_kg_L = calculate_brine_density(ion_mass_g, total_mass_g)
|
||||
volume_L = (total_mass_g / 1000.0) / density_kg_L
|
||||
|
||||
# Calculate mg/L for each ion
|
||||
concentrations_mg_L = {}
|
||||
for species, mass in masses_g.items():
|
||||
if species != 'H2O' and mass > 0.0001:
|
||||
# (grams * 1000) / Liters = mg/L
|
||||
concentrations_mg_L[species] = (mass * 1000.0) / volume_L
|
||||
|
||||
return volume_L, concentrations_mg_L
|
||||
|
||||
def process_all_empirical_runs(experimental_yaml_path="experimental_runs.yaml"):
|
||||
"""
|
||||
Loops through all experiments in the YAML file and processes them.
|
||||
Structured to easily drop in a hashing/caching mechanism later.
|
||||
"""
|
||||
print(f"\n📂 Loading batch experimental data from {experimental_yaml_path}...")
|
||||
data = load_yaml(experimental_yaml_path)
|
||||
experiments = data.get('experiments', {})
|
||||
|
||||
# ==========================================
|
||||
# FUTURE CACHE SETUP
|
||||
# ==========================================
|
||||
# hash_file = ".empirical_cache.yaml"
|
||||
# previous_hashes = load_yaml(hash_file) if os.path.exists(hash_file) else {}
|
||||
# current_hashes = {}
|
||||
|
||||
processed_count = 0
|
||||
|
||||
for experiment_name, run_data in experiments.items():
|
||||
# ==========================================
|
||||
# FUTURE CACHE CHECK
|
||||
# ==========================================
|
||||
# run_hash = hashlib.sha256(str(run_data).encode('utf-8')).hexdigest()
|
||||
# current_hashes[experiment_name] = run_hash
|
||||
# if previous_hashes.get(experiment_name) == run_hash:
|
||||
# print(f"⏩ Skipping {experiment_name} (No changes detected).")
|
||||
# continue
|
||||
|
||||
# Pass the dictionary directly to the math engine
|
||||
run_empirical_mode(experiment_name, run_data)
|
||||
processed_count += 1
|
||||
|
||||
# ==========================================
|
||||
# FUTURE CACHE SAVE
|
||||
# ==========================================
|
||||
# with open(hash_file, 'w') as f:
|
||||
# yaml.safe_dump(current_hashes, f)
|
||||
|
||||
print(f"\n🎉 Finished processing {processed_count} experimental runs!")
|
||||
|
||||
def main():
|
||||
print("🌊 Starting TS-LLE Solver...")
|
||||
|
||||
# 1. Load Files
|
||||
water_file = "water_sources/permian_brine.yaml"
|
||||
params_file = "tsse_parameters.yaml"
|
||||
|
||||
feed_water = load_yaml(water_file)
|
||||
params = load_yaml(params_file)
|
||||
|
||||
# ==========================================
|
||||
# NEW: EMPIRICAL OVERRIDE LOGIC
|
||||
# ==========================================
|
||||
empirical_file = params['process_settings'].get('empirical_solvent_file')
|
||||
|
||||
if empirical_file and os.path.exists(empirical_file):
|
||||
print(f"🔄 Overwriting default solvent thermodynamics with: {empirical_file}")
|
||||
emp_data = load_yaml(empirical_file)
|
||||
emp_props = emp_data.get('solvent_properties', {})
|
||||
|
||||
# Safely overwrite only the keys generated by the empirical parser
|
||||
if 'name' in emp_props:
|
||||
params['solvent_properties']['name'] = emp_props['name']
|
||||
if 'water_solubility' in emp_props:
|
||||
params['solvent_properties']['water_solubility'] = emp_props['water_solubility']
|
||||
if 'ion_partition_coefficients' in emp_props:
|
||||
params['solvent_properties']['ion_partition_coefficients'] = emp_props['ion_partition_coefficients']
|
||||
|
||||
elif empirical_file:
|
||||
print(f"⚠️ WARNING: Specified empirical file '{empirical_file}' not found. Using defaults.")
|
||||
|
||||
# 2. Extract Base Variables
|
||||
mode = params['process_settings'].get('mode', 'forward')
|
||||
feed_volume = params['process_settings']['feed_volume_L']
|
||||
solvent_volume_L = params['process_settings']['solvent_volume_L']
|
||||
ions_dict = feed_water.get('ions', {})
|
||||
|
||||
# Dynamically calculate solvent mass based on YAML density (which is safely preserved!)
|
||||
density_kg_L = params['solvent_properties'].get('density_kg_L', 0.8)
|
||||
solvent_mass_g = solvent_volume_L * (density_kg_L * 1000.0)
|
||||
|
||||
# 3. Route to the appropriate engine
|
||||
if mode == "reverse":
|
||||
target_reject_L = params['process_settings'].get('target_reject_volume_L', feed_volume / 2.0)
|
||||
yield_volume_L = feed_volume - target_reject_L
|
||||
target_yield_g = yield_volume_L * 1000.0
|
||||
|
||||
s_low_max = params['solvent_properties'].get('max_water_uptake_frac', 0.833)
|
||||
run_reverse_mode(target_yield_g, solvent_mass_g, s_low_max)
|
||||
|
||||
elif mode == "forward":
|
||||
run_forward_mode(params, ions_dict, feed_volume, solvent_mass_g)
|
||||
|
||||
elif mode == "empirical":
|
||||
process_all_empirical_runs("experimental_runs.yaml")
|
||||
|
||||
else:
|
||||
print(f"❌ Unknown mode selected in config: {mode}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# tsse_parameters.yaml
|
||||
|
||||
process_settings:
|
||||
mode: "forward" # options are forward, reverse, and empirical
|
||||
empirical_solvent_file: "empirical_wf_hexanoic_run.yaml" # NEW: Point to your empirical data!
|
||||
feed_volume_L: 2.0
|
||||
solvent_volume_L: 1.0
|
||||
target_reject_volume_L: 1.0 # NEW: What volume do you want left behind?
|
||||
t_low_C: 25.0
|
||||
t_high_C: 70.0
|
||||
|
||||
solvent_properties:
|
||||
name: "Generic DIPA-like Amine"
|
||||
density_kg_L: 0.80 # NEW: Solvent density
|
||||
max_water_uptake_frac: 0.833 # NEW: The 400% volumetric expansion limit
|
||||
water_solubility:
|
||||
t_low_mass_fraction: 0.35
|
||||
t_high_mass_fraction: 0.05
|
||||
|
||||
ion_partition_coefficients:
|
||||
t_low:
|
||||
default: 0.001
|
||||
t_high:
|
||||
default: 0.0001
|
||||
50
water_sources/ICP_calibration_standard.yaml
Normal file
50
water_sources/ICP_calibration_standard.yaml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
metadata:
|
||||
name: "ICP Calibration Standard"
|
||||
date_sampled: "Unknown"
|
||||
source_document:
|
||||
notes: "Created for Permian Brine at 200:1 or 150:1 dilution"
|
||||
|
||||
properties:
|
||||
pH:
|
||||
value:
|
||||
conductivity:
|
||||
value:
|
||||
unit: "dS/m"
|
||||
|
||||
ions:
|
||||
Cl:
|
||||
value: 853.49
|
||||
unit: "mg/L"
|
||||
SO4:
|
||||
value: 149.8
|
||||
unit: "mg/L"
|
||||
NO3:
|
||||
value: 0
|
||||
unit: "mg/L"
|
||||
F:
|
||||
value: 0
|
||||
unit: "mg/L"
|
||||
Ca:
|
||||
value: 50
|
||||
unit: "mg/L"
|
||||
Mg:
|
||||
value: 0
|
||||
unit: "mg/L"
|
||||
Na:
|
||||
value: 500
|
||||
unit: "mg/L"
|
||||
K:
|
||||
value: 50
|
||||
unit: "mg/L"
|
||||
B:
|
||||
value: 10
|
||||
unit: "mg/L"
|
||||
Si:
|
||||
value: 0
|
||||
unit: "mg/L"
|
||||
Li:
|
||||
value: 10
|
||||
unit: "mg/L"
|
||||
Sr:
|
||||
value: 10
|
||||
unit: "mg/L"
|
||||
44
water_sources/agua_pozo_6.yaml
Normal file
44
water_sources/agua_pozo_6.yaml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
metadata:
|
||||
name: "Agua Pozo - 6"
|
||||
date_sampled: "Unknown"
|
||||
source_document: "agua pozo.pdf"
|
||||
notes: "Units for some cations are suspiciously listed as g/100g. Requires conversion/validation."
|
||||
|
||||
properties:
|
||||
pH:
|
||||
value: 7.662
|
||||
conductivity:
|
||||
value: 8.829
|
||||
unit: "dS/m"
|
||||
|
||||
ions:
|
||||
Cl:
|
||||
value: 1740.514
|
||||
unit: "mg/L"
|
||||
SO4:
|
||||
value: 2002.490
|
||||
unit: "mg/L"
|
||||
NO3:
|
||||
value: 59.032
|
||||
unit: "mg/L"
|
||||
F:
|
||||
value: 2.101
|
||||
unit: "mg/L"
|
||||
Ca:
|
||||
value: 355.913
|
||||
unit: "mg/L"
|
||||
Mg:
|
||||
value: 310.033
|
||||
unit: "mg/L"
|
||||
Na:
|
||||
value: 19.637
|
||||
unit: "mg/L"
|
||||
K:
|
||||
value: 0.521
|
||||
unit: "mg/L"
|
||||
B:
|
||||
value: 1.362
|
||||
unit: "mg/Kg"
|
||||
Si:
|
||||
value: 9.491
|
||||
unit: "mg/Kg"
|
||||
35
water_sources/epa_moderately_hard.yaml
Normal file
35
water_sources/epa_moderately_hard.yaml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
metadata:
|
||||
name: "EPA Moderately Hard Reconstituted Water"
|
||||
date_sampled: "Standard Template"
|
||||
source_document: "EPA-821-R-02-012"
|
||||
notes: "Official EPA WET Testing baseline. Target Hardness: 80-100 mg/L. Target pH: 7.4-7.8."
|
||||
|
||||
properties:
|
||||
pH:
|
||||
value: 7.6
|
||||
alkalinity:
|
||||
value: 65.0
|
||||
unit: "mg/L CaCO3"
|
||||
|
||||
ions:
|
||||
Ca:
|
||||
value: 14.0
|
||||
unit: "mg/L"
|
||||
Mg:
|
||||
value: 12.1
|
||||
unit: "mg/L"
|
||||
Na:
|
||||
value: 26.3
|
||||
unit: "mg/L"
|
||||
K:
|
||||
value: 2.1
|
||||
unit: "mg/L"
|
||||
SO4:
|
||||
value: 81.4
|
||||
unit: "mg/L"
|
||||
Cl:
|
||||
value: 1.90
|
||||
unit: "mg/L"
|
||||
HCO3:
|
||||
value: 69.7
|
||||
unit: "mg/L"
|
||||
35
water_sources/epa_very_hard.yaml
Normal file
35
water_sources/epa_very_hard.yaml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
metadata:
|
||||
name: "EPA Very Hard Reconstituted Water"
|
||||
date_sampled: "Standard Template"
|
||||
source_document: "EPA-821-R-02-012"
|
||||
notes: "Official EPA WET Testing baseline. Target Hardness: 280-320 mg/L. Target pH: 8.0-8.4."
|
||||
|
||||
properties:
|
||||
pH:
|
||||
value: 8.2
|
||||
alkalinity:
|
||||
value: 235.0
|
||||
unit: "mg/L CaCO3"
|
||||
|
||||
ions:
|
||||
Ca:
|
||||
value: 56.0
|
||||
unit: "mg/L"
|
||||
Mg:
|
||||
value: 48.4
|
||||
unit: "mg/L"
|
||||
Na:
|
||||
value: 105.2
|
||||
unit: "mg/L"
|
||||
K:
|
||||
value: 8.4
|
||||
unit: "mg/L"
|
||||
SO4:
|
||||
value: 325.6
|
||||
unit: "mg/L"
|
||||
Cl:
|
||||
value: 7.6
|
||||
unit: "mg/L"
|
||||
HCO3:
|
||||
value: 278.8
|
||||
unit: "mg/L"
|
||||
50
water_sources/mafa_20_sample3.yaml
Normal file
50
water_sources/mafa_20_sample3.yaml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
metadata:
|
||||
name: "MAFA 20 - Sample 3"
|
||||
date_sampled: "2025-06-15"
|
||||
source_document: "MAFA 20.pdf"
|
||||
notes: "Grapes crop well. Sulfur reported as elemental S, not SO4."
|
||||
|
||||
properties:
|
||||
pH:
|
||||
value: 7.40
|
||||
TDS:
|
||||
value: 1569
|
||||
unit: "mg/L"
|
||||
conductivity:
|
||||
value: 2.60
|
||||
unit: "dS/m"
|
||||
SAR:
|
||||
value: 6
|
||||
unit: "Calculated"
|
||||
|
||||
ions:
|
||||
Ca:
|
||||
value: 113
|
||||
unit: "mg/L"
|
||||
Mg:
|
||||
value: 57
|
||||
unit: "mg/L"
|
||||
K:
|
||||
value: 7.83
|
||||
unit: "mg/L"
|
||||
Na:
|
||||
value: 331
|
||||
unit: "mg/L"
|
||||
HCO3:
|
||||
value: 674
|
||||
unit: "mg/L"
|
||||
Cl:
|
||||
value: 652
|
||||
unit: "mg/L"
|
||||
NO3:
|
||||
value: "<10"
|
||||
unit: "mg/L"
|
||||
B:
|
||||
value: 0.19
|
||||
unit: "mg/L"
|
||||
Mo:
|
||||
value: 0.001
|
||||
unit: "mg/L"
|
||||
S:
|
||||
value: 60
|
||||
unit: "mg/L"
|
||||
65
water_sources/permian_brine.yaml
Normal file
65
water_sources/permian_brine.yaml
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
metadata:
|
||||
name: "Permian Clean Brine"
|
||||
date_sampled: "2023-05-16"
|
||||
source_document: "Representative_Permian_Clean_Brine_Composition-1(1).26Feb26.pdf"
|
||||
notes: "Extreme salinity. Lab noted a 10.5% charge imbalance."
|
||||
|
||||
properties:
|
||||
pH:
|
||||
value: 7.1
|
||||
TDS:
|
||||
value: 101941
|
||||
unit: "mg/L"
|
||||
alkalinity:
|
||||
value: 482
|
||||
unit: "mg/L CaCO3"
|
||||
|
||||
ions:
|
||||
Na:
|
||||
value: 35189
|
||||
unit: "mg/L"
|
||||
Cl:
|
||||
value: 63300
|
||||
unit: "mg/L"
|
||||
Ca:
|
||||
value: 1106
|
||||
unit: "mg/L"
|
||||
Mg:
|
||||
value: 190
|
||||
unit: "mg/L"
|
||||
K:
|
||||
value: 387
|
||||
unit: "mg/L"
|
||||
SO4:
|
||||
value: 420
|
||||
unit: "mg/L"
|
||||
HCO3:
|
||||
value: 588
|
||||
unit: "mg/L"
|
||||
Sr:
|
||||
value: 389
|
||||
unit: "mg/L"
|
||||
Ba:
|
||||
value: 1.3
|
||||
unit: "mg/L"
|
||||
Fe:
|
||||
value: 27.6
|
||||
unit: "mg/L"
|
||||
Mn:
|
||||
value: 0.4
|
||||
unit: "mg/L"
|
||||
Li:
|
||||
value: 28.2
|
||||
unit: "mg/L"
|
||||
Zn:
|
||||
value: 4.1
|
||||
unit: "mg/L"
|
||||
PO4:
|
||||
value: 2.7
|
||||
unit: "mg/L"
|
||||
H3BO3:
|
||||
value: 147
|
||||
unit: "mg/L"
|
||||
SiO2:
|
||||
value: 31.9
|
||||
unit: "mg/L"
|
||||
28
water_sources/template.yaml
Normal file
28
water_sources/template.yaml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
metadata:
|
||||
name: ""
|
||||
date_sampled: ""
|
||||
source_document: ""
|
||||
notes: ""
|
||||
|
||||
properties:
|
||||
pH:
|
||||
value:
|
||||
TDS:
|
||||
value:
|
||||
unit: "mg/L"
|
||||
conductivity:
|
||||
value:
|
||||
unit: "uS/cm"
|
||||
alkalinity:
|
||||
value:
|
||||
unit: "mg/L CaCO3"
|
||||
|
||||
# List ions exactly as reported. The Python script will translate units to mol/L.
|
||||
ions:
|
||||
Na:
|
||||
value:
|
||||
unit: "mg/L"
|
||||
Cl:
|
||||
value:
|
||||
unit: "mg/L"
|
||||
# Add other ions as needed...
|
||||
Loading…
Reference in a new issue