The following proposed solution relies on a specific formula for calculating the standard deviation relative to the bounds: the standard deviation has to be a third of the difference between the mean and the required minimum or maximum.
This first code block is the TruncatedNormalDistribution class, which encapsulates MathNet's Normal class. The main technique for making a truncated normal distribution is in the constructor. Note the resulting workaround that is required in the Sample method:
using MathNet.Numerics.Distributions;
public class TruncatedNormalDistribution {
public TruncatedNormalDistribution(double xMin, double xMax) {
XMin = xMin;
XMax = xMax;
double mean = XMin + (XMax - XMin) / 2; // Halfway between minimum and maximum.
// If the standard deviation is a third of the difference between the mean and
// the required minimum or maximum of a normal distribution, 99.7% of samples should
// be in the required range.
double standardDeviation = (mean - XMin) / 3;
Distribution = new Normal(mean, standardDeviation);
}
private Normal Distribution { get; }
private double XMin { get; }
private double XMax { get; }
public double CumulativeDistribution(double x) {
return Distribution.CumulativeDistribution(x);
}
public double Density(double x) {
return Distribution.Density(x);
}
public double Sample() {
// Constrain results lower than XMin or higher than XMax
// to those bounds.
return Math.Clamp(Distribution.Sample(), XMin, XMax);
}
}
And here is a usage example. For a visual representation of the results, open each of the two output CSV files in a spreadsheet, such as Excel, and map its data to a line chart:
// Put the path of the folder where the CSVs will be saved here
const string chartFolderPath =
@"C:\Insert\chart\folder\path\here";
const double xMin = 0;
const double xMax = 100;
var distribution = new TruncatedNormalDistribution(xMin, xMax);
// Densities
var dictionary = new Dictionary<double, double>();
for (double x = xMin; x <= xMax; x += 1) {
dictionary.Add(x, distribution.Density(x));
}
string csvPath = Path.Combine(
chartFolderPath,
$"Truncated Normal Densities, Range {xMin} to {xMax}.csv");
using var writer = new StreamWriter(csvPath);
foreach ((double key, double value) in dictionary) {
writer.WriteLine($"{key},{value}");
}
// Cumulative Distributions
dictionary.Clear();
for (double x = xMin; x <= xMax; x += 1) {
dictionary.Add(x, distribution.CumulativeDistribution(x));
}
csvPath = Path.Combine(
chartFolderPath,
$"Truncated Normal Cumulative Distributions, Range {xMin} to {xMax}.csv");
using var writer2 = new StreamWriter(csvPath);
foreach ((double key, double value) in dictionary) {
writer2.WriteLine($"{key},{value}");
}