Skip to content

Examples and Tutorials

This page provides comprehensive examples demonstrating fr5's capabilities across different use cases and applications.

Basic Examples

1. Property Prediction Basics

Ionic Liquid Viscosity

from fr5.property_prediction_il import predict_viscosity

# Single prediction
result = predict_viscosity({
    'cation': 'EMIM',      # 1-ethyl-3-methylimidazolium  
    'anion': 'BF4',        # tetrafluoroborate
    'temperature': 298.15   # 25°C
})

print(f"Viscosity: {result['viscosity']:.2f} mPa·s")
print(f"Uncertainty: ±{result['uncertainty']:.2f} mPa·s")

# Temperature sweep
temperatures = [298.15, 323.15, 348.15, 373.15]
for temp in temperatures:
    result = predict_viscosity({
        'cation': 'EMIM',
        'anion': 'BF4', 
        'temperature': temp
    })
    print(f"At {temp-273.15:.0f}°C: {result['viscosity']:.1f} mPa·s")

MOF CO₂ Uptake

from fr5.property_prediction_mof import predict_co2_uptake

# From known MOF
result = predict_co2_uptake(
    mof_name='HKUST-1',
    pressure=1.0,          # 1 bar
    temperature=298.15     # 25°C  
)

print(f"HKUST-1 CO₂ uptake: {result['uptake']:.2f} mmol/g")

# From structure file
result = predict_co2_uptake(
    mof_structure='path/to/ZIF-8.cif',
    pressure=10.0,         # 10 bar
    temperature=298.15
)

print(f"ZIF-8 CO₂ uptake at 10 bar: {result['uptake']:.2f} mmol/g")

2. Molecular Descriptors

Generate IL Descriptors

from fr5.descriptors import generate_il_descriptors

# Generate comprehensive descriptors
descriptors = generate_il_descriptors(
    cation='BMIM',         # 1-butyl-3-methylimidazolium
    anion='PF6',           # hexafluorophosphate
    descriptor_types=['fragment', 'property', 'topological']
)

print(f"Generated {len(descriptors)} descriptors:")
for name, value in list(descriptors.items())[:10]:
    print(f"  {name}: {value:.3f}")

Batch Descriptor Generation

from fr5.descriptors import batch_generate_descriptors

# Define IL library
il_library = [
    {'cation': 'EMIM', 'anion': 'BF4'},
    {'cation': 'BMIM', 'anion': 'PF6'},
    {'cation': 'HMIM', 'anion': 'Tf2N'}
]

# Generate descriptors for all ILs
all_descriptors = batch_generate_descriptors(
    molecules=il_library,
    descriptor_types=['fragment', 'property'],
    n_jobs=2  # Parallel processing
)

print(f"Generated descriptors for {len(all_descriptors)} ionic liquids")

Intermediate Examples

3. High-Throughput Screening

Screen IL Library for Optimal Properties

import pandas as pd
from fr5.property_prediction_il import predict_viscosity_batch, predict_density_batch

# Define comprehensive IL library
cations = ['EMIM', 'BMIM', 'HMIM', 'OMIM']
anions = ['BF4', 'PF6', 'Tf2N', 'OTf', 'Cl']
temperatures = [298.15, 323.15, 348.15]

# Generate all combinations
il_combinations = []
for cation in cations:
    for anion in anions:
        for temp in temperatures:
            il_combinations.append({
                'cation': cation,
                'anion': anion, 
                'temperature': temp
            })

print(f"Screening {len(il_combinations)} IL combinations...")

# Predict properties
viscosity_results = predict_viscosity_batch(il_combinations)
density_results = predict_density_batch(il_combinations)

# Combine results into DataFrame
results_df = pd.DataFrame(il_combinations)
results_df['viscosity'] = [r['viscosity'] for r in viscosity_results]
results_df['density'] = [r['density'] for r in density_results]

# Find optimal ILs (low viscosity, high density)
optimal_ils = results_df[
    (results_df['viscosity'] < 20.0) &     # < 20 mPa·s
    (results_df['density'] > 1.3)          # > 1.3 g/cm³
].sort_values('viscosity')

print(f"\nFound {len(optimal_ils)} optimal ionic liquids:")
print(optimal_ils[['cation', 'anion', 'temperature', 'viscosity', 'density']].head())

MOF Database Screening

from fr5.property_prediction_mof import screen_mof_database

# Screen for CO₂ capture applications
screening_results = screen_mof_database(
    database_path='CoRE_MOF_structures/',
    properties=['co2_uptake', 'selectivity_co2_n2'],
    conditions={
        'pressure': 1.0,
        'temperature': 298.15
    },
    filters={
        'pore_volume': (0.7, 2.0),      # cm³/g
        'surface_area': (1000, 3000),   # m²/g  
    },
    n_jobs=4
)

# Get top performers
top_mofs = screening_results.get_top_performers(
    criterion='co2_uptake',
    n_top=20
)

print("Top 20 MOFs for CO₂ uptake:")
for i, mof in enumerate(top_mofs, 1):
    print(f"{i:2d}. {mof['name']:20s} "
          f"{mof['co2_uptake']:6.2f} mmol/g "
          f"(selectivity: {mof['selectivity_co2_n2']:5.1f})")

4. Data Analysis and Visualization

import matplotlib.pyplot as plt
import seaborn as sns
from fr5.shared.visualization import plot_property_comparison

# Analyze temperature dependence
temperature_data = []
for temp in range(273, 374, 10):
    result = predict_viscosity({
        'cation': 'EMIM',
        'anion': 'BF4',
        'temperature': temp
    })
    temperature_data.append({
        'temperature_C': temp - 273.15,
        'viscosity': result['viscosity']
    })

temp_df = pd.DataFrame(temperature_data)

# Plot temperature dependence
plt.figure(figsize=(8, 6))
plt.semilogy(temp_df['temperature_C'], temp_df['viscosity'], 'bo-')
plt.xlabel('Temperature (°C)')
plt.ylabel('Viscosity (mPa·s)')
plt.title('EMIM-BF4 Viscosity vs Temperature')
plt.grid(True, alpha=0.3)
plt.show()

Compare Multiple Properties

from fr5.shared.visualization import plot_multi_property_comparison

# Generate data for multiple ILs
il_names = ['EMIM-BF4', 'BMIM-PF6', 'HMIM-Tf2N']
properties = ['viscosity', 'density', 'thermal_stability']

comparison_data = []
for il_name in il_names:
    cation, anion = il_name.split('-')

    visc_result = predict_viscosity({
        'cation': cation, 'anion': anion, 'temperature': 298.15
    })
    dens_result = predict_density({
        'cation': cation, 'anion': anion, 'temperature': 298.15
    })

    comparison_data.append({
        'IL': il_name,
        'viscosity': visc_result['viscosity'],
        'density': dens_result['density'],
        'thermal_stability': 350  # Placeholder
    })

# Create multi-property radar chart
plot_multi_property_comparison(
    data=comparison_data,
    properties=['viscosity', 'density', 'thermal_stability'],
    normalize=True,
    title='IL Property Comparison'
)

Advanced Examples

5. Bayesian Optimization

Single-Objective Reaction Optimization

import pandas as pd
from fr5.bayes_opt import BOExperimentManager

# Define reaction parameter space
reaction_variables = [
    {
        'name': 'temperature',
        'type': 'continuous',
        'bounds': (50, 120),
        'unit': '°C'
    },
    {
        'name': 'catalyst_loading',
        'type': 'continuous', 
        'bounds': (0.5, 10.0),
        'unit': 'mol%'
    },
    {
        'name': 'reaction_time',
        'type': 'continuous',
        'bounds': (1.0, 24.0),
        'unit': 'hours'
    },
    {
        'name': 'solvent',
        'type': 'categorical',
        'categories': ['toluene', 'dmf', 'ethanol', 'water']
    }
]

# Historical experimental data
initial_experiments = pd.DataFrame({
    'temperature': [70, 90, 110, 80, 100],
    'catalyst_loading': [2.0, 5.0, 8.0, 3.0, 6.0],
    'reaction_time': [4, 8, 12, 6, 10],
    'solvent': ['toluene', 'dmf', 'ethanol', 'toluene', 'dmf'],
    'yield': [34.2, 67.8, 89.3, 45.6, 78.9]
})

# Set up single-objective optimization (maximize yield)
optimizer = BOExperimentManager(
    input_details=reaction_variables,
    initial_experiments=initial_experiments,
    db_path="reaction_optimization.db",
    objective_names=['yield'],
    strategy_type='single_objective',
    acquisition_function='qUCB'
)

# Optimization loop
for iteration in range(10):
    print(f"\n--- Optimization Iteration {iteration + 1} ---")

    # Get next experiment suggestions
    suggestions = optimizer.suggest_experiments(batch_size=1)
    print(f"Suggested experiment:")
    print(suggestions)

    # Simulate experimental result (replace with actual experiment)
    suggested_conditions = suggestions.iloc[0]
    simulated_yield = simulate_reaction_yield(suggested_conditions)

    # Add result to database
    new_experiment = suggested_conditions.copy()
    new_experiment['yield'] = simulated_yield

    optimizer.add_experiments(pd.DataFrame([new_experiment]))

    # Check progress
    best_result = optimizer.get_best_results()
    print(f"Best yield so far: {best_result['yield']:.1f}%")
    print(f"Best conditions: {best_result['conditions']}")

def simulate_reaction_yield(conditions):
    """Simulate reaction yield based on conditions (replace with real experiment)"""
    # Simple model for demonstration
    temp_effect = (conditions['temperature'] - 50) / 70  # 0-1 scale
    cat_effect = conditions['catalyst_loading'] / 10     # 0-1 scale
    time_effect = min(conditions['reaction_time'] / 12, 1)  # 0-1 scale

    solvent_effects = {
        'toluene': 0.7, 'dmf': 0.9, 'ethanol': 0.6, 'water': 0.4
    }
    solvent_effect = solvent_effects[conditions['solvent']]

    # Add some noise
    import random
    noise = random.gauss(0, 5)  # ±5% random noise

    yield_estimate = 100 * temp_effect * cat_effect * time_effect * solvent_effect + noise
    return max(0, min(100, yield_estimate))  # Clamp between 0-100%

Multi-Objective Optimization

# Multi-objective optimization (maximize yield and selectivity, minimize cost)
mo_optimizer = BOExperimentManager(
    input_details=reaction_variables,
    initial_experiments=multi_objective_data,  # Include yield, selectivity, cost
    db_path="multi_objective_optimization.db", 
    objective_names=['yield', 'selectivity', 'cost'],
    strategy_type='mobo_pareto',
    acquisition_function='qEHVI',
    ref_point=[0.0, 0.0, 100.0]  # Reference point for hypervolume
)

# Run optimization
for iteration in range(15):
    suggestions = mo_optimizer.suggest_experiments(batch_size=2)

    # Simulate experiments (replace with real experiments)
    new_results = []
    for _, conditions in suggestions.iterrows():
        result = {
            **conditions.to_dict(),
            'yield': simulate_reaction_yield(conditions),
            'selectivity': simulate_selectivity(conditions), 
            'cost': calculate_reaction_cost(conditions)
        }
        new_results.append(result)

    mo_optimizer.add_experiments(pd.DataFrame(new_results))

    # Analyze Pareto front
    pareto_front = mo_optimizer.get_pareto_front()
    print(f"Iteration {iteration + 1}: Pareto front has {len(pareto_front)} points")

# Plot final Pareto front
mo_optimizer.plot_pareto_front(
    objectives=['yield', 'selectivity'],
    save_path='pareto_front.png'
)

6. Machine Learning with Custom Data

Train Custom Property Prediction Model

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, r2_score
from fr5.descriptors import generate_il_descriptors

# Load your experimental data
experimental_data = pd.read_csv('my_il_experiments.csv')
print(f"Loaded {len(experimental_data)} experimental data points")

# Generate descriptors for all ILs
print("Generating molecular descriptors...")
descriptor_data = []
for _, row in experimental_data.iterrows():
    descriptors = generate_il_descriptors(
        cation=row['cation'],
        anion=row['anion'],
        temperature=row['temperature'],
        descriptor_types=['fragment', 'property']
    )
    descriptor_data.append(descriptors)

# Convert to feature matrix
feature_names = list(descriptor_data[0].keys())
X = np.array([[desc[name] for name in feature_names] for desc in descriptor_data])
y = experimental_data['target_property'].values

print(f"Feature matrix shape: {X.shape}")

# Train-test split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# Train model
model = RandomForestRegressor(
    n_estimators=100,
    max_depth=10,
    random_state=42,
    n_jobs=-1
)

print("Training model...")
model.fit(X_train, y_train)

# Evaluate model
y_pred = model.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)

print(f"\nModel Performance:")
print(f"  RMSE: {rmse:.3f}")
print(f"  R²: {r2:.3f}")

# Feature importance analysis
feature_importance = pd.DataFrame({
    'feature': feature_names,
    'importance': model.feature_importances_
}).sort_values('importance', ascending=False)

print(f"\nTop 10 Most Important Features:")
print(feature_importance.head(10))

# Make predictions for new ILs
new_ils = [
    {'cation': 'EMIM', 'anion': 'OTf', 'temperature': 298.15},
    {'cation': 'PBMIM', 'anion': 'Tf2N', 'temperature': 323.15}
]

print(f"\nPredictions for new ILs:")
for il in new_ils:
    descriptors = generate_il_descriptors(**il, descriptor_types=['fragment', 'property'])
    features = np.array([[descriptors[name] for name in feature_names]])
    prediction = model.predict(features)[0]
    print(f"{il['cation']}-{il['anion']} at {il['temperature']-273.15:.0f}°C: {prediction:.2f}")

7. Reaction Analysis and Kinetics

Analyze Reaction Database

from fr5.reaction_analysis import analyze_reaction_database, predict_reaction_outcome

# Load reaction database
reaction_db = pd.read_csv('hydrogenation_database.csv')

# Comprehensive database analysis
analysis = analyze_reaction_database(
    database=reaction_db,
    reaction_type='hydrogenation',
    analysis_types=[
        'conversion_trends',
        'catalyst_comparison', 
        'solvent_effects',
        'temperature_dependence'
    ]
)

# Generate analysis report
analysis.generate_report(
    output_path='hydrogenation_analysis.html',
    include_plots=True
)

# Extract key insights
catalyst_ranking = analysis.catalyst_performance_ranking()
print("Catalyst Performance Ranking:")
for i, (catalyst, performance) in enumerate(catalyst_ranking.items(), 1):
    print(f"{i:2d}. {catalyst:15s} - Avg conversion: {performance['avg_conversion']:.1f}%")

# Predict outcomes for new conditions
new_conditions = {
    'substrate': 'styrene',
    'catalyst': 'Pd/C',
    'solvent': 'ethanol',
    'temperature': 60,    # °C
    'pressure': 1.0,      # atm
    'time': 4.0          # hours
}

prediction = predict_reaction_outcome(
    reaction_type='hydrogenation',
    conditions=new_conditions
)

print(f"\nPredicted outcome for new conditions:")
print(f"  Conversion: {prediction['conversion']:.1f}%")
print(f"  Selectivity: {prediction['selectivity']:.1f}%") 
print(f"  Rate constant: {prediction['rate_constant']:.2e} h⁻¹")

Kinetic Analysis

from fr5.reaction_analysis import analyze_kinetics
import matplotlib.pyplot as plt

# Time-course experimental data
time_points = [0, 0.5, 1, 2, 4, 8, 12, 24]  # hours
conversion_data = [0, 12, 23, 38, 58, 75, 84, 91]  # %

# Fit kinetic models
kinetic_analysis = analyze_kinetics(
    time=time_points,
    conversion=conversion_data,
    models=['first_order', 'second_order', 'langmuir_hinshelwood']
)

# Compare models
print("Kinetic Model Comparison:")
for model_name, results in kinetic_analysis.model_results.items():
    print(f"{model_name:20s}: R² = {results['r_squared']:.3f}, "
          f"k = {results['rate_constant']:.2e}")

# Best model
best_model = kinetic_analysis.best_model
print(f"\nBest fitting model: {best_model['name']}")
print(f"Rate constant: {best_model['rate_constant']:.2e} h⁻¹")

# Plot experimental data and model fits
plt.figure(figsize=(10, 6))
plt.plot(time_points, conversion_data, 'ro', label='Experimental', markersize=8)

for model_name, results in kinetic_analysis.model_results.items():
    time_fine = np.linspace(0, 24, 100)
    conversion_fit = results['fit_function'](time_fine)
    plt.plot(time_fine, conversion_fit, '--', 
             label=f"{model_name} (R² = {results['r_squared']:.3f})")

plt.xlabel('Time (hours)')
plt.ylabel('Conversion (%)')
plt.title('Kinetic Model Fitting')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

Integration Examples

8. Complete Workflow: From Screening to Optimization

import pandas as pd
import numpy as np
from fr5.property_prediction_il import predict_viscosity_batch
from fr5.descriptors import generate_il_descriptors
from fr5.bayes_opt import BOExperimentManager
from fr5.shared.visualization import plot_optimization_progress

# Step 1: High-throughput virtual screening
print("Step 1: Virtual screening of IL library...")

# Define large IL library
cations = ['EMIM', 'BMIM', 'HMIM', 'OMIM', 'PMIM']
anions = ['BF4', 'PF6', 'Tf2N', 'OTf', 'TCM', 'DCA']

il_library = []
for cation in cations:
    for anion in anions:
        il_library.append({
            'cation': cation,
            'anion': anion,
            'temperature': 298.15
        })

# Predict properties for screening
viscosity_results = predict_viscosity_batch(il_library)

# Create screening results DataFrame
screening_df = pd.DataFrame(il_library)
screening_df['predicted_viscosity'] = [r['viscosity'] for r in viscosity_results]

# Filter for low-viscosity candidates
candidates = screening_df[screening_df['predicted_viscosity'] < 30.0].copy()
print(f"Found {len(candidates)} low-viscosity candidates from {len(il_library)} total ILs")

# Step 2: Descriptor analysis for top candidates
print("\nStep 2: Generating descriptors for top candidates...")

descriptor_data = []
for _, candidate in candidates.iterrows():
    descriptors = generate_il_descriptors(
        cation=candidate['cation'],
        anion=candidate['anion'],
        temperature=candidate['temperature']
    )
    descriptors['cation'] = candidate['cation']
    descriptors['anion'] = candidate['anion']
    descriptors['predicted_viscosity'] = candidate['predicted_viscosity']
    descriptor_data.append(descriptors)

descriptor_df = pd.DataFrame(descriptor_data)

# Step 3: Experimental design using Bayesian optimization
print("\nStep 3: Designing optimal experiments...")

# Convert categorical variables for BO
variables = [
    {
        'name': 'temperature',
        'type': 'continuous', 
        'bounds': (273.15, 373.15)
    },
    {
        'name': 'cation',
        'type': 'categorical',
        'categories': candidates['cation'].unique().tolist()
    },
    {
        'name': 'anion', 
        'type': 'categorical',
        'categories': candidates['anion'].unique().tolist()
    }
]

# Select initial experiments (best predictions + diverse sampling)
initial_experiments = candidates.nsmallest(5, 'predicted_viscosity').copy()
initial_experiments['experimental_viscosity'] = initial_experiments['predicted_viscosity'] * (1 + np.random.normal(0, 0.1, 5))

# Set up Bayesian optimization
optimizer = BOExperimentManager(
    input_details=variables,
    initial_experiments=initial_experiments[['cation', 'anion', 'temperature', 'experimental_viscosity']],
    db_path="il_optimization.db",
    objective_names=['experimental_viscosity'],
    strategy_type='single_objective'
)

# Optimization iterations
print("\nRunning optimization iterations...")
for iteration in range(8):
    # Suggest next experiments
    suggestions = optimizer.suggest_experiments(batch_size=2)

    # Simulate experimental results (replace with real experiments)
    simulated_results = []
    for _, suggestion in suggestions.iterrows():
        # Simulate experimental viscosity with some noise around prediction
        predicted = predict_viscosity({
            'cation': suggestion['cation'],
            'anion': suggestion['anion'], 
            'temperature': suggestion['temperature']
        })

        experimental = predicted['viscosity'] * (1 + np.random.normal(0, 0.15))

        simulated_results.append({
            'cation': suggestion['cation'],
            'anion': suggestion['anion'],
            'temperature': suggestion['temperature'],
            'experimental_viscosity': experimental
        })

    # Add results to optimizer
    optimizer.add_experiments(pd.DataFrame(simulated_results))

    # Check progress
    best_result = optimizer.get_best_results()
    print(f"Iteration {iteration + 1}: Best viscosity = {best_result['experimental_viscosity']:.1f} mPa·s")

# Step 4: Final analysis and reporting
print("\nStep 4: Final analysis...")

# Get all experimental data
all_experiments = optimizer.get_all_experiments()

# Find optimal IL
optimal_il = all_experiments.loc[all_experiments['experimental_viscosity'].idxmin()]
print(f"\nOptimal ionic liquid found:")
print(f"  Composition: {optimal_il['cation']}-{optimal_il['anion']}")  
print(f"  Temperature: {optimal_il['temperature']-273.15:.1f}°C")
print(f"  Viscosity: {optimal_il['experimental_viscosity']:.1f} mPa·s")

# Plot optimization progress
plot_optimization_progress(
    optimization_data=all_experiments,
    objective='experimental_viscosity',
    title='Ionic Liquid Viscosity Optimization',
    save_path='il_optimization_progress.png'
)

print(f"\nTotal experiments performed: {len(all_experiments)}")
print(f"Improvement over initial best: {initial_experiments['experimental_viscosity'].min() - optimal_il['experimental_viscosity']:.1f} mPa·s")

Utility Examples

9. Data Management and Validation

from fr5.shared import validate_chemical_data, clean_chemical_data
from fr5.shared.io import DatabaseManager

# Data validation
experimental_data = pd.read_csv('messy_experimental_data.csv')

validation_report = validate_chemical_data(
    data=experimental_data,
    required_columns=['cation', 'anion', 'temperature', 'viscosity'],
    value_ranges={
        'temperature': (200, 400),  # Kelvin
        'viscosity': (0.1, 1000)    # mPa·s
    }
)

if not validation_report['is_valid']:
    print("Data validation issues found:")
    for error in validation_report['errors']:
        print(f"  - {error}")

# Data cleaning
cleaned_data = clean_chemical_data(
    data=experimental_data,
    remove_outliers=True,
    outlier_method='isolation_forest',
    missing_value_strategy='interpolate'
)

print(f"Cleaned data: {len(cleaned_data)} rows (from {len(experimental_data)})")

# Database management
db = DatabaseManager('project_database.db')

# Store cleaned data
db.create_table(
    table_name='clean_experiments',
    data=cleaned_data
)

# Query data
recent_experiments = db.query(
    table_name='clean_experiments',
    filters={'temperature': (290, 310)},
    order_by='viscosity ASC',
    limit=50
)

print(f"Retrieved {len(recent_experiments)} recent experiments")

These examples demonstrate the full capabilities of fr5 across different use cases, from basic property prediction to advanced optimization workflows. Each example can be adapted for your specific research needs and experimental requirements.