# Choosing the right type (Float32, Float64, etc.)

## Output type

The Julia default for Floats is `Float64`.
For QMC computations that already require a large amount of memory, this is usually not needed.
In fact, like neural network packages, most QMC packages will not let you control the output type.
The underlying C/C++ implementation is made with `Float32` to save memory.

In this package, thanks to Julia's multiple dispatch and generic coding strategy, with a very small coding effort, one can get flexible output type, useful in different situations.
For example, for research/academic purposes, it might be useful to have the exact `Rational` representation of a Faure or Sobol sequence, or `Float16`, `Float64`.

!!! warning
    
    This feature might not be available consistently yet for every QMC sequence or randomization method.

### Example different output types

```@example 3
using QuasiMonteCarlo, Random
using BenchmarkTools, StatsBase
Random.seed!(1234)
m = 4
d = 2
b = QuasiMonteCarlo.nextprime(d)
N = b^m # Number of points
pad = m # Can also choose something as `2m` to get "better" randomization

QuasiMonteCarlo.sample(N, d, FaureSample(R = NoRand())) # Float64
```

```@example 3
QuasiMonteCarlo.sample(N, d, FaureSample(R = NoRand()), Float32) # Float32
```

```@example 3
QuasiMonteCarlo.sample(N, d, FaureSample(R = NoRand()), Rational) # Exact Rational
```

## Intermediate bits array type

For scrambling methods, large bit arrays with values in `0:base` are created.
Again, the default for storing is `Int` (i.e. generally `Int64` as 64-bit Julia is the common installation) which can dramatically affect memory usage when doing multiple large computations.
To avoid this, one can define the `ScrambleMethod` with `Int32` (or else) like `OwenScramble(base = Int32(b), pad = Int32(m))`.[^1]

[^1]: Currently, both `pad` and `base` must be of the same type. This might change.
### Example

To illustrate the memory gain, let's look at a bigger example with multiple randomization using [iterators](@ref DesignMatrices)

```julia
m = 6
d = 3
b = QuasiMonteCarlo.nextprime(d)
N = b^m # Number of points
pad = 32 # Can also choose something as `2m` to get "better" randomization
num_mats = 50

f(x) = prod(x) * 2^length(x) # test function ∫f(x)dᵈx = 1

iterator32 = DesignMatrix(N,
    d,
    FaureSample(R = OwenScramble(base = Int32(b), pad = Int32(pad))),
    num_mats)
iterator64 = DesignMatrix(N,
    d,
    FaureSample(R = OwenScramble(base = Int(b), pad = Int(pad))),
    num_mats)
@btime [mean(f(c) for c in eachcol(X)) for X in iterator32]
# ~21.974 ms (1302 allocations: 2.18 MiB)
@btime [mean(f(c) for c in eachcol(X)) for X in iterator64]
# ~21.532 ms (1302 allocations: 3.01 MiB)
```

The memory usage drops from `3Mib` to `2Mib`! It is not exactly a factor 2 because of all the other computations happening.
The number of allocations is the same, and the timing is similar. In larger cases, a time gain is also observed.

!!! note
    
    For very large computations, one can change both the output type and the intermediate bit array types to minimize memory usage, e.g. `DesignMatrix(N, d, FaureSample(R = OwenScramble(base = Int32(b), pad = Int32(pad))), num_mats, Float32)`
