Guide · JDistlib 0.6.0 and later

Build your own probability distribution

Supply a nonnegative formula and its support. JDistlib computes the normalizing constant, then provides density or mass, CDF, quantile, and random-generation operations—with diagnostics for the numerical assumptions that no finite test can prove.

Available from 0.6.0

Beginner path

Your formula does not need to be normalized

Start with a shape you want and the values it is allowed to take. For a continuous distribution, the shape is a nonnegative kernel. For a discrete distribution, it is a nonnegative weight for each outcome. JDistlib finds the missing constant that makes the total probability equal one.

Your first continuous law

The kernel x * (1 - x) is nonnegative between zero and one and zero at both endpoints. The result is a smooth, symmetric distribution on that interval:

NumericalContinuousDistribution first =
    NumericalContinuousDistribution.builder()
        .kernel(x -> x * (1.0 - x))
        .support(0.0, 1.0)
        .diagnosticPreset(DiagnosticPreset.STANDARD)
        .build();

double middleDensity = first.density(0.5, false);
double belowQuarter = first.cumulative(0.25, true, false);
double p90 = first.quantile(0.90, true, false);

Ready for an applied walkthrough? The bounded sensor-error vignette builds, validates, samples, and tests a custom law from start to finish.

Start here

Quick start

The constructor accepts an unnormalized formula. Do not divide by its integral yourself; that is the constructor’s job.

Continuous example: a quartic-exponential law

Choose the kernel \(g(x)=e^{-x^4}\) on the whole real line. Its normalizer and resulting density are

\[ Z=\int_{-\infty}^{\infty} e^{-t^4}\,dt =\frac{\Gamma(1/4)}{2}, \qquad f(x)=\frac{g(x)}{Z} =\frac{2e^{-x^4}}{\Gamma(1/4)}. \]

JDistlib evaluates \(Z\) once during construction.

NumericalContinuousDistribution quartic =
    new NumericalContinuousDistribution(
        x -> Math.exp(-x * x * x * x),
        Double.NEGATIVE_INFINITY,
        Double.POSITIVE_INFINITY
    );

double pdf = quartic.density(0.5, false);
double cdf = quartic.cumulative(0.5, true, false);
double q95 = quartic.quantile(0.95, true, false);
double draw = quartic.random();

Discrete example: increasing weights

Choose \(w(k)=k+1\) on the finite integer support \(\{0,1,2,3,4\}\). Then

\[ Z=\sum_{j=0}^{4}(j+1)=15, \qquad \Pr(X=k)=\frac{w(k)}{Z}=\frac{k+1}{15}, \quad k\in\{0,1,2,3,4\}. \]

The weights are evaluated once, normalized with scaled compensated summation, and retained in a cumulative-mass table.

NumericalDiscreteDistribution countLaw =
    new NumericalDiscreteDistribution(
        k -> k + 1.0,
        0, 4  // inclusive integer support
    );

double p2 = countLaw.density(2.0, false); // 3/15
double pLe2 = countLaw.cumulative(2.0, true, false);
double median = countLaw.quantile(0.5, true, false);
double draw = countLaw.random();

The model

How normalization works

Continuous

For a declared interval \(S=(a,b)\) and kernel \(g:S\to[0,\infty)\),

\[ Z=\int_a^b g(t)\,dt, \qquad f(x)=\frac{g(x)}{Z}\,\mathbf 1_{\{x\in S\}}. \]

A valid probability law requires \(0<Z<\infty\). Bounds may be finite or infinite.

Discrete

For declared outcomes \(S\) and weights \(w:S\to[0,\infty)\),

\[ Z=\sum_{s\in S}w(s), \qquad \Pr(X=x)=\frac{w(x)}{Z}\,\mathbf 1_{\{x\in S\}}. \]

The basic class requires a finite, unique support. Infinite integer support uses the certified truncation API described below.

Multiplying every kernel value by the same positive constant does not change the distribution. This is useful when algebraic rescaling makes a formula safer to evaluate.

JDistlib 0.6.0 workflow

Build, compose, summarize, and evaluate in batches

A complete compilable example accompanies these focused snippets and is checked against the packaged JAR by the release build.

Keep related choices in one builder

The fluent builders gather the formula, support, singularities, numerical controls, diagnostic strength, cache, and sampling strategy. FAST, STANDARD, and THOROUGH are starting points; their resulting options remain fully editable.

Builders analyze ordinary formulas directly and log formulas in log space. A log-kernel may return negative infinity to represent zero mass; NaN and positive infinity are diagnostic errors. Use withoutAnalysis() only when this advisory pass is intentionally disabled.

NumericalContinuousDistribution custom =
    NumericalContinuousDistribution.builder()
        .logKernel(x -> -0.5 * x * x)
        .support(-8.0, 8.0)
        .singularities(0.0)
        .diagnosticPreset(DiagnosticPreset.THOROUGH)
        .adaptiveRejectionSampling(
            x -> -x, -2.0, 0.0, 2.0)
        .build();

NumericalDiscreteDistribution.builder() accepts ordinary or log weights. NumericalPiecewiseDistribution.builder() accepts a NumericalSupport containing interval unions, holes, singularities, and atoms.

Compose existing distribution objects

Mixture, truncation, affine or general monotone transformation, and censoring are available without restating an unnormalized kernel:

\[ f_{\mathrm{mix}}(x)=\sum_i\pi_i f_i(x),\qquad f_{[a,b]}(x)=\frac{f(x)}{F(b)-F(a)},\qquad f_Y(y)=f_X(h^{-1}(y))\left|\frac{dh^{-1}(y)}{dy}\right|. \]
MixtureDistribution mixture = Distributions.mixture(
    new double[] {0.25, 0.75}, first, second);
TruncatedContinuousDistribution positive =
    Distributions.truncate(
        mixture, 0.0, Double.POSITIVE_INFINITY);
MonotoneTransformDistribution shifted =
    Distributions.affine(positive, 10.0, 2.0);
CensoredDistribution observed =
    Distributions.censor(shifted, 10.0, 20.0);

At a censoring bound, density returns atom probability rather than an ordinary continuous density, matching the mixed-support API.

For a complete beginner walkthrough of mixtures, truncation, censoring, affine changes of units, and general monotone transformations, see Compose and transform distributions.

Inspect summaries and the selected sampler

double mode = custom.mode();
ImmutableIntegrationResult variance = custom.centralMoment(2.0);
ImmutableIntegrationResult entropy = custom.entropy();
ProbabilityInterval interval = custom.probabilityInterval(0.95);

System.out.println(custom.getSamplingStrategy());
System.out.println(custom.getSamplingStrategyExplanation());

Finite discrete distributions automatically use a Walker alias table. Adaptive continuous rejection is conditional on the caller's global log-concavity promise; decreasing derivatives and all encountered tangent bounds are checked.

Evaluate without per-call allocation

double[] x = {0.1, 0.5, 0.9};
double[] probabilities = new double[x.length];
custom.cumulativeInto(
    x, 0, probabilities, 0, x.length, true, false);

double[] draws = new double[10_000];
custom.randomInto(draws, 0, draws.length);

Ordinary continuous CDF batches reuse one monotone CDF table. Logged and extreme tails continue through direct integration.

Continuous distributions

Choose support and numerical strategy deliberately

Integration controls

Use IntegrationOptions when the defaults are not appropriate. Declare known discontinuities or interior singularities as breakpoints, set a finite callback budget, and select double-exponential quadrature for difficult endpoints or infinite domains.

IntegrationOptions integration = IntegrationOptions.builder()
    .tolerances(1e-10, 1e-10)
    .subdivisions(400)
    .maxEvaluations(500_000)
    .breakpoints(0.0)
    .method(IntegrationOptions.Method.DOUBLE_EXPONENTIAL)
    .build();

NumericalContinuousDistribution custom =
    new NumericalContinuousDistribution(kernel, lower, upper, integration);

Prefer a log-kernel for extreme scale

If \(g(x)=e^{\ell(x)}\) would overflow or underflow, provide \(\ell(x)=\log g(x)\) directly. Automatic construction searches for multiple modes, scales regions independently, and combines their normalizers with log-sum-exp.

NumericalContinuousDistribution shiftedNormal =
    NumericalContinuousDistribution.fromLogKernel(
        x -> 1000.0 - 0.5 * x * x,
        Double.NEGATIVE_INFINITY,
        Double.POSITIVE_INFINITY
    );

Disconnected support, holes, singularities, and atoms

NumericalSupport and NumericalPiecewiseDistribution represent unions of intervals and mixed laws. At a declared atom, density returns its probability mass; elsewhere it returns the continuous density.

NumericalSupport support = NumericalSupport.builder()
    .interval(-10.0, 10.0)
    .hole(-1.0, 1.0)
    .atom(0.0)
    .singularity(4.0)
    .build();

NumericalPiecewiseDistribution mixed =
    new NumericalPiecewiseDistribution(
        kernel, support, atomWeights, integration
    );

CDF speed versus direct accuracy

cumulative uses direct integration. cumulativeCached opts into an adaptive monotone interpolation table. Central quantiles use that table as an initial estimate and then apply direct corrections; logged and extreme tails remain directly integrated.

Optional rejection sampling

Inverse-CDF sampling is general but can be expensive. On finite support, install a UniformRejectionEnvelope when you know a global upper bound \(L\ge\sup_x\log f(x)\). The bound is a mathematical promise from the caller; JDistlib detects encountered violations but cannot prove the bound globally.

custom.configureUniformRejectionSampling(
    logDensityUpperBound,
    10_000  // maximum proposals per draw
);

Discrete distributions

Finite, irregular, log-scale, and certified infinite support

Irregular finite outcomes

Supply a double[] when outcomes are not consecutive integers. Outcomes are copied, sorted, and required to be finite and unique.

double[] outcomes = {-3.0, -1.0, 2.5, 10.0};
NumericalDiscreteDistribution irregular =
    new NumericalDiscreteDistribution(weight, outcomes);

Log-weights

Use fromLogWeights when ordinary weights cannot be represented safely:

NumericalDiscreteDistribution logScaled =
    NumericalDiscreteDistribution.fromLogWeights(
        k -> 1000.0 + k * Math.log(2.0),
        0, 20
    );

Infinite integer support requires a certificate

CertifiedInfiniteDiscreteDistribution truncates only when a caller-supplied DiscreteTailBound proves the remaining unnormalized mass is small enough. For geometric weights \(w(k)=r^k\), \(0<r<1\), the exact remainder from the first omitted integer \(m\) is

\[ \sum_{k=m}^{\infty}r^k=\frac{r^m}{1-r}. \]
double r = 0.7;
CertifiedInfiniteDiscreteDistribution geometric =
    CertifiedInfiniteDiscreteDistribution.rightInfinite(
        k -> Math.pow(r, k),
        0,
        DiscreteTailBounds.geometricRatio(r),
        CertifiedDiscreteOptions.defaults()
    );

Before and after construction

Use diagnostics as evidence, not proof

Analyze the kernel before constructing

ProbabilityFunctionAnalyzer combines a transformed deterministic grid with reproducible randomized probes. It looks for negative or non-finite values, callback exceptions, non-repeatability, sharp changes, oscillation, large dynamic range, suspicious tails, and unstable normalization.

FunctionAnalysisOptions checks = FunctionAnalysisOptions.builder()
    .sampleCount(257)
    .randomizedProbeBudget(512)
    .adaptiveProbeRounds(6)
    .randomSeed(42L)
    .constructionPolicy(ConstructionPolicy.WARNING)
    .integrationOptions(integration)
    .build();

NumericalDistributionBuildResult candidate =
    NumericalContinuousDistribution.analyze(
        kernel, lower, upper, checks
    );

for (DiagnosticFinding finding :
        candidate.getAnalysis().getFindings()) {
    System.out.println(finding);
}

if (!candidate.canBuild()) {
    throw candidate.getFailure();
}
NumericalContinuousDistribution distribution = candidate.build();

Bound and profile expensive callbacks

Every hardened integration result includes a callback cost profile. For benchmark or service workloads, set total and per-call wall-clock limits. Opt-in daemon isolation lets the integrating thread return even when one callback blocks:

IntegrationOptions bounded = IntegrationOptions.builder()
    .maxEvaluations(250_000)
    .maxCallbackTime(250, TimeUnit.MILLISECONDS)
    .maxTotalTime(5, TimeUnit.SECONDS)
    .callbackExecution(
        IntegrationOptions.CallbackExecution.ISOLATED_DAEMON)
    .build();

Send diagnostics to logs or services

Reports expose versioned, dependency-free JSON. Non-finite numeric measurements are represented as null, because JSON has no NaN or infinity number literals.

String json = candidate.getAnalysis().toJson();
ImmutableIntegrationResult normalization =
    Integrate.integrateImmutable(kernel, lower, upper, bounded);
String integrationJson = normalization.toJson();

STRICT

Warnings or errors block construction. Useful for controlled production pipelines.

WARNING

Errors block construction; warnings remain visible. This is the default.

PERMISSIVE

Advisory findings do not block an attempt. Hard kernel and integration failures still do.

Analyze the constructed distribution

analyzeDistribution checks normalization, CDF endpoints and tails, quantile monotonicity and round trips, and absolute moments. Select moment orders and a left/right split explicitly:

MomentAnalysisOptions moments = MomentAnalysisOptions.builder()
    .orders(0.5, 1.0, 2.0, 4.0)
    .splitPoint(0.0)
    .build();

DistributionAnalysis report =
    distribution.analyzeDistribution(moments);

AbsoluteMomentAnalysis fourth =
    report.getAbsoluteMoment(4.0);
System.out.println(fourth.getLeftValue());
System.out.println(fourth.getRightValue());
System.out.println(fourth.isStable());

The calculation estimates

\[ \mathbb E[|X|^p] =\int_{-\infty}^{c}|x|^p f(x)\,dx +\int_c^{\infty}|x|^p f(x)\,dx, \]

and assesses both sides independently. This prevents symmetric cancellation from making a nonexistent signed moment appear convergent.

When the formula fights back

Diagnostics and troubleshooting

SymptomLikely causeWhat to try
Normalization fails or changes when tolerances tightenDivergent tail, missed singularity, cancellation, or insufficient budgetNarrow the support; declare breakpoints; use DOUBLE_EXPONENTIAL; inspect both tails; increase the evaluation budget only after fixing structure.
NEGATIVE or NON_FINITE findingFormula error, invalid parameter region, overflow, or an undefined endpointFix the mathematical domain; use a log-kernel; never clip a genuinely negative kernel to zero merely to pass construction.
DYNAMIC_RANGE warningOrdinary-scale values span too many orders of magnitudeUse fromLogKernel or fromLogWeights and express products as sums of logarithms.
SHARP_CHANGE or unstable CDFDiscontinuity, narrow mode, interior singularity, or disconnected supportAdd exact breakpoints; increase randomized probes; use NumericalSupport for holes and separate intervals.
Moment stable on one side but not the otherA one-sided heavy tail lacks the requested momentTreat the moment as nonexistent unless mathematical analysis supplies stronger evidence; do not rely on cancellation.
Quantile or random generation is slowEach inversion requires repeated CDF integrationsReuse the CDF table; for finite support, provide a certified rejection envelope with a reasonably tight bound.
Rejection envelope violationThe supplied \(M\) or log-density upper bound is too smallDerive a true global bound, usually at a proven mode; add a conservative numerical margin.
Callback never returnsUser code blocks or loops inside one evaluationFix or isolate the callback. Evaluation budgets and cooperative cancellation are checked only between callback invocations.

Reference

Go deeper

Browse the JavaDoc for every overload, the distribution catalog for built-in laws, and the beginner vignette for a complete applied example. Read the numerical design notes for implementation limitations and accuracy guidance.