@@ -393,6 +393,65 @@ def pft_prices(self):
393393 """
394394 return self ._fao ("_pft_prices" )
395395
396+ # -------------------------------------------------------------------------
397+ # Country-Level Reference Values (for performance score normalization)
398+ # -------------------------------------------------------------------------
399+
400+ # Default reference values for LEVELS (overwritten each year in update())
401+ reference_yield_level : float = 1.0
402+ reference_soilc_level : float = 1.0
403+ reference_moisture_level : float = 1.0
404+
405+ # Default reference values for TRENDS (overwritten each year in update())
406+ # These represent "meaningful" trends for normalization, computed as
407+ # the standard deviation of trends across farmers (not mean, since mean
408+ # trend is often near zero and would cause division issues)
409+ reference_yield_trend : float = 1.0
410+ reference_soilc_trend : float = 1.0
411+ reference_moisture_trend : float = 1.0
412+
413+ def compute_reference_values (self ):
414+ """Compute country-mean reference values for performance normalization.
415+
416+ Called once per year in update() BEFORE farmers are updated.
417+ This ensures all farmers see consistent reference values and avoids
418+ repeated computation during performance comparisons.
419+
420+ Reference values for levels use country means.
421+ Reference values for trends use standard deviation (spread of trends),
422+ which provides a natural scale for "what counts as meaningful change".
423+ """
424+ yields , soilcs , moistures = [], [], []
425+ yield_trends , soilc_trends , moisture_trends = [], [], []
426+
427+ for farmer in self .farmers :
428+ if hasattr (farmer , "cropyield" ):
429+ yields .append (farmer .cropyield )
430+ if hasattr (farmer , "soilc" ):
431+ soilcs .append (farmer .soilc )
432+ if hasattr (farmer , "root_moisture" ):
433+ moistures .append (farmer .root_moisture )
434+
435+ # Collect trends from trackers
436+ tracker = farmer .behaviour .performance_tracker
437+ if tracker is not None :
438+ yield_trends .append (tracker .yield_trend )
439+ soilc_trends .append (tracker .soilc_trend )
440+ moisture_trends .append (tracker .moisture_trend )
441+
442+ # Level references: country mean (with fallback to 1.0)
443+ self .reference_yield_level = float (np .mean (yields )) if yields else 1.0
444+ self .reference_soilc_level = float (np .mean (soilcs )) if soilcs else 1.0
445+ self .reference_moisture_level = float (np .mean (moistures )) if moistures else 1.0
446+
447+ # Trend references: standard deviation of trends (what counts as meaningful)
448+ # Use std rather than mean because mean trend can be ~0 which breaks normalization
449+ # The std represents "typical variation in trends" - a natural scale
450+ # Minimum of 0.1 to avoid extreme values when all trends are identical
451+ self .reference_yield_trend = max (0.1 , float (np .std (yield_trends ))) if yield_trends else 1.0
452+ self .reference_soilc_trend = max (0.1 , float (np .std (soilc_trends ))) if soilc_trends else 1.0
453+ self .reference_moisture_trend = max (0.1 , float (np .std (moisture_trends ))) if moisture_trends else 1.0
454+
396455 # -------------------------------------------------------------------------
397456 # Country-Level Statistics (for non-local spreading)
398457 # -------------------------------------------------------------------------
@@ -459,6 +518,10 @@ def update(self, t):
459518 Computes country-level statistics BEFORE updating farmers to ensure
460519 all farmers see the same country-level data for this timestep.
461520 """
521+ # Compute reference values for performance normalization
522+ # Must run BEFORE farmers update so they see consistent references
523+ self .compute_reference_values ()
524+
462525 # Compute country-level statistics for non-local spreading
463526 # Must run BEFORE farmers update so they see current country stats
464527 self .compute_management_performance (t )
0 commit comments