Skip to main content

A package for generating Latin Hypercube Samples.

Project description

genLhs

A lightweight, fast, NumPy‑first toolkit for generating Latin Hypercube Samples (LHS) across continuous, discrete/categorical, and mixed parameter spaces—backed by SciPy distributions for PPF/PMF transforms and designed for reproducibility and clarity.


[[TOC]]


Features

  • Continuous, discrete, and mixed LHS with a consistent, structured‑array interface.
  • Reproducible randomness: accept a scalar seed or a parent np.random.Generator.
  • Discrete/categorical sampling via PMF → CDF → inverse‑categorical mapping (indices are always in range).
  • Tail policy for infinite‑support discrete laws: tail='renormalize' (default) or tail='lump'.
  • Structured array outputs with named fields—easy to save or convert to DataFrames.
  • Unit tests provided in tests/ directory, at the repo root.

Installation

genLhs depends on NumPy and SciPy.

# Create/activate an environment, then:
pip install numpy scipy

# Install this repository in editable mode (run from the directory that contains the genLhs/ folder)
pip install -e .

Repository Layout

  • src/genLhs
    • __init__.py: public types & helpers: split_rngs, lcm_reduce, combine_samples, parse_seed
    • ContLhs.py: continuous LHS builder
    • CatLhs.py: categorical/discrete LHS builder
    • distribution.py: continuous (PPF) & discrete (PMF→CDF) distributions (instance-owned configs)
    • example.py: a simple example showing a mixed continuout and discrete sample generation.
    • Lhs.py: mixed (continuous + categorical) orchestrator
  • test/
    • test_sample_generators.py: tests the functionality of classes and functions in core.py.
    • test_distributions.py: validates the implementation of probability distributions.
  • CHANGELOG.md: description of changes between versions
  • LICENSE: the MIT license file.
  • README.md: detailed description of genLhs package
  • setup.py: is a script used in Python projects for defining package metadata and dependencies required for installing the package using pip. It includes information such as the package name, version, author details, and dependencies.
  • .gitignore: specifies intentionally untracked files and directories to ignore when committing changes.

Quick Start

Continuous LHS (ContLhs)

import numpy as np
from genLhs import ContLhs
from genLhs import distribution as dst

dists = [
    dst.Uniform('uniform', low=0.0, high=1.0),
    dst.Normal('normal', loc=0.0, scale=1.0),
    dst.Triangular('triangular', left=-1.0, mode=0.0, right=2.0),
]

lhs = ContLhs(dists, n_samples=8)
sample = lhs(seed=42)      # or lhs(rng=np.random.default_rng(42))
print(sample[:3])          # structured array, shape (n_samples,)

Categorical LHS (CatLhs)

import numpy as np
from genLhs import CatLhs
from genLhs import distribution as dst

dists = [
    dst.Default_Cat("color", np.array(["red","green","blue"])),
    dst.Poisson("label", np.array(["p1","p2","p3","p4"]), lam=1.2, tail="renormalize"),
]

lhs = CatLhs(dists, n_samples=9)   # CatLhs: no rounding when n_samples provided
sample = lhs(seed=42)
print(sample[:3])

Mixed LHS (Lhs)

import numpy as np
from genLhs import Lhs
from genLhs import distribution as dst

cont = [dst.Uniform('uniform', 0.0, 1.0), dst.Normal('normal', 0.0, 1.0)]

cat = [
    dst.Default_Cat("color", np.array(["red","green","blue"])),
    dst.Poisson("pois", np.array(["p1","p2","p3","p4"]), lam=1.1)
]

lhs = Lhs(cont + cat, sample_factor=10)  # rounded to categorical LCM if needed
sample = lhs(seed=42)
print(sample[:3])

Full Example: User Options and Replicates

from genLhs import get_parser, parse_seed, dst, Lhs

if "__main__" == __name__:
    cont = [dst.Uniform('uniform_param', 0.0, 1.0),
            dst.Normal('normal_param', 0.0, 1.0)]
    cat = [dst.Default_Cat("color", np.array(["red", "green", "blue"])),
           dst.Poisson("pois", np.array(["p1", "p2", "p3", "p4"]), lam=1.1)]
    distributions = cont + cat

    parser = get_parser(add_help=True)
    opts = parser.parse_args()
    seeds = parse_seed(opts.seed)
    lhs = Lhs(distributions, **opts.__dict__)

    replicates = []
    for seed in seeds:
        sample = lhs(seed)
        replicates.append(sample)

    scenario_matrix = np.concatenate(replicates, axis=0)

    print(f"\nSCENARIO MATRIX RESULTS\
            \n=======================\
            \nNUMBER OF SAMPLES     : {scenario_matrix.size}\
            \nNUMBER OF REPLICATES  : {len(seeds)}\
            \nNUMBER OF PARAMETERS  : {len(scenario_matrix.dtype.names)}\
            \nPARAMETERS            : {scenario_matrix.dtype.names}\
            \n\n{scenario_matrix[:3]}\n...")

LHS Builders: Behavioral Details

  • Structured array output: Every sampler returns a 1‑D structured array (n_samples,) with named fields matching names user provided distribution names.
  • Reproducibility: Mixed runs split randomness into independent substreams for continuous vs. categorical parts, keeping results stable under refactors when using a seed.
  • Discrete mapping: Discrete families map uniforms to category indices via PMF→CDF; tail policy controls the representation of extreme mass with finite categories.
  • No shared class state: All distribution instances own their configs; no cross‑instance mutation.

ContLhs

  • Inputs: list of continuous Distribution instances.
  • Construction: ContLhs(dists, n_samples=None, sample_factor=None, criterion='random')
    • If n_samples is provided, it’s used as‑is.
    • Else n_samples = n_vars * sample_factor.
  • Call: __call__(seed=None, rng=None) → structured array (n_samples,)
    • Builds a stratified U ∈ [0,1) LHS with per‑column permutations, then applies each distribution’s PPF.
    • Optional clipping if an instance provides dist.bounds = (lo, hi).

CatLhs

  • Inputs: list of discrete/categorical Distribution instances.
  • Construction: CatLhs(dists, n_samples=None, sample_factor=None, criterion='random')
    • If n_samples is provided, it’s used as‑is (no rounding).
    • Else n_samples = n_cats * sample_factor, then rounded up to the LCM of category counts for equal representation.
  • Call: __call__(seed=None, rng=None) → structured array (n_samples,)
    • Builds stratified U ∈ [0,1) and maps each column to category indices via PMF→CDF.
    • Maps indices to labels with np.take, producing only in‑range category values.

Lhs (mixed)

  • Inputs: a single list with any mix of continuous and discrete Distribution instances.
  • Construction: Lhs(dists, n_samples=None, sample_factor=None, criterion='random')
    • If any categorical columns exist, n_samples is rounded up to the LCM of their sizes (even when n_samples is provided).
    • Otherwise, behaves like ContLhs for sizing.
  • Call: __call__(seed=None, rng=None) → structured array (n_samples,)
    • Splits random streams for continuous vs. categorical parts using split_rngs (seed‑based stable substreams or RNG‑derived substreams).
    • Merges outputs with combine_samples.

Distributions

All distributions are instance‑owned: each instance carries its own distType, optional name, and a kwargs dict containing per‑instance configuration (numpy + ppf or pmf). This avoids shared mutable state and cross‑instance contamination.

Continuous Families (PPF)

Map uniforms u ∈ (0,1) to target marginals via SciPy PPF (inverse CDF):

  • Uniform(low, high)
  • Normal(loc, scale)
  • Lognormal(mean, sigma)
  • Exponential(scale)
  • Gamma(shape, scale)
  • Beta(a, b)
  • ChiSquare(df) / NoncentralChiSquare(df, nonc)
  • FisherF(dfnum, dfden) / NoncentralF(dfnum, dfden, nonc)
  • StudentT(df)
  • Cauchy(loc, scale)
  • Gumbel(loc, scale) / Laplace(loc, scale) / Logistic(loc, scale)
  • Rayleigh(scale) / Wald(mean, scale)
  • WeibullMin(a, scale) / Pareto(a, xm) / Power(a, left, right) / Triangular(left, mode, right)
  • VonMises(mu, kappa)

Optional clipping: attach dist.bounds = (lo, hi) to the instance; ContLhs will clip outputs.

Discrete/Categorical Families (PMF→CDF)

Discrete classes return index arrays (0 ≤ idx < size) by converting stratified uniforms through PMF→CDF→inverse‑categorical mapping:

  • Default_Cat(name, cats)
  • Poisson(name, cats, lam, tail=...)
  • Binomial(name, cats, n=None, p=0.5)
    • If n is None, it infers n = size - 1.
    • Mismatch between n+1 and size emits a warning and truncates/pads accordingly.
  • NegativeBinomial(name, cats, n, p, tail=...)
  • Geometric(name, cats, p, tail=...) (support {1,2,…})
  • Hypergeometric(name, cats, ngood, nbad, nsample) (finite support)
  • LogSeries(name, cats, p, tail=...) (support {1,2,…})
  • Zipf(name, cats, a, tail=...) (support {1,2,…})

Tail Policy for Infinite‑Support Discrete Laws

Applicable to {Poisson, NegativeBinomial, Geometric, LogSeries, Zipf}:

  • tail='renormalize' (default): truncate to your size categories and renormalize.
  • tail='lump': fold all remaining probability mass beyond the last visible category into the last category:
    • Poisson / NegativeBinomial (support 0..∞): last bin = P(K ≥ size-1)
    • Geometric / LogSeries / Zipf (support 1..∞): last bin = P(K ≥ size)

Seeding & RNG Control

All samplers accept either a scalar seed or a parent rng: np.random.Generator:

  • With a seed, the mixed orchestrator (Lhs) uses stable substream splitting so continuous vs. categorical streams remain reproducible even if you add/remove sibling samplers.
  • With a parent Generator, child seeds are sampled from that parent; reproducibility then follows the parent’s state.

split_rngs(seed, rng, k)

  • If rng is provided: sample k full‑range uint64 seeds from it and build child Generators.
  • Else if seed is provided: use SeedSequence(seed).spawn(k) to build child Generators.
  • Else: create a non‑deterministic parent and sample child seeds from it.

Helpers

combine_samples(*arrays)

Merges any number of structured arrays field‑wise into a single structured array with shape (n,).

lcm_reduce(sizes)

Computes the Least Common Multiple across integer sizes. Used to round up n_samples for categorical balance (e.g., in Lhs when categoricals are present).

parse_seed(list_of_strings)

Parses seeds from tokens like:

  • ['0'][0]
  • ['2-4'][2,3,4] (inclusive upper bound)
  • ['1','2','5-7'][1,2,5,6,7]

get_parser(add_help: bool = False)

returns the parser object. this is simply to allow genLhs to run either as a main module or as a submodule.

  • main: get_parser(add_help=True)
  • submodule: get_parser(add_help=False) | get_parser()`

Running the Tests

Comprehensive unittest modules live in tests/ at the repository root (next to the package code).

All tests can be run with:

# from the repository root
python -m unittest -bv

-bv are optional inputs that suppress regular stdout from the scripts being tested (-b); and to all tests run, as well as their results (-v), instead of the just the default summary.


FAQ

Why are categorical n_samples sometimes “rounded up”? CatLhs itself does not round when you pass n_samples, but the mixed orchestrator Lhs will round n_samples up to the LCM of categorical sizes to maintain equal representation when mixing continuous and categorical columns.

What format is the result? A NumPy structured array with one named field per column. Convert to a DataFrame via pd.DataFrame(sample) if desired.

How do I set bounds for continuous outputs? Attach dist.bounds = (lo, hi) to a continuous distribution instance; ContLhs clips after the PPF transform.

How do I pick a tail policy? Pass tail='renormalize' (default) or tail='lump' to infinite‑support discrete families (e.g., Poisson(..., tail='lump')).

Can I pass my own Generator instead of a seed? Yes: __call__(rng=np.random.default_rng(2025)). Mixed Lhs will derive independent child streams from the parent RNG.


Choosing n_samples and n_replicates (seeds)

This section offers practical, theory‑informed guidance for picking a single‑replicate sample size (n_samples) and the number of replicates (n_replicates, i.e., how many --seed values you run) for Latin Hypercube Sampling.

1) How to choose n_samples (single replicate)

Core ideas from the LHS and computer‑experiment literature

  • Variance reduction vs. plain Monte Carlo: LHS reduces estimator variance relative to simple random sampling, especially when the response is additive or near‑additive in its inputs. Asymptotically, the LHS estimator is normal with smaller variance than simple random sampling (SRS). This motivates using LHS even when budgets are tight.
  • Space‑filling designs for emulation: For surrogate modeling (e.g., Gaussian process emulators), a common rule‑of‑thumb for an initial design is n_samples ≈ 10 × d, where d is the number of inputs. This balances coverage and cost and has empirical/theoretical support; it’s a starting point, not a guarantee. If your surface is rough/complex or high‑dimensional, you’ll typically need more than 10d.
  • Revisiting the 10d rule: Later work shows that the “right” n depends on anticipated roughness and the accuracy criterion (e.g., IMSPE). Use 10d as an initial plan and be prepared to scale up for complex responses. Sequential designs and maximum‑projection/sliced LHS can improve efficiency if you expand in stages.
  • Foundational texts: For broader context and good practice around LHS and space‑filling designs in computer experiments, see Santner, Williams & Notz.

Practical guidance

  • Start with: n_samples = max(10*d, L), where
    • d = number of inputs,
    • L = LCM of category sizes if you have discrete/categorical variables, so each category is represented equally (for mixed designs, Lhs will round up to the LCM automatically). This respects both coverage and categorical balance in one shot. (The package already handles LCM rounding for mixed designs.)
  • If your model surface seems rough (non‑additive, interactions, discontinuities), plan to increase n_samples beyond 10d. Use diagnostic plots or emulator fit diagnostics to justify increments.

2) How to choose n_replicates (how many --seeds)

Replicates answer different questions than n_samples: they measure stability of conclusions under the randomization that remains in LHS (column permutations/jitter) and support uncertainty analysis workflows (quantiles, bands, reproducibility checks).

  • Replicating LHS (i.e., running multiple seeds with the same n_samples) is a standard uncertainty propagation and sensitivity practice. Replicates let you quantify spread across runs and test the robustness of rankings/metrics. Helton & Davis explicitly discuss replicated LHS samples to test reproducibility and to support uncertainty/sensitivity summaries.
  • How many replicates?
    • For screening or quick feasibility: n_replicates = 3–5 is common to get a sense of stability.
    • For formal UQ or publication‑grade summaries: 5–10 (or more) depending on how tight you need your across‑replicate confidence intervals to be. (This is analogous to using multiple randomized space‑filling designs to gauge design‑to‑design variability.)
  • Budget trade‑off: With a fixed total budget N_total, you can either
    • run fewer replicates with a larger n_samples each (better within‑replicate coverage), or
    • run more replicates with smaller n_samples (better across‑replicate stability). In practice, ensure each replicate’s n_samples still meets ≥ max(10d, L) for meaningful coverage.

3) Recommended workflow (putting it together)

  1. Pick an initial n_samples: n0 = max(10*d, LCM_of_category_sizes).
  2. Choose n_replicates: start with 3 for screening; increase to 5–10 as needed for stability in summaries (means, quantiles, PRCC/SRC, etc.).
  3. Assess stability: Compare replicate summaries (e.g., mean/quantiles of key outputs). If across‑replicate variability is large, increase n_samples or increase n_replicates, guided by your accuracy needs and compute budget.

Environment Compatibility

There are newer versions of scipy that include latin hypercube generation, but this package-release is intended for stacks common in industrial HPC stacks that often rely on older versions of software.

Intended Versions

The design originally targeted:

  • Python 3.8.8
  • NumPy 1.20.1
  • SciPy 1.6.2

These versions are fully workable on x86_64 platforms (Linux, Windows, macOS‑Intel) where SciPy 1.6.2 wheels exist.

Actually Tested Versions (Today)

On modern Apple Silicon (macOS arm64), the earliest SciPy providing native arm64 wheels for Python 3.8+ is SciPy 1.7.3; earlier versions (e.g., 1.6.2) do not publish arm64 wheels. Therefore, the tested environment is:

  • Python 3.8.13
  • NumPy 1.22.4
  • SciPy 1.7.3 (first macOS arm64 wheel for Py3.8+)

SciPy’s version matrix shows that SciPy 1.7.x supports Python 3.8 and NumPy < 1.23, which includes NumPy 1.22.4.

Why They Differ

  • Platform support moved forward: Apple Silicon wheels for SciPy were added later; 1.6.2 predates that. Hence it can’t be installed natively on macOS arm64 without building from source (Fortran + BLAS), which is non‑trivial on modern macOS.
  • Functionality is unchanged for genLhs: The APIs genLhs relies on (NumPy arrays, SciPy ppf/pmf/cdf) are stable across SciPy 1.6.x → 1.7.x, so behavior is equivalent. The difference is binary wheel availability, not algorithmic changes.
  • If you target x86_64 (Linux/Windows/macOS‑Intel): using Python 3.8.8 + NumPy 1.20.1 + SciPy 1.6.2 remains valid, provided wheels are available on your platform.

Troubleshooting

  • ModuleNotFoundError: genLhs Run from the repository root, or install the package in editable mode:

    python -m pip install -e .
    
  • SciPy installation fails on Apple Silicon with older versions Use SciPy 1.7.3+ which provides macOS arm64 wheels for Python 3.8+; earlier versions (e.g., 1.6.2) lack arm64 wheels.

  • Numerical asserts are too strict If you validate floating values to machine precision, consider slightly looser tolerances (e.g., rtol=1e-10, atol=1e-12) to accommodate minor numerical differences across platforms or BLAS variants.


References

  • McKay, Beckman, ConoverTechnometrics (1979)

  • SteinTechnometrics (1987)

  • Helton & Davis — Sandia Reports (2002–2003)

  • Loeppky, Sacks, WelchTechnometrics (2009)

  • Santner, Williams, NotzDesign and Analysis of Computer Experiments (2003/2018)

  • SciPy 1.7.3 Release Notes — first to provide macOS arm64 wheels for Python 3.8, 3.9, 3.10 (macOS 12+).

  • SciPy Toolchain/Version Support — compatibility matrix for SciPy ↔ Python ↔ NumPy (shows 1.7.x supports Python 3.8 and NumPy <1.23).

  • SciPy Install Guide — general installation help across platforms.


License

MIT Copyright (c) 2026 SpaceDragon Labs

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

genlhs-0.1.1.tar.gz (26.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

genlhs-0.1.1-py3-none-any.whl (23.4 kB view details)

Uploaded Python 3

File details

Details for the file genlhs-0.1.1.tar.gz.

File metadata

  • Download URL: genlhs-0.1.1.tar.gz
  • Upload date:
  • Size: 26.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.5 CPython/3.8.13 Darwin/24.6.0

File hashes

Hashes for genlhs-0.1.1.tar.gz
Algorithm Hash digest
SHA256 34a08cff58f21cbd0f6cfd356af71b62c35a7ccfe1aaf2e111e71d7ea5452725
MD5 0d0d3004f4fc6982eb64caae76e4aa40
BLAKE2b-256 193f9b173411dd66feb40565d9c659a93f16a876c407732ac2570cf100ce9d53

See more details on using hashes here.

File details

Details for the file genlhs-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: genlhs-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 23.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.5 CPython/3.8.13 Darwin/24.6.0

File hashes

Hashes for genlhs-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 7c6d7578610521c333d90681ddfaaa420effcdda6e4e111677043124ac53146a
MD5 8426873bed361bbcd4542e60b726844d
BLAKE2b-256 15e15068642942f55d4ea815e7cf6fbd5379373fa52fbf0b40ad975a484c035a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page