Scoring Module

Terrain suitability scoring functions for outdoor activities.

This module provides scoring functions for evaluating terrain suitability for activities like sledding and cross-country skiing. Scores are normalized to [0, 1] range where 1.0 = optimal conditions.

Core Scoring Functions

src.terrain.scoring.trapezoid_score(value, min_value, optimal_min, optimal_max, max_value)[source]

Compute trapezoid (sweet spot) scoring for a parameter.

The trapezoid function creates a sweet spot scoring pattern: - Below min_value: score = 0 - Between min_value and optimal_min: linear ramp from 0 to 1 - Between optimal_min and optimal_max: score = 1 (optimal range) - Between optimal_max and max_value: linear ramp from 1 to 0 - Above max_value: score = 0

This is useful for parameters where there’s a “just right” range and both too little and too much are bad.

Parameters:
  • value – The value(s) to score (scalar or array)

  • min_value (float) – Minimum acceptable value (below this scores 0)

  • optimal_min (float) – Start of optimal range (scores 1.0)

  • optimal_max (float) – End of optimal range (scores 1.0)

  • max_value (float) – Maximum acceptable value (above this scores 0)

Returns:

Score(s) in range [0, 1], same shape as value

Return type:

ndarray

Example

>>> # Snow depth scoring: 4-8-16-24 inches
>>> trapezoid_score(12.0, 4.0, 8.0, 16.0, 24.0)
1.0  # In optimal range
>>> trapezoid_score(6.0, 4.0, 8.0, 16.0, 24.0)
0.5  # Halfway up ramp

Compute trapezoid (sweet spot) scoring pattern.

Trapezoid scoring pattern:

  • Below min_value: score = 0 (too low)

  • min_valueoptimal_min: linear ramp 0→1

  • optimal_minoptimal_max: score = 1 (sweet spot!)

  • optimal_maxmax_value: linear ramp 1→0

  • Above max_value: score = 0 (too high)

Example:

from src.terrain.scoring import trapezoid_score

# Snow depth scoring: 4-8-16-24 inches
# (too shallow, ramp up, optimal, ramp down, too deep)
scores = trapezoid_score(
    snow_depth,
    min_value=4.0,
    optimal_min=8.0,
    optimal_max=16.0,
    max_value=24.0
)

# 12" snow → score = 1.0 (in sweet spot)
# 6" snow → score = 0.5 (halfway up ramp)
# 2" snow → score = 0.0 (too shallow)

Sledding Scoring

src.terrain.scoring.compute_sledding_score(snow_depth, slope, coverage_months, roughness)[source]

Compute overall sledding suitability score.

Combines multiple factors using a multiplicative model where: 1. Deal breakers → immediate zero score 2. Base score = snow_score × slope_score × coverage_score 3. Final score = base_score × synergy_bonus

The multiplicative approach ensures that poor performance in any one factor significantly reduces the overall score, while synergies can boost exceptional combinations.

Parameters:
  • snow_depth – Snow depth in inches (scalar or array)

  • slope – Terrain slope in degrees (scalar or array)

  • coverage_months – Months of snow coverage (scalar or array)

  • roughness – Elevation std dev in meters (scalar or array) - physical terrain roughness

Returns:

Score(s) in range [0, ~1.5], same shape as inputs (Can exceed 1.0 due to synergy bonuses)

Return type:

ndarray

Example

>>> # Perfect conditions
>>> compute_sledding_score(12.0, 9.0, 4.0, 2.0)
1.12  # High score with bonuses
>>> # Deal breaker (too steep)
>>> compute_sledding_score(12.0, 45.0, 4.0, 2.0)
0.0  # Too steep (>40°)
>>> # Deal breaker (too rough)
>>> compute_sledding_score(12.0, 10.0, 4.0, 8.0)
0.0  # Too rough (>6m)

Compute sledding suitability score from terrain and snow data.

Factors evaluated:

  • Slope (5-15° optimal, steeper = faster but harder)

  • Snow depth (8-16” optimal)

  • Snow coverage duration (more months = better)

  • Terrain roughness (smoother = safer)

Deal breakers (automatic score = 0):

  • Slope > 40° (extreme terrain)

  • Roughness > 6m (cliff faces)

  • Coverage < 0.5 months (too little snow)

Example:

from src.terrain.scoring import compute_sledding_score

sledding_scores = compute_sledding_score(
    slope=slope_data,           # degrees
    snow_depth=swe_data * 10,   # inches
    coverage_months=coverage,   # months
    roughness=roughness_data    # meters
)

# Returns array of scores [0-1]
print(f"Mean score: {sledding_scores.mean():.2f}")

See Snow Integration: Sledding Location Analysis for complete usage.

src.terrain.scoring.sledding_deal_breakers(slope, roughness, coverage_months, max_slope=40.0, max_roughness=6.0, min_coverage=0.5)[source]

Identify deal breaker conditions for sledding.

Some terrain features are absolute deal breakers for sledding: - Slope > 40°: Extreme cliffs (double-black-diamond terrain) - Roughness > 6m: Very rough terrain (cliff faces, boulders) - Coverage < 0.5 months: Not enough snow season

Parameters:
  • slope – Terrain slope in degrees (scalar or array)

  • roughness – Elevation std dev in meters (scalar or array) - physical terrain roughness

  • coverage_months – Months of snow coverage (scalar or array)

  • max_slope (float) – Maximum acceptable slope (default: 40°)

  • max_roughness (float) – Maximum acceptable roughness in meters (default: 6.0m)

  • min_coverage (float) – Minimum acceptable coverage months (default: 0.5)

Returns:

Boolean array indicating deal breaker locations (True = deal breaker)

Return type:

ndarray

Example

>>> sledding_deal_breakers(45.0, 3.0, 3.0)
True  # Too steep (>40°)
>>> sledding_deal_breakers(10.0, 2.0, 3.0)
False  # Acceptable
>>> sledding_deal_breakers(15.0, 8.0, 3.0)
True  # Too rough (>6m)

Identify deal-breaker conditions for sledding.

Returns boolean mask where True = deal breaker (unsuitable).

src.terrain.scoring.sledding_synergy_bonus(slope, snow_depth, coverage_months, roughness)[source]

Compute synergy bonuses for exceptional sledding combinations.

Some combinations of factors create exceptional sledding that’s more than the sum of its parts. This function identifies these synergies and applies multiplicative bonuses.

Synergies (applied in hierarchical priority order): 1. Perfect combo (slope + snow + coverage) = +30% (highest priority) 2. Consistent coverage + good slope + smooth terrain = +15% 3. Moderate slope + very smooth terrain = +20% 4. Smooth terrain + perfect slope = +10% (lowest priority)

Higher priority bonuses prevent lower priority bonuses from applying to avoid over-rewarding.

Parameters:
  • slope – Terrain slope in degrees (scalar or array)

  • snow_depth – Snow depth in inches (scalar or array)

  • coverage_months – Months of snow coverage (scalar or array)

  • roughness – Elevation std dev in meters (scalar or array) - physical terrain roughness

Returns:

Bonus multiplier(s) >= 1.0, same shape as inputs

Return type:

ndarray

Example

>>> sledding_synergy_bonus(9.0, 12.0, 3.5, 2.0)
1.30  # Perfect combo bonus only
>>> sledding_synergy_bonus(9.0, 12.0, 4.0, 1.5)
1.65  # Perfect combo + smooth terrain bonuses stack
>>> sledding_synergy_bonus(15.0, 5.0, 1.0, 5.0)
1.0  # No synergies

Apply bonus for favorable slope + snow depth combinations.

Synergy: Steeper slopes need deeper snow for safe landings.

src.terrain.scoring.coverage_diminishing_returns(coverage_months, tau=2.0)[source]

Score snow coverage with diminishing returns.

Some coverage is critical, but beyond a certain point more coverage doesn’t add much value. Uses exponential saturation:

score = 1 - exp(-coverage_months / tau)

This gives: - 0 months: 0.0 - 1 month: ~0.39 - 2 months: ~0.63 - 4 months: ~0.86 - 6 months: ~0.95

Parameters:
  • coverage_months – Months of snow coverage (scalar or array)

  • tau (float) – Time constant for saturation (default: 2.0 months)

Returns:

Score(s) in range [0, 1), same shape as coverage_months

Return type:

ndarray

Example

>>> coverage_diminishing_returns(2.0)
0.632  # Two months gives ~63% score
>>> coverage_diminishing_returns(6.0)
0.950  # Six months gives ~95% score

Apply diminishing returns to snow coverage duration.

Logic: 3 months vs 2 months is significant; 8 months vs 7 months less so.

Cross-Country Skiing Scoring

src.terrain.scoring.compute_xc_skiing_score(snow_depth, snow_coverage, snow_consistency, min_depth=50.0, optimal_depth_min=100.0, optimal_depth_max=400.0, max_depth=800.0, min_coverage=0.15)[source]

Compute overall cross-country skiing suitability score.

XC skiing scoring focuses on snow conditions (parks handle terrain safety): - Snow depth trapezoid (30% weight): optimal 100-400mm, usable 50-800mm - Snow coverage linear (60% weight): proportional to days with snow - Snow consistency inverted (10% weight): low CV = reliable

Deal breaker: - Snow coverage < 15% (< ~18 days per season) → Score = 0

Final score combines: - Base score: Weighted sum of depth, coverage, consistency - Range: 0 (poor) to 1.0 (excellent)

Parameters:
  • snow_depth (float | ndarray) – Snow depth in mm (SNODAS native units)

  • snow_coverage (float | ndarray) – Fraction of days with snow (0-1)

  • snow_consistency (float | ndarray) – Coefficient of variation (lower is better, 0-1.5)

  • min_depth (float) – Minimum usable snow depth (mm)

  • optimal_depth_min (float) – Lower bound of optimal depth range (mm)

  • optimal_depth_max (float) – Upper bound of optimal depth range (mm)

  • max_depth (float) – Maximum usable snow depth (mm)

  • min_coverage (float) – Minimum snow coverage threshold (15% = 0.15)

Returns:

XC skiing suitability score (0-1)

Return type:

float | ndarray

Examples

>>> compute_xc_skiing_score(250.0, 0.75, 0.3)
0.78  # Excellent conditions
>>> compute_xc_skiing_score(150.0, 0.1, 0.5)
0.0  # Deal breaker - coverage too low
>>> compute_xc_skiing_score(100.0, 0.5, 0.8)
0.53  # Good enough conditions

Compute cross-country skiing suitability score.

Factors evaluated:

  • Slope (0-10° optimal, flatter = easier)

  • Snow depth (6-18” optimal)

  • Snow coverage duration (longer = better season)

  • Terrain roughness (smoother = better)

Deal breakers (automatic score = 0):

  • Slope > 25° (too steep for XC)

  • Coverage < 1.0 months (too little snow)

Example:

from src.terrain.scoring import compute_xc_skiing_score

xc_scores = compute_xc_skiing_score(
    slope=slope_data,
    snow_depth=swe_data * 10,
    coverage_months=coverage,
    roughness=roughness_data
)

See examples/detroit_xc_skiing.py for complete usage.

src.terrain.scoring.xc_skiing_deal_breakers(snow_coverage, min_coverage=0.15)[source]

Identify deal breaker conditions for cross-country skiing.

XC skiing depends primarily on snow reliability. Parks handle terrain safety, so only snow coverage matters as a deal breaker.

Deal breaker: - Snow coverage < 15% of days (< ~18 days per winter season)

Parameters:
  • snow_coverage (float | ndarray) – Fraction of days with snow (0-1)

  • min_coverage (float) – Minimum snow coverage threshold (default 0.15)

Returns:

Boolean or array of booleans indicating deal breaker conditions

Return type:

bool | ndarray

Examples

>>> xc_skiing_deal_breakers(0.5)
False  # 50% coverage is good
>>> xc_skiing_deal_breakers(0.1)
True  # Only 10% coverage - too unreliable
>>> xc_skiing_deal_breakers(np.array([0.1, 0.3, 0.8]))
array([True, False, False])

Identify deal-breaker conditions for cross-country skiing.

Score Combination Patterns

Pattern 1: Multiply individual factor scores

slope_score = trapezoid_score(slope, 5, 10, 15, 25)
depth_score = trapezoid_score(depth, 4, 8, 16, 24)
coverage_score = coverage_diminishing_returns(coverage)

# Multiplicative combination (all factors matter)
final_score = slope_score * depth_score * coverage_score

Pattern 2: Apply deal breakers

# Compute base score
base_score = slope_score * depth_score

# Zero out deal breakers
deal_breaker_mask = sledding_deal_breakers(slope, roughness, coverage)
final_score = np.where(deal_breaker_mask, 0.0, base_score)

Pattern 3: Add synergy bonuses

base_score = slope_score * depth_score * coverage_score

# Bonus for favorable combinations
synergy = sledding_synergy_bonus(slope, depth)
final_score = base_score * (1.0 + 0.2 * synergy)  # Up to 20% bonus

Score Interpretation

Score ranges:

  • 0.8-1.0: Excellent conditions

  • 0.6-0.8: Good conditions

  • 0.4-0.6: Fair conditions

  • 0.2-0.4: Marginal conditions

  • 0.0-0.2: Poor conditions

Visualization:

Use perceptually uniform colormaps for score visualization:

from src.terrain.color_mapping import elevation_colormap

# Visualize scores
colors = elevation_colormap(
    sledding_scores,
    cmap_name='boreal_mako',  # Custom sledding colormap
    min_elev=0.0,
    max_elev=1.0
)

See Also