47
47
from pytensor .graph .fg import FunctionGraph
48
48
from pytensor .scan import scan
49
49
50
+ from pymc .distributions .continuous import Cauchy
50
51
from pymc .distributions .transforms import _default_transform , log , logodds
51
52
from pymc .logprob .abstract import MeasurableVariable , _logprob
52
53
from pymc .logprob .basic import conditional_logp , icdf , logcdf , logp
@@ -764,14 +765,24 @@ def test_exp_transform_rv():
764
765
y_rv .name = "y"
765
766
766
767
y_vv = y_rv .clone ()
767
- logprob = logp (y_rv , y_vv )
768
- logp_fn = pytensor .function ([y_vv ], logprob )
768
+ logp_fn = pytensor .function ([y_vv ], logp (y_rv , y_vv ))
769
+ logcdf_fn = pytensor .function ([y_vv ], logcdf (y_rv , y_vv ))
770
+ icdf_fn = pytensor .function ([y_vv ], icdf (y_rv , y_vv ))
769
771
770
772
y_val = [- 2.0 , 0.1 , 0.3 ]
773
+ q_val = [0.2 , 0.5 , 0.9 ]
771
774
np .testing .assert_allclose (
772
775
logp_fn (y_val ),
773
776
sp .stats .lognorm (s = 1 ).logpdf (y_val ),
774
777
)
778
+ np .testing .assert_almost_equal (
779
+ logcdf_fn (y_val ),
780
+ sp .stats .lognorm (s = 1 ).logcdf (y_val ),
781
+ )
782
+ np .testing .assert_almost_equal (
783
+ icdf_fn (q_val ),
784
+ sp .stats .lognorm (s = 1 ).ppf (q_val ),
785
+ )
775
786
776
787
777
788
def test_log_transform_rv ():
@@ -811,14 +822,24 @@ def test_loc_transform_rv(self, rv_size, loc_type, addition):
811
822
logprob = logp (y_rv , y_vv )
812
823
assert_no_rvs (logprob )
813
824
logp_fn = pytensor .function ([loc , y_vv ], logprob )
825
+ logcdf_fn = pytensor .function ([loc , y_vv ], logcdf (y_rv , y_vv ))
826
+ icdf_fn = pytensor .function ([loc , y_vv ], icdf (y_rv , y_vv ))
814
827
815
828
loc_test_val = np .full (rv_size , 4.0 )
816
829
y_test_val = np .full (rv_size , 1.0 )
817
-
830
+ q_test_val = np . full ( rv_size , 0.7 )
818
831
np .testing .assert_allclose (
819
832
logp_fn (loc_test_val , y_test_val ),
820
833
sp .stats .norm (loc_test_val , 1 ).logpdf (y_test_val ),
821
834
)
835
+ np .testing .assert_allclose (
836
+ logcdf_fn (loc_test_val , y_test_val ),
837
+ sp .stats .norm (loc_test_val , 1 ).logcdf (y_test_val ),
838
+ )
839
+ np .testing .assert_allclose (
840
+ icdf_fn (loc_test_val , q_test_val ),
841
+ sp .stats .norm (loc_test_val , 1 ).ppf (q_test_val ),
842
+ )
822
843
823
844
@pytest .mark .parametrize (
824
845
"rv_size, scale_type, product" ,
@@ -840,23 +861,37 @@ def test_scale_transform_rv(self, rv_size, scale_type, product):
840
861
logprob = logp (y_rv , y_vv )
841
862
assert_no_rvs (logprob )
842
863
logp_fn = pytensor .function ([scale , y_vv ], logprob )
864
+ logcdf_fn = pytensor .function ([scale , y_vv ], logcdf (y_rv , y_vv ))
865
+ icdf_fn = pytensor .function ([scale , y_vv ], icdf (y_rv , y_vv ))
843
866
844
867
scale_test_val = np .full (rv_size , 4.0 )
845
868
y_test_val = np .full (rv_size , 1.0 )
846
-
869
+ q_test_val = np . full ( rv_size , 0.3 )
847
870
np .testing .assert_allclose (
848
871
logp_fn (scale_test_val , y_test_val ),
849
872
sp .stats .norm (0 , scale_test_val ).logpdf (y_test_val ),
850
873
)
874
+ np .testing .assert_allclose (
875
+ logcdf_fn (scale_test_val , y_test_val ),
876
+ sp .stats .norm (0 , scale_test_val ).logcdf (y_test_val ),
877
+ )
878
+ np .testing .assert_allclose (
879
+ icdf_fn (scale_test_val , q_test_val ),
880
+ sp .stats .norm (0 , scale_test_val ).ppf (q_test_val ),
881
+ )
851
882
852
883
def test_negated_rv_transform (self ):
853
884
x_rv = - pt .random .halfnormal ()
854
885
x_rv .name = "x"
855
886
856
887
x_vv = x_rv .clone ()
857
- x_logp_fn = pytensor .function ([x_vv ], pt .sum (logp (x_rv , x_vv )))
888
+ x_logp_fn = pytensor .function ([x_vv ], logp (x_rv , x_vv ))
889
+ x_logcdf_fn = pytensor .function ([x_vv ], logcdf (x_rv , x_vv ))
890
+ x_icdf_fn = pytensor .function ([x_vv ], icdf (x_rv , x_vv ))
858
891
859
892
np .testing .assert_allclose (x_logp_fn (- 1.5 ), sp .stats .halfnorm .logpdf (1.5 ))
893
+ np .testing .assert_allclose (x_logcdf_fn (- 1.5 ), sp .stats .halfnorm .logsf (1.5 ))
894
+ np .testing .assert_allclose (x_icdf_fn (0.3 ), - sp .stats .halfnorm .ppf (1 - 0.3 ))
860
895
861
896
def test_subtracted_rv_transform (self ):
862
897
# Choose base RV that is asymmetric around zero
@@ -899,25 +934,55 @@ def test_reciprocal_rv_transform(self, numerator):
899
934
900
935
x_vv = x_rv .clone ()
901
936
x_logp_fn = pytensor .function ([x_vv ], logp (x_rv , x_vv ))
937
+ x_logcdf_fn = pytensor .function ([x_vv ], logcdf (x_rv , x_vv ))
938
+
939
+ with pytest .raises (NotImplementedError ):
940
+ icdf (x_rv , x_vv )
902
941
903
942
x_test_val = np .r_ [- 0.5 , 1.5 ]
904
943
np .testing .assert_allclose (
905
944
x_logp_fn (x_test_val ),
906
945
sp .stats .invgamma (shape , scale = scale * numerator ).logpdf (x_test_val ),
907
946
)
947
+ np .testing .assert_allclose (
948
+ x_logcdf_fn (x_test_val ),
949
+ sp .stats .invgamma (shape , scale = scale * numerator ).logcdf (x_test_val ),
950
+ )
951
+
952
+ def test_reciprocal_real_rv_transform (self ):
953
+ # 1 / Cauchy(mu, sigma) = Cauchy(mu / (mu^2 + sigma ^2), sigma / (mu ^ 2, sigma ^ 2))
954
+ test_value = [- 0.5 , 0.9 ]
955
+ test_rv = Cauchy .dist (1 , 2 , size = (2 ,)) ** (- 1 )
956
+
957
+ np .testing .assert_allclose (
958
+ logp (test_rv , test_value ).eval (),
959
+ sp .stats .cauchy (1 / 5 , 2 / 5 ).logpdf (test_value ),
960
+ )
961
+ np .testing .assert_allclose (
962
+ logcdf (test_rv , test_value ).eval (),
963
+ sp .stats .cauchy (1 / 5 , 2 / 5 ).logcdf (test_value ),
964
+ )
965
+ with pytest .raises (NotImplementedError ):
966
+ icdf (test_rv , test_value )
908
967
909
968
def test_sqr_transform (self ):
910
- # The square of a unit normal is a chi-square with 1 df
911
- x_rv = pt .random .normal (0 , 1 , size = (4 ,)) ** 2
969
+ # The square of a normal with unit variance is a noncentral chi-square with 1 df and nc = mean ** 2
970
+ x_rv = pt .random .normal (0.5 , 1 , size = (4 ,)) ** 2
912
971
x_rv .name = "x"
913
972
914
973
x_vv = x_rv .clone ()
915
974
x_logp_fn = pytensor .function ([x_vv ], logp (x_rv , x_vv ))
916
975
976
+ with pytest .raises (NotImplementedError ):
977
+ logcdf (x_rv , x_vv )
978
+
979
+ with pytest .raises (NotImplementedError ):
980
+ icdf (x_rv , x_vv )
981
+
917
982
x_test_val = np .r_ [- 0.5 , 0.5 , 1 , 2.5 ]
918
983
np .testing .assert_allclose (
919
984
x_logp_fn (x_test_val ),
920
- sp .stats .chi2 (df = 1 ).logpdf (x_test_val ),
985
+ sp .stats .ncx2 (df = 1 , nc = 0.5 ** 2 ).logpdf (x_test_val ),
921
986
)
922
987
923
988
def test_sqrt_transform (self ):
@@ -927,12 +992,29 @@ def test_sqrt_transform(self):
927
992
928
993
x_vv = x_rv .clone ()
929
994
x_logp_fn = pytensor .function ([x_vv ], logp (x_rv , x_vv ))
995
+ x_logcdf_fn = pytensor .function ([x_vv ], logcdf (x_rv , x_vv ))
930
996
931
997
x_test_val = np .r_ [- 2.5 , 0.5 , 1 , 2.5 ]
932
998
np .testing .assert_allclose (
933
999
x_logp_fn (x_test_val ),
934
1000
sp .stats .chi (df = 3 ).logpdf (x_test_val ),
935
1001
)
1002
+ np .testing .assert_allclose (
1003
+ x_logcdf_fn (x_test_val ),
1004
+ sp .stats .chi (df = 3 ).logcdf (x_test_val ),
1005
+ )
1006
+
1007
+ # ICDF is not implemented for chisquare, so we have to test with another identity
1008
+ # sqrt(exponential(lam)) = rayleigh(1 / sqrt(2 * lam))
1009
+ lam = 2.5
1010
+ y_rv = pt .sqrt (pt .random .exponential (scale = 1 / lam ))
1011
+ y_vv = x_rv .clone ()
1012
+ y_icdf_fn = pytensor .function ([y_vv ], icdf (y_rv , y_vv ))
1013
+ q_test_val = np .r_ [0.2 , 0.5 , 0.7 , 0.9 ]
1014
+ np .testing .assert_allclose (
1015
+ y_icdf_fn (q_test_val ),
1016
+ (1 / np .sqrt (2 * lam )) * np .sqrt (- 2 * np .log (1 - q_test_val )),
1017
+ )
936
1018
937
1019
@pytest .mark .parametrize ("power" , (- 3 , - 1 , 1 , 5 , 7 ))
938
1020
def test_negative_value_odd_power_transform (self , power ):
@@ -947,7 +1029,7 @@ def test_negative_value_odd_power_transform(self, power):
947
1029
assert np .isfinite (x_logp_fn (- 1 ))
948
1030
949
1031
@pytest .mark .parametrize ("power" , (- 2 , 2 , 4 , 6 , 8 ))
950
- def test_negative_value_even_power_transform (self , power ):
1032
+ def test_negative_value_even_power_transform_logp (self , power ):
951
1033
# check that negative values and odd powers evaluate to -inf logp
952
1034
x_rv = pt .random .normal () ** power
953
1035
x_rv .name = "x"
@@ -959,7 +1041,7 @@ def test_negative_value_even_power_transform(self, power):
959
1041
assert np .isneginf (x_logp_fn (- 1 ))
960
1042
961
1043
@pytest .mark .parametrize ("power" , (- 1 / 3 , - 1 / 2 , 1 / 2 , 1 / 3 ))
962
- def test_negative_value_frac_power_transform (self , power ):
1044
+ def test_negative_value_frac_power_transform_logp (self , power ):
963
1045
# check that negative values and fractional powers evaluate to -inf logp
964
1046
x_rv = pt .random .normal () ** power
965
1047
x_rv .name = "x"
@@ -979,8 +1061,12 @@ def test_absolute_rv_transform(test_val):
979
1061
x_vv = x_rv .clone ()
980
1062
y_vv = y_rv .clone ()
981
1063
x_logp_fn = pytensor .function ([x_vv ], logp (x_rv , x_vv ))
982
- y_logp_fn = pytensor .function ([y_vv ], logp (y_rv , y_vv ))
1064
+ with pytest .raises (NotImplementedError ):
1065
+ logcdf (x_rv , x_vv )
1066
+ with pytest .raises (NotImplementedError ):
1067
+ icdf (x_rv , x_vv )
983
1068
1069
+ y_logp_fn = pytensor .function ([y_vv ], logp (y_rv , y_vv ))
984
1070
np .testing .assert_allclose (x_logp_fn (test_val ), y_logp_fn (test_val ))
985
1071
986
1072
@@ -1022,6 +1108,10 @@ def test_cosh_rv_transform():
1022
1108
1023
1109
vv = rv .clone ()
1024
1110
rv_logp = logp (rv , vv )
1111
+ with pytest .raises (NotImplementedError ):
1112
+ logcdf (rv , vv )
1113
+ with pytest .raises (NotImplementedError ):
1114
+ icdf (rv , vv )
1025
1115
1026
1116
transform = CoshTransform ()
1027
1117
[back_neg , back_pos ] = transform .backward (vv )
@@ -1083,37 +1173,3 @@ def test_invalid_broadcasted_transform_rv_fails():
1083
1173
# This logp derivation should fail or count only once the values that are broadcasted
1084
1174
logprob = logp (y_rv , y_vv )
1085
1175
assert logprob .eval ({y_vv : [0 , 0 , 0 , 0 ], loc : [0 , 0 , 0 , 0 ]}).shape == ()
1086
-
1087
-
1088
- def test_logcdf_measurable_transform ():
1089
- x = pt .exp (pt .random .uniform (0 , 1 ))
1090
- value = x .type ()
1091
- logcdf_fn = pytensor .function ([value ], logcdf (x , value ))
1092
-
1093
- assert logcdf_fn (0 ) == - np .inf
1094
- np .testing .assert_allclose (logcdf_fn (np .exp (0.5 )), np .log (0.5 ))
1095
- np .testing .assert_allclose (logcdf_fn (5 ), 0 )
1096
-
1097
-
1098
- def test_logcdf_measurable_non_injective_fails ():
1099
- x = pt .abs (pt .random .uniform (0 , 1 ))
1100
- value = x .type ()
1101
- with pytest .raises (NotImplementedError ):
1102
- logcdf (x , value )
1103
-
1104
-
1105
- def test_icdf_measurable_transform ():
1106
- x = pt .exp (pt .random .uniform (0 , 1 ))
1107
- value = x .type ()
1108
- icdf_fn = pytensor .function ([value ], icdf (x , value ))
1109
-
1110
- np .testing .assert_allclose (icdf_fn (1e-16 ), 1 )
1111
- np .testing .assert_allclose (icdf_fn (0.5 ), np .exp (0.5 ))
1112
- np .testing .assert_allclose (icdf_fn (1 - 1e-16 ), np .e )
1113
-
1114
-
1115
- def test_icdf_measurable_non_injective_fails ():
1116
- x = pt .abs (pt .random .uniform (0 , 1 ))
1117
- value = x .type ()
1118
- with pytest .raises (NotImplementedError ):
1119
- icdf (x , value )
0 commit comments