Using FSCheck generators
Asked Answered
V

3

5

I have a function to generate doubles in a range:

let gen_doublein = 
    fun mx mn -> Arb.generate<float> |> Gen.suchThat ( (>) mx ) |> Gen.suchThat ( (<) mn )

and then a function to generate an array of 2 of these:

let gen_params:Gen<double array> = 
    gen { let! x = gen_doublein 0.0 20000.0
          let! y = gen_doublein 0.0 2000.0
          return [| x;y|] }

I put:

static member arb_params = Arb.fromGen  gen_params

in the Generator class and register it. All seems OK. To test that this is all OK I have:

let f2 (xs:double array) :double= exp (-2.0*xs.[0]) + xs.[1]*exp (-2.0*xs.[0])
let fcheck fn xs = fn xs > 0.0

then using an array generator 'arrayOfLength':

Check.Quick (Prop.forAll (arrayOfLength 2) (fcheck f2))

works as expected, however:

Check.Quick (Prop.forAll (Generators.arb_params) (fcheck f2))

just starts doing some calculation and never comes back. f# gurus please help.

Ventilation answered 20/3, 2012 at 15:46 Comment(3)
Solved - the minimum and maximum where the wrong way round - so the conditions could not be satisfied. Sorry for wasting your time!Ventilation
Feel free to post this as an answer and mark it. fun mx mn is bound to cause surprises down the line :)Mord
Tip: if you use Gen.suchThat, it's a good idea to experiment with using the implication operator ==> in your property first. Gen.suchThat will keep trying to filter results indefinitely - the ==> operator keeps a count of how many values did not pass the filter and fails when it reaches the threshold. So it's much easier to figure out what's going on.Acaudal
G
4

I did not try this, but I think the problem is that the generator creates float values randomly and then checks whether they match the predicate you specified (the range). This means that it has to generate a large number of floats before it (randomly) generates one that matches.

It would be easier to generate values in a specified range by generating float values in a range [0 .. 1]
and then re-scaling them to match the range you need.

I'm not familiar with FsCheck enough, so I don't know if there is a generator for [0 .. 1] floating-point range, but you could start by generating integers and transforming them to floats:

let gen_doublein mx mn = gen {
   let! n = Arb.generate<int>
   let f = float n / float Int32.MaxValue 
   return mx + (f * (mn - mx)) }

EDIT I see that you solved the problem already. I think the solution I posted might still be relevant for smaller ranges (where the random generator does not produce enough matching values soon enough).

Garlandgarlanda answered 20/3, 2012 at 16:9 Comment(2)
Yes, I'll use this anyway, for the efficiency. I will have more parameters and some of the ranges will be small.Ventilation
Out of curiosity, what is does "!" in "let! n = ..." mean?Projectionist
V
2

The problem was the parameters were the wrong way round. Tomas's suggestion is a good one, and there are some helper functions to implement it.

// Another id function
let fd (d:double) = d
// Check that it is in bounds
let mn=1.0
let mx=5.0
let fdcheck d = (fd d <= mx) && (fd d >= mn)
// Run the check with the numbers generated within the bounds
Check.Quick (Prop.forAll (Arb.fromGen (Gen.map (fun x->
                                                match x with 
                                                | _ when Double.IsNaN x -> (mn+mx)/2.0
                                                | _ when x>  1e+17 ->mx
                                                | _ when x< -1e17 ->mn
                                                | _ -> mn + (mx-mn)*(sin x+1.0)/2.0
                                            ) Arb.generate<double>
                                    )
                       ) fdcheck
         )

Here I have a function which passes the test if the parameter is generated correctly. I'm not sure Tomas's idea with integers works because I think that a lot of small integers are generated and so the doubles don't explore the domain much - but maybe somebody who knows FSCheck might enlighten us.

Ventilation answered 21/3, 2012 at 11:51 Comment(1)
Depends on how you want the distribution to be - would you like it to focus on some particular edge cases more? A uniform distribution seems ideal, but in the case of FsCheck I think it's rarely necessary or even wanted. I usually go for something that empirically covers the input space, and focuses on some edge cases more. I use Prop.collect and friends a lot for tuning generators. With the size mechanism and all the transformations, uniformity is actually very hard to achieve.Acaudal
P
2

Rewritten a sample from @b1g3ar5 this way

let mapRangeNormal (min : float<_>, max : float<_>) x =
    match x with
    | _ when Double.IsNaN x -> (min + max) / 2.0
    | _ when Double.IsPositiveInfinity x -> max
    | _ when Double.IsNegativeInfinity x -> min
    | _ -> min + (max - min) * (sin x + 1.0) / 2.0

let mapRangeUniform (min : float<_>, max : float<_>) x =
    match x with
    | _ when Double.IsNaN x -> (min + max) / 2.0
    | _ when Double.IsPositiveInfinity x -> max
    | _ when Double.IsNegativeInfinity x -> min
    | _ when x < 0.0 ->
           let newRange = max - min
           min - x * (newRange / Double.MaxValue) - newRange / 2.0
    | _ -> let newRange = max - min
           min + x * (newRange / Double.MaxValue) + newRange / 2.0
Provincialism answered 14/9, 2016 at 12:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.