Skip to content

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: float or array - like

optimal_E

Sabatier-optimal binding energy (eV)

TYPE: float

width

Activity tolerance σ_a (eV). Defines acceptable deviation. Typical value: 0.15 eV

TYPE: float

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: float or array - like

optimal_E

Sabatier-optimal binding energy (eV)

TYPE: float

width

Activity tolerance σ_a (eV). Controls Gaussian width.

TYPE: float

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: ReactionConfig or None

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: ReactionConfig DEFAULT: None

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: float or array - like

gamma_min

Minimum surface energy for normalization. If None (default), uses min value from input data (data-driven).

TYPE: float DEFAULT: None

gamma_max

Maximum surface energy for normalization. If None (default), uses max value from input data (data-driven).

TYPE: float DEFAULT: None

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: float or array - like

cost_min

Minimum cost for normalization. If None (default), uses min value from input data (data-driven).

TYPE: float DEFAULT: None

cost_max

Maximum cost for normalization. If None (default), uses max value from input data (data-driven).

TYPE: float DEFAULT: None

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: float or array - like

optimal_E

Sabatier-optimal binding energy (eV)

TYPE: float

width

Activity tolerance σ_a (eV)

TYPE: float

method

Scoring method (default: 'linear') - 'linear': Linear decay from optimum - 'gaussian': Exponential decay from optimum

TYPE: (linear, gaussian) DEFAULT: 'linear'

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: float or array - like

stability_score

Normalized stability scores [0, 1]

TYPE: float or array - like

cost_score

Normalized cost scores [0, 1]

TYPE: float or array - like

w_a

Activity weight [0, 1]

TYPE: float

w_s

Stability weight [0, 1]

TYPE: float

w_c

Cost weight [0, 1] Constraint: w_a + w_s + w_c = 1

TYPE: float

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:

  1. Equal Weights (0.33, 0.33, 0.34) - DEFAULT
  2. Unbiased exploratory screening
  3. No a priori preference
  4. Recommended starting point

  5. Activity-Focused (0.5, 0.3, 0.2)

  6. Performance-critical applications
  7. Stability less constraining
  8. Research/fundamental studies

  9. Stability-Focused (0.3, 0.5, 0.2)

  10. Long-term operation required
  11. Harsh electrochemical conditions
  12. Industrial durability emphasis

  13. Cost-Focused (0.3, 0.2, 0.5)

  14. Large-scale deployment
  15. Commodity applications
  16. 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)