@@ -71,6 +71,78 @@ def test_skillscores():
7171 skill_lw = ta .skill .liu_weissberg (lon_obs , lat_obs , lon_model , lat_model )
7272 np .testing .assert_almost_equal (skill_lw , 0.99099 , 5 )
7373
74+ def test_skillscores_cumulative ():
75+ lon_obs = np .array ([0 , 1 , 2 , 3 , 4 , 5 ], dtype = float )
76+ lat_obs = np .array ([0 , 0 , 0 , 0 , 0 , 0 ], dtype = float )
77+ lon_model = lon_obs .copy ()
78+ km2deg = 111
79+ lon_model [- 1 ] = lon_obs [- 1 ] + 1.8 / km2deg
80+ lat_model = np .array ([0 , 1.2 / km2deg , - 3.4 / km2deg , 6.3 / km2deg , 4.2 / km2deg , 0 ])
81+
82+ skill_cum = ta .skill .liu_weissberg (lon_obs , lat_obs , lon_model , lat_model , cumulative = True )
83+
84+ # Output length must match input length
85+ assert len (skill_cum ) == len (lon_obs )
86+ # First value is NaN (no arc length at t=0); second value is always valid
87+ assert np .isnan (skill_cum [0 ])
88+ assert not np .isnan (skill_cum [1 ])
89+ # All other values are in [0, 1]
90+ assert np .all (skill_cum [1 :] >= 0 ) and np .all (skill_cum [1 :] <= 1 )
91+ # Last value equals the non-cumulative score
92+ skill_scalar = ta .skill .liu_weissberg (lon_obs , lat_obs , lon_model , lat_model )
93+ np .testing .assert_almost_equal (skill_cum [- 1 ], skill_scalar , 10 )
94+ # Score decreases when divergence accelerates (d_k/L_k exceeds running average)
95+ lon_div = np .array ([0 , 1 , 2 , 3 , 4 , 5 ], dtype = float )
96+ lat_div = np .zeros (6 )
97+ lat_model_div = np .array ([0 , 0.5 / 111 , 2.0 / 111 , 5.0 / 111 , 10.0 / 111 , 18.0 / 111 ]) # super-linear divergence
98+ skill_div = ta .skill .liu_weissberg (lon_div , lat_div , lon_div , lat_div + lat_model_div , cumulative = True )
99+ assert skill_div [- 1 ] < skill_div [- 2 ], "Score should decrease for accelerating divergence"
100+
101+
102+ def test_skillscores_cumulative_2d ():
103+ lon_obs = np .array ([[0 , 1 , 2 , 3 , 4 , 5 ], [0 , 1 , 2 , 3 , 4 , 5 ]], dtype = float ).T
104+ lat_obs = np .zeros_like (lon_obs )
105+ lon_model = lon_obs .copy ()
106+ km2deg = 111
107+ lon_model [:, - 1 ] = lon_obs [:, - 1 ] + 1.8 / km2deg
108+ lat_model = np .array ([[0 , 1.2 / km2deg , - 3.4 / km2deg , 6.3 / km2deg , 4.2 / km2deg , 0 ],
109+ [0 , 1.2 / km2deg , - 3.4 / km2deg , 6.3 / km2deg , 4.2 / km2deg , 0 ]]).T
110+
111+ skill_cum = ta .skill .liu_weissberg (lon_obs , lat_obs , lon_model , lat_model , cumulative = True )
112+
113+ assert skill_cum .shape == (6 , 2 )
114+ assert np .all (np .isnan (skill_cum [0 , :]))
115+ assert not np .any (np .isnan (skill_cum [1 , :]))
116+ assert np .all (skill_cum [1 :, :] >= 0 ) and np .all (skill_cum [1 :, :] <= 1 )
117+ # Last value matches non-cumulative score for each trajectory
118+ skill_scalar = ta .skill .liu_weissberg (lon_obs , lat_obs , lon_model , lat_model )
119+ np .testing .assert_array_almost_equal (skill_cum [- 1 , :], skill_scalar )
120+
121+
122+ def test_skillscores_cumulative_xarray (barents ):
123+ barents = barents .traj .gridtime ('1h' )
124+ b0 = barents .isel (trajectory = 0 ).dropna ('time' )
125+ b1 = barents .isel (trajectory = 1 ).sel (time = slice (b0 .time [0 ], b0 .time [- 1 ]))
126+ b1 = b1 .traj .gridtime (b0 .time )
127+
128+ skill_cum = b0 .traj .skill (b1 , cumulative = True )
129+
130+ # Cumulative result has exactly the same dims and coords as self's internal dataset
131+ assert skill_cum .dims == b0 .traj .ds .lon .dims
132+ assert skill_cum .sizes == {d : b0 .traj .ds .sizes [d ] for d in b0 .traj .ds .lon .dims }
133+ np .testing .assert_array_equal (skill_cum .coords ['time' ].values , b0 .coords ['time' ].values )
134+ assert np .all (np .isnan (skill_cum .isel (time = 0 ).values ))
135+ assert not np .any (np .isnan (skill_cum .isel (time = 1 ).values ))
136+ # Cumulative array values are in [0, 1] beyond t=0
137+ assert np .all (skill_cum .isel (time = slice (1 , None )).values >= 0 )
138+ assert np .all (skill_cum .isel (time = slice (1 , None )).values <= 1 )
139+ # Last value equals the non-cumulative score
140+ skill_scalar = b0 .traj .skill (b1 )
141+ np .testing .assert_array_almost_equal (
142+ skill_cum .isel (time = - 1 ).values .squeeze (),
143+ skill_scalar .values .squeeze (), 5 )
144+
145+
74146def test_skillscores_2d ():
75147 lon_obs = np .array ([[0 , 1 , 2 , 3 , 4 , 5 ], [0 , 1 , 2 , 3 , 4 , 5 ]]).T
76148 lat_obs = np .array ([[0 , 0 , 0 , 0 , 0 , 0 ], [0 , 0 , 0 , 0 , 0 , 0 ]]).T
0 commit comments