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