You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ah...@apache.org on 2024/08/16 10:29:58 UTC

(commons-statistics) branch master updated: STATISTICS-88: Add a log uniform distribution

This is an automated email from the ASF dual-hosted git repository.

aherbert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-statistics.git


The following commit(s) were added to refs/heads/master by this push:
     new 7c6a763  STATISTICS-88: Add a log uniform distribution
7c6a763 is described below

commit 7c6a7637e569ca3904a82b880cd290c9b662f28c
Author: Alex Herbert <ah...@apache.org>
AuthorDate: Thu Aug 15 18:35:36 2024 +0100

    STATISTICS-88: Add a log uniform distribution
---
 .../distribution/LogUniformDistribution.java       | 239 +++++++++++++++++++++
 .../distribution/LogUniformDistributionTest.java   | 115 ++++++++++
 .../distribution/test.loguniform.1.properties      |  33 +++
 .../distribution/test.loguniform.2.properties      |  32 +++
 .../distribution/test.loguniform.3.properties      |  32 +++
 .../distribution/DistributionsApplication.java     |   1 +
 .../examples/distribution/LogUniformCommand.java   | 156 ++++++++++++++
 src/changes/changes.xml                            |   3 +
 8 files changed, 611 insertions(+)

diff --git a/commons-statistics-distribution/src/main/java/org/apache/commons/statistics/distribution/LogUniformDistribution.java b/commons-statistics-distribution/src/main/java/org/apache/commons/statistics/distribution/LogUniformDistribution.java
new file mode 100644
index 0000000..196b413
--- /dev/null
+++ b/commons-statistics-distribution/src/main/java/org/apache/commons/statistics/distribution/LogUniformDistribution.java
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.distribution;
+
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.sampling.distribution.ContinuousUniformSampler;
+import org.apache.commons.rng.sampling.distribution.SharedStateContinuousSampler;
+
+/**
+ * Implementation of the log-uniform distribution. This is also known as the reciprocal distribution.
+ *
+ * <p>The probability density function of \( X \) is:
+ *
+ * <p>\[ f(x; a, b) = \frac{1}{x \ln \frac b a} \]
+ *
+ * <p>for \( 0 \lt a \lt b \lt \infty \) and
+ * \( x \in [a, b] \).
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/Reciprocal_distribution">Reciprocal distribution (Wikipedia)</a>
+ * @since 1.1
+ */
+public final class LogUniformDistribution extends AbstractContinuousDistribution {
+    /** Lower bound (a) of this distribution (inclusive). */
+    private final double lower;
+    /** Upper bound (b) of this distribution (exclusive). */
+    private final double upper;
+    /** log(a). */
+    private final double logA;
+    /** log(b). */
+    private final double logB;
+    /** log(b) - log(a). */
+    private final double logBmLogA;
+    /** log(log(b) - log(a)). */
+    private final double logLogBmLogA;
+
+    /**
+     * @param lower Lower bound of this distribution (inclusive).
+     * @param upper Upper bound of this distribution (inclusive).
+     */
+    private LogUniformDistribution(double lower,
+                                   double upper) {
+        this.lower = lower;
+        this.upper = upper;
+        logA = Math.log(lower);
+        logB = Math.log(upper);
+        logBmLogA = logB - logA;
+        logLogBmLogA = Math.log(logBmLogA);
+    }
+
+    /**
+     * Creates a log-uniform distribution.
+     *
+     * @param lower Lower bound of this distribution (inclusive).
+     * @param upper Upper bound of this distribution (inclusive).
+     * @return the distribution
+     * @throws IllegalArgumentException if {@code lower >= upper}; the range between the bounds
+     * is not finite; or {@code lower <= 0}
+     */
+    public static LogUniformDistribution of(double lower,
+                                            double upper) {
+        if (lower >= upper) {
+            throw new DistributionException(DistributionException.INVALID_RANGE_LOW_GTE_HIGH,
+                                            lower, upper);
+        }
+        if (!Double.isFinite(upper - lower)) {
+            throw new DistributionException("Range %s is not finite", upper - lower);
+        }
+        if (lower <= 0) {
+            throw new DistributionException(DistributionException.NOT_STRICTLY_POSITIVE, lower);
+        }
+        return new LogUniformDistribution(lower, upper);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double density(double x) {
+        if (x < lower || x > upper) {
+            return 0;
+        }
+        return Math.exp(logDensity(x));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double logDensity(double x) {
+        if (x < lower || x > upper) {
+            return Double.NEGATIVE_INFINITY;
+        }
+        return -Math.log(x) - logLogBmLogA;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double cumulativeProbability(double x)  {
+        if (x <= lower) {
+            return 0;
+        }
+        if (x >= upper) {
+            return 1;
+        }
+        return (Math.log(x) - logA) / logBmLogA;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double survivalProbability(double x) {
+        if (x <= lower) {
+            return 1;
+        }
+        if (x >= upper) {
+            return 0;
+        }
+        return (logB - Math.log(x)) / logBmLogA;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double inverseCumulativeProbability(double p) {
+        ArgumentUtils.checkProbability(p);
+        // Avoid floating-point error at the bounds
+        return clipToRange(Math.exp(logA + p * logBmLogA));
+    }
+
+    @Override
+    public double inverseSurvivalProbability(double p) {
+        ArgumentUtils.checkProbability(p);
+        // Avoid floating-point error at the bounds
+        return clipToRange(Math.exp(logB - p * logBmLogA));
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>For lower bound \( a \) and upper bound \( b \), the mean is:
+     *
+     * <p>\[ \frac{b - a}{\ln \frac b a} \]
+     */
+    @Override
+    public double getMean() {
+        return (upper - lower) / logBmLogA;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>For lower bound \( a \) and upper bound \( b \), the variance is:
+     *
+     * <p>\[ \frac{b^2 - a^2}{2 \ln \frac b a} - \left( \frac{b - a}{\ln \frac b a} \right)^2 \]
+     */
+    @Override
+    public double getVariance() {
+        // Compute u_2 via a stabilising rearrangement:
+        // https://docs.scipy.org/doc/scipy/tutorial/stats/continuous_loguniform.html
+        final double a = lower;
+        final double b = upper;
+        final double d = -logBmLogA;
+        return (a - b) * (a * (d - 2) + b * (d + 2)) / (2 * d * d);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>The lower bound of the support is equal to the lower bound parameter
+     * of the distribution.
+     */
+    @Override
+    public double getSupportLowerBound() {
+        return lower;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>The upper bound of the support is equal to the upper bound parameter
+     * of the distribution.
+     */
+    @Override
+    public double getSupportUpperBound() {
+        return upper;
+    }
+
+    /**
+     * Clip the value to the range [lower, upper].
+     * This is used to handle floating-point error at the support bound.
+     *
+     * @param x Value x
+     * @return x clipped to the range
+     */
+    private double clipToRange(double x) {
+        return clip(x, lower, upper);
+    }
+
+    /**
+     * Clip the value to the range [lower, upper].
+     *
+     * @param x Value x
+     * @param lower Lower bound (inclusive)
+     * @param upper Upper bound (inclusive)
+     * @return x clipped to the range
+     */
+    private static double clip(double x, double lower, double upper) {
+        if (x <= lower) {
+            return lower;
+        }
+        return x < upper ? x : upper;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    double getMedian() {
+        // Overridden for the probability(double, double) method.
+        // This is intentionally not a public method.
+        // sqrt(ab) avoiding overflow
+        return Math.exp(0.5 * (logA + logB));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public ContinuousDistribution.Sampler createSampler(final UniformRandomProvider rng) {
+        // Exponentiate a uniform distribution sampler of the logarithmic range.
+        final SharedStateContinuousSampler s = ContinuousUniformSampler.of(rng, logA, logB);
+        return () -> Math.exp(s.sample());
+    }
+}
diff --git a/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/LogUniformDistributionTest.java b/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/LogUniformDistributionTest.java
new file mode 100644
index 0000000..e4aac72
--- /dev/null
+++ b/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/LogUniformDistributionTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.distribution;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test cases for {@link LogUniformDistribution}.
+ * Extends {@link BaseContinuousDistributionTest}. See javadoc of that class for details.
+ */
+class LogUniformDistributionTest extends BaseContinuousDistributionTest {
+    @Override
+    ContinuousDistribution makeDistribution(Object... parameters) {
+        final double a = (Double) parameters[0];
+        final double b = (Double) parameters[1];
+        return LogUniformDistribution.of(a, b);
+    }
+
+    @Override
+    Object[][] makeInvalidParameters() {
+        return new Object[][] {
+            // lower >= upper
+            {0.0, 0.0},
+            {1.0, 0.5},
+            // Range not finite
+            {Double.NaN, 1.0},
+            {0.5, Double.NaN},
+            // lower <= 0
+            {-1.0, 1.0},
+            {0.0, 1.0},
+        };
+    }
+
+    @Override
+    String[] getParameterNames() {
+        return new String[] {"SupportLowerBound", "SupportUpperBound"};
+    }
+
+    @Override
+    protected double getRelativeTolerance() {
+        return 5e-15;
+    }
+
+    //-------------------- Additional test cases -------------------------------
+
+    /**
+     * Test the moments using the canonical formulas (from the javadoc).
+     */
+    @ParameterizedTest
+    @MethodSource
+    void testAdditionalMoments(double a, double b) {
+        final double diff = b - a;
+        final double denom = Math.log(b / a);
+        final double mean = diff / denom;
+        // Note: b^2 - a^2 = (b-a)*(b+a)
+        final double variance = mean * (b + a) / 2 - mean * mean;
+        TestUtils.assertEquals(mean, LogUniformDistribution.of(a, b).getMean(),
+            DoubleTolerances.relative(1e-14), "Mean");
+        TestUtils.assertEquals(variance, LogUniformDistribution.of(a, b).getVariance(),
+            DoubleTolerances.relative(1e-10), "Variance");
+    }
+
+    static Stream<Arguments> testAdditionalMoments() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        for (final double a : new double[] {1, 10, 100}) {
+            for (final double x : new double[] {10, 20, 40}) {
+                builder.add(Arguments.of(a, a + x));
+            }
+        }
+        return builder.build();
+    }
+
+    /**
+     * Test the survival function for extreme values is computed using high precision,
+     * i.e. is not the default of 1 - CDF.
+     */
+    @ParameterizedTest
+    @CsvSource({
+        "1e-100, 1e100",
+        "1e-10, 1e10",
+    })
+    void testExtremeSurvivalFunction(double a, double b) {
+        final LogUniformDistribution d = LogUniformDistribution.of(a, b);
+        // Find a small non-zero survival probability
+        final double u = Math.ulp(b);
+        int i = 1;
+        double p;
+        do {
+            p = d.survivalProbability(b - i * u);
+            i *= 2;
+        } while (p == 0);
+        Assertions.assertTrue(p < 0x1.0p-53, "sf is not small enough for high precision");
+        Assertions.assertNotEquals(1 - d.cumulativeProbability(b - i * u), p, "sf is not high precision: sf == 1 - cdf");
+    }
+}
diff --git a/commons-statistics-distribution/src/test/resources/org/apache/commons/statistics/distribution/test.loguniform.1.properties b/commons-statistics-distribution/src/test/resources/org/apache/commons/statistics/distribution/test.loguniform.1.properties
new file mode 100644
index 0000000..dbb3e0a
--- /dev/null
+++ b/commons-statistics-distribution/src/test/resources/org/apache/commons/statistics/distribution/test.loguniform.1.properties
@@ -0,0 +1,33 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+parameters = 1.0 10.0
+
+# Computed using scipy.stats reciprocal (v1.13.0)
+mean = 3.9086503371292665
+variance = 6.220029396270247
+lower = 1
+upper = 10
+cdf.points = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
+cdf.values = \
+  0.                 , 0.30102999566398114, 0.47712125471966244, \
+  0.6020599913279623 , 0.6989700043360187 , 0.7781512503836435 , \
+  0.8450980400142567 , 0.9030899869919434 , 0.9542425094393249 , \
+  1.
+pdf.values = \
+  0.43429448190325176 , 0.21714724095162585 , 0.14476482730108392 , \
+  0.10857362047581297 , 0.08685889638065038 , 0.07238241365054196 , \
+  0.062042068843321696, 0.05428681023790648 , 0.04825494243369463 , \
+  0.04342944819032517
diff --git a/commons-statistics-distribution/src/test/resources/org/apache/commons/statistics/distribution/test.loguniform.2.properties b/commons-statistics-distribution/src/test/resources/org/apache/commons/statistics/distribution/test.loguniform.2.properties
new file mode 100644
index 0000000..8f3a069
--- /dev/null
+++ b/commons-statistics-distribution/src/test/resources/org/apache/commons/statistics/distribution/test.loguniform.2.properties
@@ -0,0 +1,32 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+parameters = 1e3 1e10
+
+# Computed using scipy.stats reciprocal (v1.13.0)
+mean = 620420626.3911486
+variance = 2.7171816885144796e+18
+lower = 1e3
+upper = 1e10
+cdf.points = 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10
+cdf.values = \
+  0.                 , 0.14285714285714293, 0.28571428571428575, \
+  0.42857142857142855, 0.5714285714285715 , 0.7142857142857144 , \
+  0.8571428571428571 , 1.
+pdf.values = \
+  6.204206884332176e-05, 6.204206884332170e-06, \
+  6.204206884332163e-07, 6.204206884332178e-08, \
+  6.204206884332171e-09, 6.204206884332165e-10, \
+  6.204206884332179e-11, 6.204206884332173e-12
diff --git a/commons-statistics-distribution/src/test/resources/org/apache/commons/statistics/distribution/test.loguniform.3.properties b/commons-statistics-distribution/src/test/resources/org/apache/commons/statistics/distribution/test.loguniform.3.properties
new file mode 100644
index 0000000..cfef804
--- /dev/null
+++ b/commons-statistics-distribution/src/test/resources/org/apache/commons/statistics/distribution/test.loguniform.3.properties
@@ -0,0 +1,32 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+parameters = 1e-10 1e-3
+
+# Computed using scipy.stats reciprocal (v1.13.0)
+mean = 6.204206263911483e-05
+variance = 2.717181688514468e-08
+lower = 1e-10
+upper = 1e-3
+cdf.points = 1e-10, 1e-9, 1e-8, 1e-7, 1e-6, 1e-5, 1e-4, 1e-3
+cdf.values = \
+  0.                 , 0.14285714285714293, 0.28571428571428564, \
+  0.42857142857142855, 0.5714285714285715 , 0.7142857142857143 , \
+  0.8571428571428572 , 1.
+pdf.values = \
+  6.204206884332178e+08, 6.204206884332171e+07, \
+  6.204206884332187e+06, 6.204206884332179e+05, \
+  6.204206884332172e+04, 6.204206884332166e+03, \
+  6.204206884332165e+02, 6.204206884332169e+01
diff --git a/commons-statistics-examples/examples-distribution/src/main/java/org/apache/commons/statistics/examples/distribution/DistributionsApplication.java b/commons-statistics-examples/examples-distribution/src/main/java/org/apache/commons/statistics/examples/distribution/DistributionsApplication.java
index 0fbfd27..2477049 100644
--- a/commons-statistics-examples/examples-distribution/src/main/java/org/apache/commons/statistics/examples/distribution/DistributionsApplication.java
+++ b/commons-statistics-examples/examples-distribution/src/main/java/org/apache/commons/statistics/examples/distribution/DistributionsApplication.java
@@ -83,6 +83,7 @@ public final class DistributionsApplication {
                  LevyCommand.class,
                  LogisticCommand.class,
                  LogNormalCommand.class,
+                 LogUniformCommand.class,
                  NakagamiCommand.class,
                  NormalCommand.class,
                  ParetoCommand.class,
diff --git a/commons-statistics-examples/examples-distribution/src/main/java/org/apache/commons/statistics/examples/distribution/LogUniformCommand.java b/commons-statistics-examples/examples-distribution/src/main/java/org/apache/commons/statistics/examples/distribution/LogUniformCommand.java
new file mode 100644
index 0000000..8592ddc
--- /dev/null
+++ b/commons-statistics-examples/examples-distribution/src/main/java/org/apache/commons/statistics/examples/distribution/LogUniformCommand.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.examples.distribution;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.statistics.distribution.ContinuousDistribution;
+import org.apache.commons.statistics.distribution.LogUniformDistribution;
+import picocli.CommandLine.ArgGroup;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+
+/**
+ * Command for the {@link LogUniformDistribution}.
+ */
+@Command(name = "logunif",
+         aliases = {"reciprocal"},
+         description = "Log uniform distribution.",
+         subcommands = {
+             LogUniformCommand.Check.class,
+             LogUniformCommand.PDF.class,
+             LogUniformCommand.LPDF.class,
+             LogUniformCommand.CDF.class,
+             LogUniformCommand.SF.class,
+             LogUniformCommand.ICDF.class,
+             LogUniformCommand.ISF.class,
+         })
+class LogUniformCommand extends AbstractDistributionCommand {
+
+    /** Base command for the distribution that defines the parameters. */
+    private abstract static class BaseCommand extends AbstractContinuousDistributionCommand {
+        /** Distribution parameters. */
+        @ArgGroup(validate = false, heading = "Distribution parameters:%n", order = 1)
+        private Params params = new Params();
+
+        /** Parameters class. */
+        static class Params {
+            /** The distribution lower limit. */
+            @Option(names = {"-a", "--lower"},
+                    paramLabel = "a",
+                    arity = "1..*",
+                    split = ",",
+                    description = {"lower bound (default: ${DEFAULT-VALUE})."})
+            private double[] lower = {1, 2};
+
+            /** The distribution upper limit. */
+            @Option(names = {"-b", "--upper"},
+                    paramLabel = "b",
+                    arity = "1..*",
+                    split = ",",
+                    description = {"upper bound (default: ${DEFAULT-VALUE})."})
+            private double[] upper = {4, 6};
+        }
+
+        /** Extend the options to set the default values for this distribution. */
+        static final class Options extends ContinuousDistributionOptions {
+            /** Set defaults. */
+            private Options() {
+                min = -5;
+                max = 5;
+            }
+        }
+
+        @Override
+        protected List<Distribution<ContinuousDistribution>> getDistributions() {
+            double[] lower = params.lower;
+            double[] upper = params.upper;
+            final int n = DistributionUtils.validateLengths(lower.length, upper.length);
+
+            lower = DistributionUtils.expandToLength(lower, n);
+            upper = DistributionUtils.expandToLength(upper, n);
+
+            // Create distributions
+            final ArrayList<Distribution<ContinuousDistribution>> list = new ArrayList<>();
+            for (int i = 0; i < n; i++) {
+                final ContinuousDistribution d = LogUniformDistribution.of(lower[i], upper[i]);
+                list.add(new Distribution<>(d, "a=" + lower[i] + ",b=" + upper[i]));
+            }
+            return list;
+        }
+    }
+
+    /** Base command for the distribution that defines the parameters. */
+    private abstract static class ProbabilityCommand extends BaseCommand {
+        /** The distribution options. */
+        @ArgGroup(validate = false, heading = "Evaluation options:%n", order = 2)
+        private Options distributionOptions = new Options();
+
+        @Override
+        protected DistributionOptions getDistributionOptions() {
+            return distributionOptions;
+        }
+    }
+
+    /** Base command for the distribution that defines the parameters for inverse probability functions. */
+    private abstract static class InverseProbabilityCommand extends BaseCommand {
+        /** The distribution options. */
+        @ArgGroup(validate = false, heading = "Evaluation options:%n", order = 2)
+        private InverseContinuousDistributionOptions distributionOptions = new InverseContinuousDistributionOptions();
+
+        @Override
+        protected DistributionOptions getDistributionOptions() {
+            return distributionOptions;
+        }
+    }
+
+    /** Verification checks command. */
+    @Command(name = "check",
+             hidden = true,
+             description = "Log uniform distribution verification checks.")
+    static class Check extends ProbabilityCommand {}
+
+    /** PDF command. */
+    @Command(name = "pdf",
+             description = "Log uniform distribution PDF.")
+    static class PDF extends ProbabilityCommand {}
+
+    /** LPDF command. */
+    @Command(name = "lpdf",
+             description = "Log uniform distribution natural logarithm of the PDF.")
+    static class LPDF extends ProbabilityCommand {}
+
+    /** CDF command. */
+    @Command(name = "cdf",
+             description = "Log uniform distribution CDF.")
+    static class CDF extends ProbabilityCommand {}
+
+    /** SF command. */
+    @Command(name = "sf",
+             description = "Log uniform distribution survival probability.")
+    static class SF extends ProbabilityCommand {}
+
+    /** ICDF command. */
+    @Command(name = "icdf",
+             description = "Log uniform distribution inverse CDF.")
+    static class ICDF extends InverseProbabilityCommand {}
+
+    /** ISF command. */
+    @Command(name = "isf",
+             description = "Log uniform distribution inverse SF.")
+    static class ISF extends InverseProbabilityCommand {}
+}
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 4b5a1eb..82fffef 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -56,6 +56,9 @@ If the output is not quite correct, check for invisible trailing spaces!
     <release version="1.1" date="TBD" description="
 Adds ranking, inference, descriptive and bom modules. (requires Java 8).
 ">
+      <action dev="aherbert" type="add" issue="STATISTICS-88">
+        "LogUniformDistribution": Add a log uniform distribution.
+      </action>
       <action dev="aherbert" type="add" issue="STATISTICS-87">
         "FoldedNormalDistribution": Add a folded normal distribution implementation with
         specialized support for a half-normal distribution.