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
seedor a parentnp.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) ortail='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_seedContLhs.py: continuous LHS builderCatLhs.py: categorical/discrete LHS builderdistribution.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 incore.py.test_distributions.py: validates the implementation of probability distributions.
CHANGELOG.md: description of changes between versionsLICENSE: the MIT license file.README.md: detailed description ofgenLhspackagesetup.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
Distributioninstances. - Construction:
ContLhs(dists, n_samples=None, sample_factor=None, criterion='random')- If
n_samplesis provided, it’s used as‑is. - Else
n_samples = n_vars * sample_factor.
- If
- 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).
- Builds a stratified
CatLhs
- Inputs: list of discrete/categorical
Distributioninstances. - Construction:
CatLhs(dists, n_samples=None, sample_factor=None, criterion='random')- If
n_samplesis 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.
- If
- 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.
- Builds stratified
Lhs (mixed)
- Inputs: a single list with any mix of continuous and discrete
Distributioninstances. - Construction:
Lhs(dists, n_samples=None, sample_factor=None, criterion='random')- If any categorical columns exist,
n_samplesis rounded up to the LCM of their sizes (even whenn_samplesis provided). - Otherwise, behaves like
ContLhsfor sizing.
- If any categorical columns exist,
- 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.
- Splits random streams for continuous vs. categorical parts using
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;ContLhswill 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 infersn = size - 1. - Mismatch between
n+1andsizeemits a warning and truncates/pads accordingly.
- If
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 yoursizecategories 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)
- Poisson / NegativeBinomial (support 0..∞): last bin =
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
rngis provided: samplekfull‑rangeuint64seeds from it and build childGenerators. - Else if
seedis provided: useSeedSequence(seed).spawn(k)to build childGenerators. - 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, wheredis 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 than10d. - Revisiting the
10drule: Later work shows that the “right”ndepends on anticipated roughness and the accuracy criterion (e.g., IMSPE). Use10das 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), whered= number of inputs,L= LCM of category sizes if you have discrete/categorical variables, so each category is represented equally (for mixed designs,Lhswill 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_samplesbeyond10d. 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–5is 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.)
- For screening or quick feasibility:
- Budget trade‑off: With a fixed total budget
N_total, you can either- run fewer replicates with a larger
n_sampleseach (better within‑replicate coverage), or - run more replicates with smaller
n_samples(better across‑replicate stability). In practice, ensure each replicate’sn_samplesstill meets≥ max(10d, L)for meaningful coverage.
- run fewer replicates with a larger
3) Recommended workflow (putting it together)
- Pick an initial
n_samples:n0 = max(10*d, LCM_of_category_sizes). - Choose
n_replicates: start with 3 for screening; increase to 5–10 as needed for stability in summaries (means, quantiles, PRCC/SRC, etc.). - Assess stability:
Compare replicate summaries (e.g., mean/quantiles of key outputs). If across‑replicate variability is large, increase
n_samplesor increasen_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
genLhsrelies on (NumPy arrays, SciPyppf/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: genLhsRun 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, Conover — Technometrics (1979)
-
Stein — Technometrics (1987)
-
Helton & Davis — Sandia Reports (2002–2003)
-
Loeppky, Sacks, Welch — Technometrics (2009)
-
Santner, Williams, Notz — Design 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
34a08cff58f21cbd0f6cfd356af71b62c35a7ccfe1aaf2e111e71d7ea5452725
|
|
| MD5 |
0d0d3004f4fc6982eb64caae76e4aa40
|
|
| BLAKE2b-256 |
193f9b173411dd66feb40565d9c659a93f16a876c407732ac2570cf100ce9d53
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7c6d7578610521c333d90681ddfaaa420effcdda6e4e111677043124ac53146a
|
|
| MD5 |
8426873bed361bbcd4542e60b726844d
|
|
| BLAKE2b-256 |
15e15068642942f55d4ea815e7cf6fbd5379373fa52fbf0b40ad975a484c035a
|