Common Workflow Patterns¶
This guide presents common workflow patterns in geological modeling and resource estimation, demonstrating how to implement them using pollywog.
Grade Estimation Workflows¶
Pattern 1: Preprocessing → Estimation → Postprocessing¶
This is the most common workflow in resource estimation:
from pollywog.core import CalcSet, Number
# Step 1: Preprocess drillhole data
# Create a calcset for drillhole composites
preprocess = CalcSet([
# Remove outliers using clamping
Number(name="Au_clamped", "clamp([Au], 0, 100)",
comment_equation="Cap gold at 100 g/t to remove outliers"),
Number(name="Cu_clamped", "clamp([Cu], 0, 5)",
comment_equation="Cap copper at 5% to remove outliers"),
# Log transforms for geostatistics
Number(name="Au_log", "log([Au_clamped] + 0.01)",
comment_equation="Log transform for kriging"),
Number(name="Cu_log", "log([Cu_clamped] + 0.01)",
comment_equation="Log transform for kriging"),
])
# Export for drillhole calculations
preprocess.to_lfcalc("01_drillhole_preprocessing.lfcalc")
# Step 2: Perform estimation in Leapfrog
# (This happens in Leapfrog UI - estimate Au_log and Cu_log)
# Step 3: Postprocess block model
# Back-transform and apply recovery
postprocess = CalcSet([
# Back-transform from log space
Number(name="Au_est", "exp([Au_log_kriged]) - 0.01",
comment_equation="Back-transform from log space"),
Number(name="Cu_est", "exp([Cu_log_kriged]) - 0.01",
comment_equation="Back-transform from log space"),
# Apply minimum mining width dilution
Number(name="Au_diluted", "[Au_est] * 0.95",
comment_equation="5% dilution factor"),
Number(name="Cu_diluted", "[Cu_est] * 0.95",
comment_equation="5% dilution factor"),
# Apply metallurgical recovery
Number(name="Au_recovered", "[Au_diluted] * 0.88",
comment_equation="88% Au recovery"),
Number(name="Cu_recovered", "[Cu_diluted] * 0.82",
comment_equation="82% Cu recovery"),
])
postprocess.to_lfcalc("03_block_postprocessing.lfcalc")
Pattern 2: Multi-Domain Estimation with Proportions¶
When estimating across geological domains with varying proportions:
from pollywog.core import CalcSet, Number
from pollywog.helpers import WeightedAverage
# Define your domains and metals
domains = ["oxide", "transition", "sulfide"]
metals = ["Au", "Ag", "Cu"]
# Assume Leapfrog has estimated:
# - Au_oxide, Au_transition, Au_sulfide (and same for Ag, Cu)
# - prop_oxide, prop_transition, prop_sulfide
# Create weighted averages by domain proportions
weighted_estimates = CalcSet([
WeightedAverage(
variables=[f"{metal}_{domain}" for domain in domains],
weights=[f"prop_{domain}" for domain in domains],
name=f"{metal}_final",
comment=f"Weighted {metal} grade by domain proportions"
)
for metal in metals
])
weighted_estimates.to_lfcalc("weighted_domain_grades.lfcalc")
Pattern 3: Conditional Estimation by Rock Type¶
Apply different estimation approaches based on rock type:
from pollywog.core import CalcSet, Number, If
calcset = CalcSet([
# Use different estimation methods based on rock type
Number(name="Au_final", expression=[
If([
("[rocktype] = 'basalt'", "[Au_ordinary_kriging]"),
("[rocktype] = 'breccia'", "[Au_indicator_kriging]"),
("[rocktype] = 'skarn'", "[Au_nearest_neighbor]"),
], otherwise=["[Au_inverse_distance]"])
], comment_equation="Select estimation method by rock type"),
])
calcset.to_lfcalc("conditional_estimation.lfcalc")
Geometallurgy Workflows¶
Pattern 4: Recovery Models from Test Work¶
Integrate metallurgical test data to predict recovery:
from pollywog.core import CalcSet, Number, If
# Based on geometallurgical domains and test work
recovery_model = CalcSet([
# Gold recovery as a function of grind size and domain
Number(name="Au_recovery", expression=[
If([
("([geo_domain] = 'free_milling') and ([p80] <= 75)", "0.92"),
("([geo_domain] = 'free_milling') and ([p80] > 75)", "0.88"),
("([geo_domain] = 'refractory') and ([p80] <= 75)", "0.78"),
("([geo_domain] = 'refractory') and ([p80] > 75)", "0.72"),
], otherwise=["0.70"])
], comment_equation="Recovery by geo-domain and grind size"),
# Copper recovery based on mineralogy
Number(name="Cu_recovery", expression=[
If([
("[Cu_sulfide_pct] > 80", "0.85"),
("[Cu_sulfide_pct] > 50", "0.78"),
("[Cu_sulfide_pct] > 20", "0.65"),
], otherwise=["0.45"])
], comment_equation="Recovery based on sulfide content"),
# Recoverable metal
Number(name="Au_payable", "[Au_est] * [Au_recovery]",
comment_equation="Payable gold"),
Number(name="Cu_payable", "[Cu_est] * [Cu_recovery]",
comment_equation="Payable copper"),
])
recovery_model.to_lfcalc("geometallurgy_recovery.lfcalc")
Pattern 5: Process Plant Feed Blending¶
Model mill throughput and blending constraints:
from pollywog.core import CalcSet, Number
mill_performance = CalcSet([
# Hardness-based throughput adjustment
Number(name="relative_throughput", expression=[
"100 / (([bond_wi] / 15) ^ 0.82)"
], comment_equation="Throughput relative to 15 kWh/t reference"),
# Tonnes per hour
Number(name="tph", expression=[
"[relative_throughput] * [base_tph]"
], comment_equation="Estimated mill throughput"),
# Metals production per hour
Number(name="Au_oz_per_hour", expression=[
"[Au_payable] * [tph] / 31.1035"
], comment_equation="Gold ounces per hour"),
])
mill_performance.to_lfcalc("mill_throughput.lfcalc")
Economic Evaluation Workflows¶
Pattern 6: Net Smelter Return (NSR)¶
Calculate the value of ore based on multiple commodities:
from pollywog.core import CalcSet, Number
# Define metal prices and costs
nsr_model = CalcSet([
# Gross revenue per tonne
Number(name="Au_revenue_per_t", expression=[
"[Au_recovered] * [Au_price] / 31.1035"
], comment_equation="Gold revenue ($/t), price in $/oz"),
Number(name="Ag_revenue_per_t", expression=[
"[Ag_recovered] * [Ag_price] / 31.1035"
], comment_equation="Silver revenue ($/t), price in $/oz"),
Number(name="Cu_revenue_per_t", expression=[
"[Cu_recovered] * [Cu_price] * 10"
], comment_equation="Copper revenue ($/t), price in $/lb, grade in %"),
# Total gross revenue
Number(name="gross_revenue", expression=[
"[Au_revenue_per_t] + [Ag_revenue_per_t] + [Cu_revenue_per_t]"
], comment_equation="Total revenue per tonne"),
# Deduct costs
Number(name="mining_cost", "35",
comment_equation="Mining cost $/t"),
Number(name="processing_cost", "18",
comment_equation="Processing cost $/t"),
Number(name="admin_cost", "5",
comment_equation="G&A cost $/t"),
# NSR calculation
Number(name="nsr", expression=[
"[gross_revenue] - [mining_cost] - [processing_cost] - [admin_cost]"
], comment_equation="Net Smelter Return ($/t)"),
])
nsr_model.to_lfcalc("economic_nsr.lfcalc")
Pattern 7: Cut-off Grade Classification¶
Classify blocks as ore or waste based on economic cut-off:
from pollywog.core import CalcSet, Number, Category, If
from pollywog.helpers import CategoryFromThresholds
cutoff_classification = CalcSet([
# Economic value (NSR from previous example)
# Assume [nsr] is already calculated
# Simple ore/waste classification
Category(name="ore_waste", expression=[
If("[nsr] >= [cutoff_grade]", "'ore'", "'waste'")
], comment_equation="Binary ore/waste flag"),
# Multi-tier classification
CategoryFromThresholds(
variable="nsr",
thresholds=[0, 20, 40],
categories=["waste", "marginal", "ore", "high_grade"],
"material_type",
comment="Material classification by NSR value"
),
# Tonnage flag (1 for ore, 0 for waste)
Number(name="ore_tonnes_flag", expression=[
If("[nsr] >= [cutoff_grade]", "1", "0")
], comment_equation="Flag for ore tonnage reporting"),
])
cutoff_classification.to_lfcalc("cutoff_classification.lfcalc")
Quality Control Workflows¶
Pattern 8: Data Validation and Flagging¶
Create flags to identify data quality issues:
from pollywog.core import CalcSet, Number, Category, If
qa_qc = CalcSet([
# Flag negative grades
Number(name="flag_negative", expression=[
If("([Au] < 0) or ([Cu] < 0) or ([Ag] < 0)", "1", "0")
], comment_equation="Flag negative assays"),
# Flag extreme values (potential outliers)
Number(name="flag_extreme", expression=[
If("([Au] > 100) or ([Cu] > 10) or ([Ag] > 500)", "1", "0")
], comment_equation="Flag extreme values"),
# Flag missing critical data
Number(name="flag_missing", expression=[
If("(not is_normal([density])) or ([domain] = '')", "1", "0")
], comment_equation="Flag missing density or domain"),
# Overall QA/QC status
Category(name="qa_status", expression=[
If([
("[flag_negative] = 1", "'FAILED_NEGATIVE'"),
("[flag_extreme] = 1", "'REVIEW_OUTLIER'"),
("[flag_missing] = 1", "'FAILED_MISSING'"),
], otherwise=["'PASSED'"])
], comment_equation="Overall QA/QC status"),
])
qa_qc.to_lfcalc("qa_qc_flags.lfcalc")
Pattern 9: Grade Control and Reconciliation¶
Compare estimated vs. actual grades for reconciliation:
from pollywog.core import CalcSet, Number
reconciliation = CalcSet([
# Calculate difference between estimate and actual
Number(name="Au_variance", expression=[
"[Au_actual] - [Au_estimated]"
], comment_equation="Grade variance"),
# Percent difference
Number(name="Au_pct_diff", expression=[
"100 * ([Au_actual] - [Au_estimated]) / [Au_estimated]"
], comment_equation="Percentage difference"),
# Tonnage difference
Number(name="tonnes_variance", expression=[
"[tonnes_actual] - [tonnes_estimated]"
], comment_equation="Tonnage variance"),
# Metal difference
Number(name="metal_variance_oz", expression=[
"([Au_actual] * [tonnes_actual] - [Au_estimated] * [tonnes_estimated]) / 31.1035"
], comment_equation="Metal variance in ounces"),
# Reconciliation ratio
Number(name="recon_ratio", expression=[
"[Au_actual] / [Au_estimated]"
], comment_equation="Actual to estimated ratio"),
])
reconciliation.to_lfcalc("reconciliation.lfcalc")
Machine Learning Integration¶
Pattern 10: Scikit-learn Model Deployment¶
Integrate trained ML models into Leapfrog calculations:
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from pollywog.conversion.sklearn import convert_tree, convert_forest
from pollywog.core import CalcSet
# Example: Predict density from geochemistry
# Training data (from lab measurements)
X_train = np.array([
[0.5, 1.2, 45], # Au, Cu, SiO2
[1.0, 0.8, 52],
[0.3, 2.1, 38],
# ... more training data
])
y_train = np.array([2.7, 2.65, 2.8]) # Measured densities
# Train random forest model
rf_model = RandomForestRegressor(n_estimators=10, max_depth=5, random_state=42)
rf_model.fit(X_train, y_train)
# Convert to Leapfrog calculation
feature_names = ["Au_est", "Cu_est", "SiO2_est"]
density_calc = convert_forest(
rf_model,
feature_names,
"density_predicted",
comment_equation="ML-predicted density from geochemistry"
)
# Create calcset with ML model
ml_calcset = CalcSet([density_calc])
ml_calcset.to_lfcalc("ml_density_prediction.lfcalc")
Pattern 11: Classification Models for Domains¶
Use ML to predict geological domains:
from sklearn.tree import DecisionTreeClassifier
from pollywog.conversion.sklearn import convert_tree
from pollywog.core import CalcSet
# Train domain classifier
# Features: Au, Cu, Ag, Zn, Fe
X_train = np.array([
[0.2, 0.1, 5, 0.5, 3], # Oxide
[1.5, 0.8, 20, 1.2, 5], # Sulfide
[0.8, 0.4, 10, 0.8, 4], # Transition
# ... more training data
])
y_train = ["oxide", "sulfide", "transition", ...] # Domain labels
# Train decision tree classifier
dt_classifier = DecisionTreeClassifier(max_depth=8, random_state=42)
dt_classifier.fit(X_train, y_train)
# Convert to Leapfrog calculation
feature_names = ["Au_composite", "Cu_composite", "Ag_composite", "Zn_composite", "Fe_composite"]
domain_calc = convert_tree(
dt_classifier,
feature_names,
"domain_predicted",
comment_equation="ML-predicted geological domain"
)
domain_calcset = CalcSet([domain_calc])
domain_calcset.to_lfcalc("ml_domain_classification.lfcalc")
Advanced Patterns¶
Pattern 12: Combining Multiple CalcSets¶
Build complex workflows by combining calculation sets:
from pollywog.core import CalcSet, Number
# Create separate calculation sets for different purposes
data_prep = CalcSet([
Number(name="Au_clamped", expression=["clamp([Au], 0, 50)"]),
Number(name="Cu_clamped", expression=["clamp([Cu], 0, 5)"]),
])
estimation_support = CalcSet([
Number(name="Au_log", expression=["log([Au_clamped] + 0.01)"]),
Number(name="Cu_log", expression=["log([Cu_clamped] + 0.01)"]),
])
# Combine them
combined = CalcSet(data_prep.items + estimation_support.items)
combined.to_lfcalc("combined_preprocessing.lfcalc")
Pattern 13: Modular Workflow with Reusable Components¶
Create reusable calculation components:
from pollywog.core import CalcSet, Number
from pollywog.helpers import WeightedAverage
def create_metal_calcs(metal, domains, apply_recovery=True):
"""Generate standard calculations for a metal across domains."""
calcs = [
# Weighted average by domain
WeightedAverage(
variables=[f"{metal}_{d}" for d in domains],
weights=[f"prop_{d}" for d in domains],
name=f"{metal}_composite"
),
]
if apply_recovery:
calcs.append(
Number(name=f"{metal}_recovered",
expression=[f"[{metal}_composite] * [recovery_{metal}]"])
)
return calcs
# Use the function to generate calculations
domains = ["oxide", "transition", "sulfide"]
all_metals = CalcSet([
*create_metal_calcs("Au", domains, apply_recovery=True),
*create_metal_calcs("Ag", domains, apply_recovery=True),
*create_metal_calcs("Cu", domains, apply_recovery=True),
])
all_metals.to_lfcalc("modular_metals.lfcalc")
Pattern 14: Topological Sorting for Dependencies¶
Ensure calculations are ordered correctly:
from pollywog.core import CalcSet, Number
# Create calculations in any order
unordered = CalcSet([
Number(name="final_value", expression=["[intermediate] * 2"]),
Number(name="intermediate", expression=["[Au] + [Ag]"]),
])
# Sort by dependencies
ordered = unordered.topological_sort()
# Now intermediate will be calculated before final_value
ordered.to_lfcalc("properly_ordered.lfcalc")
Tips for Building Effective Workflows¶
Start Simple: Begin with basic calculations and add complexity incrementally
Use Descriptive Names: Make variable names self-documenting
Add Comments: Use
comment_equationparameter to explain business logicTest in Stages: Export and test each stage of the workflow in Leapfrog
Validate Results: Use QA/QC calculations to verify outputs
Version Control: Keep your Python scripts in version control (Git)
Document Assumptions: Record cut-off grades, prices, recoveries in your code
Modularize: Break complex workflows into reusable functions
Handle Edge Cases: Use clamp, conditional logic to handle invalid inputs
Review Dependencies: Use
topological_sort()to ensure proper calculation order
Common Pitfalls to Avoid¶
Missing Parentheses: Always use parentheses in complex expressions
Division by Zero: Clamp denominators away from zero
Log of Zero/Negative: Add small epsilon before taking logarithms
Incorrect Order: Ensure dependent calculations come after their dependencies
Type Mismatches: Use Number for numeric outputs, Category for text
Hardcoded Values: Use variables for parameters that might change
Missing Back-transforms: Remember to back-transform after log-domain estimation
Ignoring Units: Keep track of units (%, ppm, g/t, oz/t, etc.)
See Also¶
Leapfrog Expression Syntax Guide - Detailed syntax reference
Tutorials - Step-by-step tutorials
API Reference - Complete API documentation
Helper Functions Guide - Helper function reference