Skip to main content

Titration - Acid-Base Analysis

Acid-base potentiometric titration is used to measure freebase/limestone content in pilot plant liquids. The OrionStarT940 Auto Titrator generates pH vs. volume curves and calculates endpoint values.

Method Overview​

PropertyValue
Full NameAcid-Base Potentiometric Titration
PurposeMeasures freebase/limestone content in pilot plant liquids
InstrumentOrionStarT940 Auto Titrator
OutputpH vs. volume titration curves and endpoint values
SharePoint LocationAnalytical Data > OrionStarT940 Auto Titrator > Data Exports
File FormatCSV (paired files)

Instrument​

OrionStarT940 Auto Titrator​

The OrionStarT940 is an automated potentiometric titrator that measures pH as a function of titrant volume added. It automatically detects endpoints based on inflection points in the pH curve.

Output Files:

  • *_Tsum.csv - Summary/metadata file (sample ID, method, endpoint values)
  • *_Tdata1.csv - Data file (pH vs. volume curve points)

Sample Types​

TypeDescription
BBE Pilot LiquidsLiquid samples from the BBE pilot plant

Sample ID Format​

Titrator sample IDs follow pilot plant naming conventions:

Input FormatCanonical Sample ID
R204121620PIL13-441-121620-R204-LIQ-RUSH
D101121620PIL13-441-121620-D101-LIQ-RUSH
121700D101PIL13-441-121700-D101-LIQ-RUSH

The format is: PIL13-441-{MMDDYY}-{Location}-LIQ-RUSH

Data Pipeline​

SharePoint Source​

  • Site: Analytical Data
  • Folder: OrionStarT940 Auto Titrator/Data Exports/
  • File Type: CSV (paired files)

File Pairing​

Files are generated in pairs with matching prefixes:

T40277_20251216_1711_506_Tsum.csv   <- Metadata
T40277_20251216_1711_506_Tdata1.csv <- Curve data

The pipeline automatically:

  1. Separates files by type (Tsum vs Tdata1)
  2. Matches pairs by prefix
  3. Fetches missing pairs from SharePoint when only one file arrives

Dagster Assets​

The titration data pipeline is defined in apps/datasmart/src/datasmart/assets/analytical/tritation.py:

freebase_titration (sharepoint_multi_asset)
├── freebase_titration_samples (analytical.freebase_titration_samples)
└── freebase_titration_data (analytical.freebase_titration_data)

Asset Description​

AssetSchemaDescription
freebase_titration_samplesanalyticalSample metadata and endpoint values
freebase_titration_dataanalyticalpH vs volume curve data points

Database Tables​

analytical.freebase_titration_samples​

Metadata table with one row per sample.

ColumnTypeDescription
file_idstringSharePoint file identifier (primary key)
original_sample_idstringRaw sample ID from file
sample_idstringNormalized pilot sample ID
sample_datedatetimeAnalysis date/time
titrator_namestringTitrator instrument name
method_namestringTitration method
titrant_namestringTitrant solution name
titrant_concfloatTitrant concentration (M)
sample_amountfloatSample amount (g or mL)
sample_concfloatSample concentration
ep_volumefloatEndpoint volume (mL)
ep_valuefloatEndpoint pH value
initial_phfloatInitial pH
temperaturefloatTemperature (C)
durationstringAnalysis duration (min)
sp_sitestringSharePoint site
file_pathstringFile path
file_urlstringSharePoint URL
last_updatedatetimeLast modification time

analytical.freebase_titration_data​

Curve data table with multiple rows per sample (one per data point).

ColumnTypeDescription
file_idstringSharePoint file identifier
volumefloatTitrant volume (mL)
phfloatpH value

File Formats​

Tsum File (Metadata)​

Comma-separated key-value pairs:

Titrator Name, OrionStarT940
Date, 12/16/2024
Time, 05:11 PM
Method Name, Freebase
Sample ID, R204121620
Titrant Name, 0.1M HCl
Titrant Conc., 0.1 M
Sample Amount, 10.000 gram
EP Volume 1, 15.234 mL
EP Value 1, 4.50
Initial pH, 12.34
Temperature, 25.0
Duration (min), 5:30

Tdata1 File (Curve Data)​

CSV with header row followed by data:

Point,Volume (mL),E(pH),dE/dV,d2E/dV2
1,0.000,12.34,0.00,0.00
2,0.100,12.30,-0.40,0.10
3,0.200,12.25,-0.50,0.10
...

Usage Examples​

Query Sample Metadata​

from shared.db.sql import SQL

# Get all samples with endpoints
samples = SQL.read("""
SELECT sample_id, sample_date, ep_volume, ep_value, initial_ph
FROM analytical.freebase_titration_samples
WHERE sample_id IS NOT NULL
ORDER BY sample_date DESC
""")

Get Titration Curve​

# Get curve for a specific sample
curve = SQL.read("""
SELECT d.volume, d.ph
FROM analytical.freebase_titration_data d
JOIN analytical.freebase_titration_samples s ON d.file_id = s.file_id
WHERE s.sample_id = 'PIL13-441-121620-R204-LIQ-RUSH'
ORDER BY d.volume
""")

Filter by Location​

# Get all samples from R204 location
r204_samples = SQL.read("""
SELECT *
FROM analytical.freebase_titration_samples
WHERE sample_id LIKE 'PIL13-441-%-R204-%'
""")

Plot Titration Curve​

import matplotlib.pyplot as plt
from shared.db.sql import SQL

# Get curve data
curve = SQL.read("""
SELECT d.volume, d.ph
FROM analytical.freebase_titration_data d
JOIN analytical.freebase_titration_samples s ON d.file_id = s.file_id
WHERE s.sample_id = 'PIL13-441-121620-R204-LIQ-RUSH'
ORDER BY d.volume
""")

# Get endpoint values
sample = SQL.read("""
SELECT ep_volume, ep_value
FROM analytical.freebase_titration_samples
WHERE sample_id = 'PIL13-441-121620-R204-LIQ-RUSH'
""").iloc[0]

plt.figure(figsize=(10, 6))
plt.plot(curve['volume'], curve['ph'], 'b-', linewidth=1.5)
plt.axhline(y=sample['ep_value'], color='r', linestyle='--', label=f"Endpoint: pH {sample['ep_value']}")
plt.axvline(x=sample['ep_volume'], color='g', linestyle='--', label=f"EP Volume: {sample['ep_volume']} mL")
plt.xlabel('Volume (mL)')
plt.ylabel('pH')
plt.title('Titration Curve: PIL13-441-121620-R204-LIQ-RUSH')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

Sample ID Parsing​

The pipeline automatically converts raw titrator sample IDs to canonical pilot plant format:

import re

SAMPLE_ID_LOCATION_DATE_REGEX = re.compile(r"^([A-Z]\d{3})(\d{6})$")
SAMPLE_ID_DATE_LOCATION_REGEX = re.compile(r"^(\d{6})([A-Z]\d{3})$")

def parse_pilot_sample_id(raw_sample_id: str) -> str | None:
"""
Parse a titrator sample ID into canonical pilot sample ID format.

Examples:
R204121620 -> PIL13-441-121620-R204-LIQ-RUSH
D101121620 -> PIL13-441-121620-D101-LIQ-RUSH
121700D101 -> PIL13-441-121700-D101-LIQ-RUSH
"""
raw_sample_id = raw_sample_id.strip().upper()

# Try location + date format (e.g., R204121620)
match = SAMPLE_ID_LOCATION_DATE_REGEX.match(raw_sample_id)
if match:
location = match.group(1)
date = match.group(2)
return f"PIL13-441-{date}-{location}-LIQ-RUSH"

# Try date + location format (e.g., 121700D101)
match = SAMPLE_ID_DATE_LOCATION_REGEX.match(raw_sample_id)
if match:
date = match.group(1)
location = match.group(2)
return f"PIL13-441-{date}-{location}-LIQ-RUSH"

return None
TITRATOR_FOLDER = "OrionStarT940 Auto Titrator/Data Exports"
TITRATOR_SITE = "Analytical Data"

Troubleshooting​

Missing Sample ID​

If sample_id is NULL:

  1. The raw sample ID didn't match expected pilot plant formats
  2. Check original_sample_id column for the raw value
  3. Verify the sample ID follows LNNN + MMDDYY or MMDDYY + LNNN format

Missing Curve Data​

If curve data is empty:

  1. Check if the paired Tdata1 file exists in SharePoint
  2. Verify the file prefix matches between Tsum and Tdata1 files
  3. Check Dagster logs for parsing errors

Unpaired Files​

The pipeline attempts to fetch missing pairs from SharePoint. If files remain unpaired:

  1. Verify both files exist in the SharePoint folder
  2. Check that filenames have matching prefixes (before _Tsum.csv / _Tdata1.csv)

Source Files​

  • apps/datasmart/src/datasmart/assets/analytical/tritation.py - Dagster asset definitions