Scoring Functions¶
Functions for calculating activity, stability, and cost scores.
Module Reference¶
ascicat/scoring.py Mathematical Scoring Functions for ASCI Framework
Implements rigorous normalization and scoring for: - Activity (Sabatier principle: linear & Gaussian) - Stability (surface energy: inverse linear) - Cost (economic viability: logarithmic) - Combined ASCI metric (weighted integration)
All scores normalized to [0, 1] with comprehensive validation.
Author: N. Khossossi Institution: DIFFER (Dutch Institute for Fundamental Energy Research)
Mathematical Framework: φ_ASCI = w_a·S_a(ΔE) + w_s·S_s(γ) + w_c·S_c(C)
References: - Nørskov, J. K. et al. Nat. Chem. 1, 37 (2009) - Greeley, J. et al. Nat. Mater. 5, 909 (2006) - Sabatier, P. Ber. Dtsch. Chem. Ges. 44, 1984 (1911)
Classes¶
ActivityScorer ¶
Activity scoring based on Sabatier principle.
Implements both linear and Gaussian scoring methods to quantify proximity to thermodynamically optimal binding energies.
The Sabatier principle states that optimal catalysts have intermediate binding: too weak → no activation, too strong → no desorption.
| METHOD | DESCRIPTION |
|---|---|
linear : Linear scoring | |
gaussian : Gaussian scoring | |
Functions¶
linear(delta_E, optimal_E, width) staticmethod ¶
Linear activity scoring (DEFAULT METHOD).
Scores decrease linearly with distance from optimal binding energy. Maximum score (1.0) at ΔE = ΔE_opt, zero score at |ΔE - ΔE_opt| ≥ σ_a.
Mathematical Form: S_a(ΔE) = max(0, 1 - |ΔE - ΔE_opt| / σ_a)
| PARAMETER | DESCRIPTION |
|---|---|
delta_E | Adsorption energy (eV) TYPE: |
optimal_E | Sabatier-optimal binding energy (eV) TYPE: |
width | Activity tolerance σ_a (eV). Defines acceptable deviation. Typical value: 0.15 eV TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
float or ndarray | Activity scores normalized to [0, 1] - 1.0: Perfect activity (at optimum) - 0.5: Moderate activity (σ_a/2 from optimum) - 0.0: Poor activity (>σ_a from optimum) |
Notes
Linear scoring advantages: - Computationally efficient - Easy interpretation (proportional to distance) - Consistent with traditional volcano plots - Symmetric treatment of over/underbinding
Examples:
>>> from ascicat.scoring import ActivityScorer
>>> scorer = ActivityScorer()
>>>
>>> # Perfect catalyst at optimum
>>> score = scorer.linear(delta_E=-0.27, optimal_E=-0.27, width=0.15)
>>> print(f"Score: {score:.3f}")
Score: 1.000
>>>
>>> # Good catalyst (0.075 eV from optimum)
>>> score = scorer.linear(delta_E=-0.195, optimal_E=-0.27, width=0.15)
>>> print(f"Score: {score:.3f}")
Score: 0.500
>>>
>>> # Poor catalyst (far from optimum)
>>> score = scorer.linear(delta_E=0.0, optimal_E=-0.27, width=0.15)
>>> print(f"Score: {score:.3f}")
Score: 0.000
References
Greeley, J. et al. Nat. Mater. 5, 909 (2006)
gaussian(delta_E, optimal_E, width) staticmethod ¶
Gaussian activity scoring (ALTERNATIVE METHOD).
Scores decay exponentially with squared distance from optimal energy. Provides sharper discrimination near the Sabatier optimum.
Mathematical Form: S_a(ΔE) = exp(-(ΔE - ΔE_opt)² / (2σ_a²))
| PARAMETER | DESCRIPTION |
|---|---|
delta_E | Adsorption energy (eV) TYPE: |
optimal_E | Sabatier-optimal binding energy (eV) TYPE: |
width | Activity tolerance σ_a (eV). Controls Gaussian width. TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
float or ndarray | Activity scores normalized to [0, 1] - 1.0: At optimum - 0.61: At σ_a from optimum (1 standard deviation) - 0.14: At 2σ_a from optimum |
Notes
Gaussian scoring advantages: - Sharper discrimination near optimum - Never reaches exactly zero - Better for narrow volcano peaks - Mathematically smooth derivatives
Disadvantages: - Less intuitive than linear - More aggressive penalization
Examples:
>>> from ascicat.scoring import ActivityScorer
>>> scorer = ActivityScorer()
>>>
>>> # Compare linear vs Gaussian
>>> energies = np.array([-0.42, -0.35, -0.27, -0.19, -0.12])
>>> linear_scores = scorer.linear(energies, -0.27, 0.15)
>>> gaussian_scores = scorer.gaussian(energies, -0.27, 0.15)
>>>
>>> for E, lin, gauss in zip(energies, linear_scores, gaussian_scores):
... print(f"ΔE={E:+.2f}: Linear={lin:.3f}, Gaussian={gauss:.3f}")
ΔE=-0.42: Linear=0.000, Gaussian=0.135
ΔE=-0.35: Linear=0.467, Gaussian=0.606
ΔE=-0.27: Linear=1.000, Gaussian=1.000
ΔE=-0.19: Linear=0.467, Gaussian=0.606
ΔE=-0.12: Linear=0.000, Gaussian=0.135
References
Nørskov, J. K. et al. Nat. Chem. 1, 37 (2009)
ScoringFunctions ¶
Unified interface for all ASCI scoring functions.
Provides both static methods and instance methods for scoring operations. Useful for batch processing and configuration management.
| ATTRIBUTE | DESCRIPTION |
|---|---|
config | Reaction configuration (if initialized with config) TYPE: |
| METHOD | DESCRIPTION |
|---|---|
score_activity_linear : Linear activity scoring | |
score_activity_gaussian : Gaussian activity scoring | |
score_stability : Stability scoring | |
score_cost : Cost scoring | |
combined_asci_score : Full ASCI calculation | |
Examples:
>>> from ascicat.scoring import ScoringFunctions
>>> from ascicat.config import get_reaction_config
>>>
>>> # Initialize with HER configuration
>>> config = get_reaction_config('HER')
>>> scorer = ScoringFunctions(config)
>>>
>>> # Score single catalyst
>>> activity = scorer.score_activity_linear(-0.25)
>>> stability = scorer.score_stability(0.52)
>>> cost = scorer.score_cost(8.5)
>>> asci = scorer.combined_asci_score(activity, stability, cost,
... 0.33, 0.33, 0.34)
>>> print(f"ASCI: {asci:.3f}")
ASCI: 0.948
Functions¶
__init__(config=None) ¶
Initialize ScoringFunctions.
| PARAMETER | DESCRIPTION |
|---|---|
config | Reaction configuration for automatic parameter loading TYPE: |
score_activity_linear(delta_E) ¶
Linear activity scoring using config parameters.
score_activity_gaussian(delta_E) ¶
Gaussian activity scoring using config parameters.
score_stability(surface_energy, gamma_min=0.1, gamma_max=5.0) staticmethod ¶
Stability scoring (static method).
score_cost(cost, cost_min=1.0, cost_max=200000.0) staticmethod ¶
Cost scoring (static method).
combined_asci_score(activity_score, stability_score, cost_score, w_a, w_s, w_c) staticmethod ¶
Combined ASCI calculation (static method).
Functions¶
score_stability(surface_energy, gamma_min=None, gamma_max=None) ¶
Stability scoring via inverse linear normalization.
Lower surface energy indicates stronger metal-metal bonding and enhanced thermodynamic stability against reconstruction/dissolution.
Mathematical Form: S_s(γ) = (γ_max - γ) / (γ_max - γ_min)
Physical Interpretation: - Low γ → Strong bonding → High stability → High score - High γ → Weak bonding → Low stability → Low score
| PARAMETER | DESCRIPTION |
|---|---|
surface_energy | Surface energy γ (J/m²) TYPE: |
gamma_min | Minimum surface energy for normalization. If None (default), uses min value from input data (data-driven). TYPE: |
gamma_max | Maximum surface energy for normalization. If None (default), uses max value from input data (data-driven). TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
float or ndarray | Stability scores normalized to [0, 1] - 1.0: Maximum stability (γ = γ_min) - 0.0: Minimum stability (γ = γ_max) |
| RAISES | DESCRIPTION |
|---|---|
ValueError | If gamma_min >= gamma_max If any surface energy is negative (physically impossible) |
Notes
Data-Driven Normalization (Default): When gamma_min and gamma_max are None, the function automatically computes them from the input data. This ensures scores span the full [0, 1] range, which is scientifically correct for ranking.
Surface Energy Physical Ranges: - Pt(111): ~0.52 J/m² (highly stable) - Cu(111): ~1.83 J/m² (moderately stable) - Open surfaces: 2-4 J/m² (less stable)
Examples:
>>> from ascicat.scoring import score_stability
>>>
>>> # Data-driven normalization (recommended)
>>> gammas = np.array([0.5, 1.0, 2.0, 3.0, 4.0])
>>> scores = score_stability(gammas) # Auto min/max
>>> print(f"Score range: [{scores.min():.3f}, {scores.max():.3f}]")
Score range: [0.000, 1.000]
References
Hansen, H. A. et al. Phys. Chem. Chem. Phys. 10, 3722 (2008)
score_cost(cost, cost_min=None, cost_max=None) ¶
Cost scoring via logarithmic normalization.
Logarithmic scaling handles the enormous range (5 orders of magnitude) in material costs across the periodic table while maintaining discrimination throughout the spectrum.
Mathematical Form: S_c(C) = (log C_max - log C) / (log C_max - log C_min)
Economic Interpretation: - Low cost → Economically viable → High score - High cost → Economically prohibitive → Low score
| PARAMETER | DESCRIPTION |
|---|---|
cost | Material cost ($/kg), composition-weighted for alloys TYPE: |
cost_min | Minimum cost for normalization. If None (default), uses min value from input data (data-driven). TYPE: |
cost_max | Maximum cost for normalization. If None (default), uses max value from input data (data-driven). TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
float or ndarray | Cost scores normalized to [0, 1] - 1.0: Maximum affordability (C = C_min) - 0.0: Minimum affordability (C = C_max) |
| RAISES | DESCRIPTION |
|---|---|
ValueError | If cost_min >= cost_max If any cost is negative or zero (mathematically invalid for log) |
Notes
Data-Driven Normalization (Default): When cost_min and cost_max are None, the function automatically computes them from the input data. This ensures scores span the full [0, 1] range, which is scientifically correct for ranking.
Logarithmic scaling ensures: - $1 vs $10: Significant score difference - $10,000 vs $10,010: Negligible score difference - Physically realistic economic sensitivity
Examples:
>>> from ascicat.scoring import score_cost
>>>
>>> # Data-driven normalization (recommended)
>>> costs = np.array([2.67, 10, 100, 1000, 10000, 107544])
>>> scores = score_cost(costs) # Auto min/max from data
>>> print(f"Score range: [{scores.min():.3f}, {scores.max():.3f}]")
Score range: [0.000, 1.000]
References
U.S. Geological Survey. Mineral Commodity Summaries 2024.
score_activity(delta_E, optimal_E, width, method='linear') ¶
Activity scoring with automatic method selection.
Wrapper function that dispatches to either linear or Gaussian scoring based on user preference. Provides unified interface for activity assessment.
| PARAMETER | DESCRIPTION |
|---|---|
delta_E | Adsorption energy (eV) TYPE: |
optimal_E | Sabatier-optimal binding energy (eV) TYPE: |
width | Activity tolerance σ_a (eV) TYPE: |
method | Scoring method (default: 'linear') - 'linear': Linear decay from optimum - 'gaussian': Exponential decay from optimum TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
float or ndarray | Activity scores normalized to [0, 1] |
| RAISES | DESCRIPTION |
|---|---|
ValueError | If method is not 'linear' or 'gaussian' |
Examples:
>>> from ascicat.scoring import score_activity
>>>
>>> # Linear scoring (default)
>>> score = score_activity(-0.27, optimal_E=-0.27, width=0.15)
>>> print(f"Linear: {score:.3f}")
Linear: 1.000
>>>
>>> # Gaussian scoring
>>> score = score_activity(-0.27, optimal_E=-0.27, width=0.15,
... method='gaussian')
>>> print(f"Gaussian: {score:.3f}")
Gaussian: 1.000
See Also
ActivityScorer.linear : Linear activity scoring ActivityScorer.gaussian : Gaussian activity scoring
calculate_asci(activity_score, stability_score, cost_score, w_a, w_s, w_c) ¶
Calculate Activity-Stability-Cost Index (ASCI).
Combines three normalized descriptor scores into a single unified metric through weighted linear combination. Provides objective catalyst ranking that balances catalytic performance, durability, and economic viability.
Mathematical Form: φ_ASCI = w_a·S_a + w_s·S_s + w_c·S_c
where: S_a ∈ [0, 1]: Activity score (Sabatier principle) S_s ∈ [0, 1]: Stability score (surface energy) S_c ∈ [0, 1]: Cost score (economic viability) w_a + w_s + w_c = 1 (weight normalization)
| PARAMETER | DESCRIPTION |
|---|---|
activity_score | Normalized activity scores [0, 1] TYPE: |
stability_score | Normalized stability scores [0, 1] TYPE: |
cost_score | Normalized cost scores [0, 1] TYPE: |
w_a | Activity weight [0, 1] TYPE: |
w_s | Stability weight [0, 1] TYPE: |
w_c | Cost weight [0, 1] Constraint: w_a + w_s + w_c = 1 TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
float or ndarray | ASCI scores normalized to [0, 1] - 1.0: Ideal catalyst (perfect in all dimensions) - 0.5: Average performance - 0.0: Poor catalyst (fails all criteria) |
| RAISES | DESCRIPTION |
|---|---|
ValueError | If weights don't sum to 1 (within tolerance) If any weight is outside [0, 1] If score arrays have incompatible shapes |
Notes
Weight Selection Guidelines:
- Equal Weights (0.33, 0.33, 0.34) - DEFAULT
- Unbiased exploratory screening
- No a priori preference
-
Recommended starting point
-
Activity-Focused (0.5, 0.3, 0.2)
- Performance-critical applications
- Stability less constraining
-
Research/fundamental studies
-
Stability-Focused (0.3, 0.5, 0.2)
- Long-term operation required
- Harsh electrochemical conditions
-
Industrial durability emphasis
-
Cost-Focused (0.3, 0.2, 0.5)
- Large-scale deployment
- Commodity applications
- Earth-abundant materials priority
Unlike Pareto frontier methods that generate multiple solutions requiring subjective selection, ASCI provides deterministic ranking for reproducible catalyst prioritization.
Examples:
>>> from ascicat.scoring import calculate_asci
>>>
>>> # Perfect catalyst in all dimensions
>>> asci = calculate_asci(1.0, 1.0, 1.0, 0.33, 0.33, 0.34)
>>> print(f"Perfect catalyst: {asci:.3f}")
Perfect catalyst: 1.000
>>>
>>> # Realistic catalyst (good activity, moderate stability, cheap)
>>> asci = calculate_asci(0.85, 0.65, 0.92, 0.33, 0.33, 0.34)
>>> print(f"Cu-based catalyst: {asci:.3f}")
Cu-based catalyst: 0.807
>>>
>>> # Array calculation
>>> activity = np.array([0.9, 0.7, 0.5])
>>> stability = np.array([0.8, 0.6, 0.4])
>>> cost = np.array([0.95, 0.85, 0.75])
>>> asci = calculate_asci(activity, stability, cost, 0.33, 0.33, 0.34)
>>> print(asci)
[0.884 0.717 0.550]
>>>
>>> # Weight sensitivity
>>> # Equal weights
>>> asci_equal = calculate_asci(0.8, 0.6, 0.9, 0.33, 0.33, 0.34)
>>> # Activity-focused
>>> asci_activity = calculate_asci(0.8, 0.6, 0.9, 0.5, 0.3, 0.2)
>>> # Cost-focused
>>> asci_cost = calculate_asci(0.8, 0.6, 0.9, 0.3, 0.2, 0.5)
>>> print(f"Equal: {asci_equal:.3f}, Activity: {asci_activity:.3f}, Cost: {asci_cost:.3f}")
Equal: 0.770, Activity: 0.760, Cost: 0.800
References
Khossossi, N. (2025). ASCICat: Activity-Stability-Cost Integrated Framework for Electrocatalyst Discovery.
Quick Reference¶
Activity Scoring¶
from ascicat.scoring import score_activity
# Linear method (default)
S_a = score_activity(
delta_E=-0.25,
optimal_E=-0.27,
width=0.15,
method='linear'
)
# Gaussian method
S_a = score_activity(
delta_E=-0.25,
optimal_E=-0.27,
width=0.15,
method='gaussian'
)
Stability Scoring¶
from ascicat.scoring import score_stability
# Data-driven normalization
gammas = [0.5, 1.0, 2.0, 3.0]
scores = score_stability(gammas)
# With explicit range
scores = score_stability(gammas, gamma_min=0.1, gamma_max=5.0)
Cost Scoring¶
from ascicat.scoring import score_cost
# Data-driven normalization
costs = [10, 100, 1000, 10000]
scores = score_cost(costs)
# With explicit range
scores = score_cost(costs, cost_min=1, cost_max=200000)
Combined ASCI¶
from ascicat.scoring import calculate_asci
phi = calculate_asci(
activity_score=0.85,
stability_score=0.70,
cost_score=0.90,
w_a=0.33,
w_s=0.33,
w_c=0.34
)
Function Details¶
score_activity¶
Calculate activity score based on Sabatier principle.
def score_activity(
delta_E: float | np.ndarray,
optimal_E: float,
width: float,
method: str = 'linear'
) -> float | np.ndarray:
Parameters:
| Parameter | Type | Description |
|---|---|---|
delta_E | float or array | Adsorption energy (eV) |
optimal_E | float | Sabatier optimum (eV) |
width | float | Activity width σ_a (eV) |
method | str | 'linear' or 'gaussian' |
Returns: Score(s) in [0, 1]
Formulas:
Linear: $\(S_a = \max(0, 1 - |\Delta E - \Delta E_{opt}| / \sigma_a)\)$
Gaussian: $\(S_a = \exp(-(\Delta E - \Delta E_{opt})^2 / (2\sigma_a^2))\)$
score_stability¶
Calculate stability score from surface energy.
def score_stability(
gamma: float | np.ndarray,
gamma_min: float = None,
gamma_max: float = None
) -> float | np.ndarray:
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
gamma | float or array | Required | Surface energy (J/m²) |
gamma_min | float | None | Min for normalization (auto if None) |
gamma_max | float | None | Max for normalization (auto if None) |
Returns: Score(s) in [0, 1]
Formula: $\(S_s = (\gamma_{max} - \gamma) / (\gamma_{max} - \gamma_{min})\)$
score_cost¶
Calculate cost score with logarithmic normalization.
def score_cost(
cost: float | np.ndarray,
cost_min: float = None,
cost_max: float = None
) -> float | np.ndarray:
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
cost | float or array | Required | Material cost ($/kg) |
cost_min | float | None | Min for normalization (auto if None) |
cost_max | float | None | Max for normalization (auto if None) |
Returns: Score(s) in [0, 1]
Formula: $\(S_c = (\log C_{max} - \log C) / (\log C_{max} - \log C_{min})\)$
calculate_asci¶
Calculate combined ASCI score.
def calculate_asci(
activity_score: float | np.ndarray,
stability_score: float | np.ndarray,
cost_score: float | np.ndarray,
w_a: float,
w_s: float,
w_c: float
) -> float | np.ndarray:
Parameters:
| Parameter | Type | Description |
|---|---|---|
activity_score | float or array | Activity score S_a |
stability_score | float or array | Stability score S_s |
cost_score | float or array | Cost score S_c |
w_a | float | Activity weight |
w_s | float | Stability weight |
w_c | float | Cost weight |
Returns: ASCI score(s) in [0, 1]
Formula: $\(\phi_{ASCI} = w_a \cdot S_a + w_s \cdot S_s + w_c \cdot S_c\)$
ScoringFunctions Class¶
from ascicat.scoring import ScoringFunctions
scorer = ScoringFunctions()
# Activity scores
S_a = scorer.activity_linear(delta_E, optimal_E, width)
S_a = scorer.activity_gaussian(delta_E, optimal_E, width)
# Stability score
S_s = scorer.stability_inverse_linear(gamma, gamma_min, gamma_max)
# Cost score
S_c = scorer.cost_logarithmic(cost, cost_min, cost_max)
# Combined
phi = scorer.combined_asci_score(S_a, S_s, S_c, w_a, w_s, w_c)
ActivityScorer Class¶
Specialized class for activity scoring:
from ascicat.scoring import ActivityScorer
scorer = ActivityScorer()
# Linear method
S_a = scorer.linear(delta_E=-0.25, optimal_E=-0.27, width=0.15)
# Gaussian method
S_a = scorer.gaussian(delta_E=-0.25, optimal_E=-0.27, width=0.15)
# With array input
import numpy as np
energies = np.array([-0.42, -0.27, -0.12])
scores = scorer.linear(energies, optimal_E=-0.27, width=0.15)