88# the Free Software Foundation, either version 3 of the License, or
99# (at your option) any later version.
1010
11- import json
12- import pathlib
11+ import numpy as np
1312import pytest
14- import wfcommons .utils
1513
16- from typing import Dict , List , Tuple
14+ import wfcommons . utils
1715
1816
1917class TestUtils :
20-
18+
2119 @pytest .mark .unit
22- @pytest .mark .parametrize (
23- "data,distribution" ,
24- [
25- pytest .param ([1 , 2 , 3 ], ("rdist" , (1.5504806356651624 , 0.0013236200991527764 , 0.0013236200991527767 ))),
26- pytest .param ([1 , 1 , 1 ], ("pareto" , (2.25803497307119 , - 7.535551383120264e-19 , 4.806600020282434e-19 ))),
27- ],
28- )
29- def test_best_fit_distribution (self , data : List [float ], distribution : Tuple ) -> None :
30- assert (wfcommons .utils .best_fit_distribution (data ) == distribution )
20+ def test_best_fit_distribution_uses_normalized_samples_and_density_histogram (
21+ self ,
22+ monkeypatch : pytest .MonkeyPatch ,
23+ ) -> None :
24+ # Twenty observations produce two bins under the current
25+ # ceil(len(data) / 10) rule.
26+ data = list (range (20 ))
27+
28+ expected_normalized = np .asarray (data , dtype = float )
29+ expected_normalized = (
30+ (expected_normalized - expected_normalized .min ())
31+ / (expected_normalized .max () - expected_normalized .min ())
32+ )
33+
34+ histogram_call = {}
35+ fit_calls = []
36+ pdf_calls = []
37+
38+ def fake_histogram (values , bins , density = False ):
39+ histogram_call ["values" ] = np .asarray (
40+ values ,
41+ dtype = float ,
42+ ).copy ()
43+ histogram_call ["bins" ] = bins
44+ histogram_call ["density" ] = density
45+
46+ # A valid two-bin density:
47+ # 0.5 * 0.5 + 1.5 * 0.5 == 1.0
48+ return (
49+ np .array ([0.5 , 1.5 ], dtype = float ),
50+ np .array ([0.0 , 0.5 , 1.0 ], dtype = float ),
51+ )
52+
53+ class FakeDistribution :
54+
55+ def __init__ (self , index : int ) -> None :
56+ self .index = index
57+
58+ def fit (self , values ):
59+ fit_calls .append (
60+ np .asarray (values , dtype = float ).copy ()
61+ )
62+ return 0.0 , 1.0
63+
64+ def pdf (self , x , * args , loc , scale ):
65+ pdf_calls .append (
66+ np .asarray (x , dtype = float ).copy ()
67+ )
68+
69+ if self .index == 0 :
70+ # Closest to the density histogram [0.5, 1.5].
71+ return np .array ([0.55 , 1.45 ], dtype = float )
72+
73+ if self .index == 1 :
74+ # Closest to the old min-max-scaled histogram
75+ # [0.0, 1.0]. This candidate would win if that
76+ # obsolete transformation were restored.
77+ return np .array ([0.05 , 0.95 ], dtype = float )
78+
79+ return np .array ([10.0 , 10.0 ], dtype = float )
80+
81+ class FakeStats :
82+
83+ def __init__ (self ) -> None :
84+ self .requested_names = []
85+
86+ def __getattr__ (self , name ):
87+ index = len (self .requested_names )
88+ self .requested_names .append (name )
89+ return FakeDistribution (index )
90+
91+ fake_stats = FakeStats ()
92+
93+ monkeypatch .setattr (
94+ wfcommons .utils .np ,
95+ "histogram" ,
96+ fake_histogram ,
97+ )
98+ monkeypatch .setattr (
99+ wfcommons .utils .scipy ,
100+ "stats" ,
101+ fake_stats ,
102+ )
103+
104+ distribution_name , params = (
105+ wfcommons .utils .best_fit_distribution (data )
106+ )
107+
108+ assert histogram_call ["bins" ] == 2
109+ assert histogram_call ["density" ] is True
110+ np .testing .assert_allclose (
111+ histogram_call ["values" ],
112+ expected_normalized ,
113+ )
114+
115+ # Every candidate must be fitted to the normalized observations,
116+ # not to the histogram heights.
117+ assert fit_calls
118+ for fit_data in fit_calls :
119+ np .testing .assert_allclose (
120+ fit_data ,
121+ expected_normalized ,
122+ )
123+
124+ # The PDF must be evaluated at the centers of [0.0, 0.5]
125+ # and [0.5, 1.0].
126+ expected_centers = np .array ([0.25 , 0.75 ], dtype = float )
127+
128+ assert pdf_calls
129+ for pdf_x in pdf_calls :
130+ np .testing .assert_allclose (
131+ pdf_x ,
132+ expected_centers ,
133+ )
134+
135+ # The first fake distribution is closest to the density
136+ # histogram. The second would win under the old transformation.
137+ assert distribution_name == fake_stats .requested_names [0 ]
138+ assert params == (0.0 , 1.0 )
31139
32140 @pytest .mark .unit
33- @pytest .mark .parametrize (
34- "distribution,min_value,max_value" ,
35- [
36- pytest .param (None , 10 , 100 ),
37- pytest .param ({"name" : "norm" , "params" : [0.08688656476267097 , 0.2572832376513094 ]}, 10 , 100 ),
38- ],
39- )
40- def test_generate_rvs (self , distribution : Dict , min_value : float , max_value : float ) -> None :
41- assert (min_value <= wfcommons .utils .generate_rvs (distribution , min_value , max_value ) <= max_value )
141+ def test_best_fit_distribution_returns_usable_fit (self ) -> None :
142+ rng = np .random .default_rng (12345 )
143+ data = rng .lognormal (
144+ mean = 0.0 ,
145+ sigma = 0.5 ,
146+ size = 30 ,
147+ ).tolist ()
148+
149+ distribution_name , params = (
150+ wfcommons .utils .best_fit_distribution (data )
151+ )
152+
153+ assert isinstance (distribution_name , str )
154+ assert distribution_name
155+ assert hasattr (
156+ wfcommons .utils .scipy .stats ,
157+ distribution_name ,
158+ )
159+
160+ params_array = np .asarray (params , dtype = float )
161+
162+ # SciPy continuous-distribution fits end with loc and scale.
163+ assert params_array .size >= 2
164+ assert np .all (np .isfinite (params_array ))
165+ assert params_array [- 1 ] > 0
166+
167+ @pytest .mark .unit
168+ def test_generate_rvs_without_distribution_returns_minimum (
169+ self ,
170+ ) -> None :
171+ assert wfcommons .utils .generate_rvs (
172+ None ,
173+ min_value = 10 ,
174+ max_value = 100 ,
175+ ) == 10
42176
43177 @pytest .mark .unit
44178 @pytest .mark .parametrize (
@@ -48,6 +182,11 @@ def test_generate_rvs(self, distribution: Dict, min_value: float, max_value: flo
48182 pytest .param (10 , 2 , 45 ),
49183 pytest .param (10 , 3 , 120 ),
50184 ],
51- )
52- def test_ncr (self , n : int , r : int , combinations : int ) -> None :
53- assert (wfcommons .utils .ncr (n , r ) == combinations )
185+ )
186+ def test_ncr (
187+ self ,
188+ n : int ,
189+ r : int ,
190+ combinations : int ,
191+ ) -> None :
192+ assert wfcommons .utils .ncr (n , r ) == combinations
0 commit comments