Skip to content

Commit a9c1266

Browse files
authored
Update synth.py (#9)
* Update synth.py * Update synth.py * Update fuzz_synth.py
1 parent 49826c2 commit a9c1266

2 files changed

Lines changed: 220 additions & 102 deletions

File tree

src/synth.py

Lines changed: 134 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,219 @@
11
import numpy as np
2-
from numpy.random import default_rng
32
from math import log
43
from scipy.stats import norm
54

5+
6+
# -------- RNG --------
67
def make_rng(seed=None):
7-
"""Create a reproducible NumPy Generator."""
8+
"""Create a reproducible NumPy Generator (PCG64)."""
89
return np.random.default_rng(seed)
910

11+
12+
# -------- Core Sampler --------
1013
def sample(dist, size, rng=None, **params):
1114
"""
1215
Generic sampler for common distributions.
13-
dist in {'normal','lognormal','pareto','gamma','poisson','binomial',
14-
'negbin','exponential','weibull','mvnormal'}
16+
17+
dist in {
18+
'normal','lognormal','pareto','gamma','poisson','binomial',
19+
'negbin','exponential','weibull','mvnormal'
20+
}
21+
22+
Note:
23+
- Discrete dists ('poisson','binomial','negbin') return integer dtype.
24+
- 'lognormal' supports `from_quantiles=True` with (x1,p1),(x2,p2).
25+
- 'negbin' expects `mean` (mu) and `k` (dispersion > 0) and uses a
26+
Gamma–Poisson mixture so k may be non-integer.
1527
"""
1628
rng = rng or make_rng()
17-
d = dist.lower()
29+
n = int(size)
30+
if n <= 0:
31+
raise ValueError("size must be a positive integer")
32+
d = str(dist).lower()
1833

1934
if d == "normal":
20-
return rng.normal(loc=params["loc"], scale=params["scale"], size=size)
35+
loc = float(params["loc"])
36+
scale = float(params["scale"])
37+
if scale <= 0:
38+
raise ValueError("normal: scale must be > 0")
39+
return rng.normal(loc=loc, scale=scale, size=n)
2140

2241
if d == "lognormal":
2342
if params.get("from_quantiles"):
2443
mu, sigma = _lognormal_mu_sigma_from_quantiles(
2544
x1=params["x1"], p1=params["p1"], x2=params["x2"], p2=params["p2"]
2645
)
2746
else:
28-
mu, sigma = params["meanlog"], params["sdlog"]
29-
return rng.lognormal(mean=mu, sigma=sigma, size=size)
47+
mu = float(params["meanlog"])
48+
sigma = float(params["sdlog"])
49+
if sigma <= 0:
50+
raise ValueError("lognormal: sdlog must be > 0")
51+
return rng.lognormal(mean=mu, sigma=sigma, size=n)
3052

3153
if d == "pareto":
32-
a = params["alpha"]; xm = params.get("xm", 1.0)
33-
return xm * (1.0 + rng.pareto(a, size=size))
54+
a = float(params["alpha"])
55+
if a <= 0:
56+
raise ValueError("pareto: alpha must be > 0")
57+
xm = float(params.get("xm", 1.0))
58+
# NumPy's pareto is Lomax-1; this yields support [xm, inf)
59+
return xm * (1.0 + rng.pareto(a, size=n))
3460

3561
if d == "gamma":
36-
return rng.gamma(shape=params["shape"], scale=params["scale"], size=size)
62+
shape = float(params["shape"])
63+
scale = float(params["scale"])
64+
if shape <= 0 or scale <= 0:
65+
raise ValueError("gamma: shape, scale must be > 0")
66+
return rng.gamma(shape=shape, scale=scale, size=n)
3767

3868
if d == "poisson":
39-
return rng.poisson(lam=params["lam"], size=size)
69+
lam = float(params["lam"])
70+
if lam < 0:
71+
raise ValueError("poisson: lam must be >= 0")
72+
# ensure integer dtype (NumPy already returns ints, make explicit)
73+
return rng.poisson(lam=lam, size=n).astype(np.int64)
4074

4175
if d == "binomial":
42-
return rng.binomial(n=int(params["n"]), p=float(params["p"]), size=size)
76+
trials = int(params["n"])
77+
p = float(params["p"])
78+
if trials < 0 or not (0.0 <= p <= 1.0):
79+
raise ValueError("binomial: n >= 0 and 0 <= p <= 1 required")
80+
return rng.binomial(n=trials, p=p, size=n).astype(np.int64)
4381

4482
if d == "negbin":
45-
mu, k = float(params["mean"]), float(params["k"])
46-
p = k / (k + mu); n = k
47-
return rng.negative_binomial(n=n, p=p, size=size)
83+
# Gamma–Poisson mixture parameterization (robust for non-integer k)
84+
mu = float(params["mean"])
85+
k = float(params["k"])
86+
if mu < 0 or k <= 0:
87+
raise ValueError("negbin: mean >= 0 and k > 0 required")
88+
lam = rng.gamma(shape=k, scale=mu / k, size=n)
89+
return rng.poisson(lam).astype(np.int64)
4890

4991
if d == "exponential":
5092
lam = float(params["lam"])
51-
return rng.exponential(scale=1.0 / lam, size=size)
93+
if lam <= 0:
94+
raise ValueError("exponential: lam must be > 0")
95+
return rng.exponential(scale=1.0 / lam, size=n)
5296

5397
if d == "weibull":
54-
k = float(params["k"]); lam = float(params["lam"])
55-
return lam * rng.weibull(a=k, size=size)
98+
k = float(params["k"])
99+
lam = float(params["lam"])
100+
if k <= 0 or lam <= 0:
101+
raise ValueError("weibull: k, lam must be > 0")
102+
# NumPy uses Weibull with scale=1; multiply by scale (lam)
103+
return lam * rng.weibull(a=k, size=n)
56104

57105
if d == "mvnormal":
58106
mean = np.asarray(params["mean"], dtype=float)
59107
cov = np.asarray(params["cov"], dtype=float)
60-
return rng.multivariate_normal(mean=mean, cov=cov, size=size)
108+
if mean.ndim != 1 or cov.ndim != 2 or cov.shape[0] != cov.shape[1] or cov.shape[0] != mean.size:
109+
raise ValueError("mvnormal: mean (d,), cov (d,d) required")
110+
return rng.multivariate_normal(mean=mean, cov=cov, size=n)
111+
112+
raise ValueError(f"Unsupported distribution: {dist!r}")
61113

62-
raise ValueError("Unsupported distribution: %s" % dist)
63114

115+
# -------- Helpers --------
64116
def categorical(probs, size, rng=None, labels=None):
65117
"""Draw from a categorical distribution (optionally return labels)."""
66118
rng = rng or make_rng()
67-
p = np.asarray(probs, dtype=float); p = p / p.sum()
68-
idx = rng.choice(len(p), size=size, p=p)
119+
p = np.asarray(probs, dtype=float)
120+
if p.ndim != 1 or p.size == 0:
121+
raise ValueError("categorical: probs must be a 1D non-empty array")
122+
s = p.sum()
123+
if not np.isfinite(s) or s <= 0:
124+
raise ValueError("categorical: probs must sum to a positive finite value")
125+
p = p / s
126+
idx = rng.choice(len(p), size=int(size), p=p)
69127
if labels is None:
70128
return idx
71129
labels = np.asarray(labels, dtype=object)
130+
if labels.size != p.size:
131+
raise ValueError("categorical: labels length must match probs")
72132
return labels[idx]
73133

134+
74135
def dirichlet(alpha, size=1, rng=None):
75-
"""Draw Dirichlet vectors (size x K)."""
136+
"""Draw Dirichlet vectors; returns shape (size, K)."""
76137
rng = rng or make_rng()
77-
return rng.dirichlet(alpha, size=size)
138+
a = np.asarray(alpha, dtype=float)
139+
if (a <= 0).any():
140+
raise ValueError("dirichlet: all alpha > 0 required")
141+
return rng.dirichlet(a, size=int(size))
142+
78143

79144
def mixture(components, weights, size, rng=None):
80145
"""
81146
Finite mixture sampler.
82147
83148
components: list of (dist, params) tuples
84-
weights: list of floats (sum to 1)
149+
weights: list/array of floats (sum to 1)
150+
Returns float array of length `size`.
85151
"""
86152
rng = rng or make_rng()
87-
w = np.asarray(weights, float); w = w / w.sum()
88-
comp_idx = rng.choice(len(components), size=size, p=w)
89-
out = np.empty(size, dtype=float)
90-
for i, (dist, params) in enumerate(components):
153+
w = np.asarray(weights, float)
154+
if w.ndim != 1 or w.size != len(components):
155+
raise ValueError("mixture: weights length must match components")
156+
s = w.sum()
157+
if not np.isfinite(s) or s <= 0:
158+
raise ValueError("mixture: weights must sum to a positive finite value")
159+
w = w / s
160+
161+
comp_idx = rng.choice(len(components), size=int(size), p=w)
162+
out = np.empty(int(size), dtype=float)
163+
for i, (dname, dparams) in enumerate(components):
91164
m = np.sum(comp_idx == i)
92165
if m:
93-
out[comp_idx == i] = sample(dist, m, rng=rng, **params)
166+
out[comp_idx == i] = sample(dname, m, rng=rng, **dparams)
167+
# result may mix ints/floats; keep float for generality
94168
return out
95169

170+
96171
def survival_times(dist, size, rng=None, censor_at=None, **params):
97172
"""
98173
Draw time-to-event data with optional right-censoring.
99174
dist in {'exponential','weibull','lognormal'}
100175
Returns (times, events) where events is 1 for observed, 0 for censored.
101176
"""
102177
rng = rng or make_rng()
103-
if dist == "lognormal":
178+
d = str(dist).lower()
179+
if d == "lognormal":
104180
t = sample("lognormal", size, rng=rng, **params)
105-
elif dist == "weibull":
181+
elif d == "weibull":
106182
t = sample("weibull", size, rng=rng, **params)
107-
elif dist == "exponential":
183+
elif d == "exponential":
108184
t = sample("exponential", size, rng=rng, **params)
109185
else:
110186
raise ValueError("survival_times supports 'lognormal','weibull','exponential'")
111187

112-
events = np.ones(size, dtype=int)
188+
events = np.ones(int(size), dtype=int)
113189
if censor_at is not None:
114-
cens = t > censor_at
115-
t[cens] = censor_at
190+
c = float(censor_at)
191+
cens = t > c
192+
t = t.copy()
193+
t[cens] = c
116194
events[cens] = 0
117195
return t, events
118196

197+
198+
# -------- Internal --------
119199
def _lognormal_mu_sigma_from_quantiles(x1, p1, x2, p2):
120200
"""Solve (mu, sigma) of ln(X) ~ N(mu,sigma^2) from two quantiles."""
201+
x1 = float(x1); x2 = float(x2)
202+
p1 = float(p1); p2 = float(p2)
203+
if not (0.0 < p1 < 1.0 and 0.0 < p2 < 1.0):
204+
raise ValueError("lognormal quantiles: p1,p2 must be in (0,1)")
205+
if not (0.0 < x1 < x2):
206+
raise ValueError("lognormal quantiles: require 0 < x1 < x2")
207+
121208
z1 = norm.ppf(p1); z2 = norm.ppf(p2)
122-
if not (0 < x1 < x2):
123-
raise ValueError("Require 0 < x1 < x2 for lognormal quantiles.")
124-
sigma = (log(x2) - log(x1)) / (z2 - z1)
209+
dz = z2 - z1
210+
# New: reject nearly-equal percentiles (ill-conditioned)
211+
if not np.isfinite(z1) or not np.isfinite(z2) or abs(dz) < 1e-3:
212+
raise ValueError("lognormal quantiles: p1 and p2 too close or invalid")
213+
214+
sigma = (log(x2) - log(x1)) / dz
215+
# New: sanity bound on sigma to avoid numerical blowups in RNG
216+
if not np.isfinite(sigma) or sigma <= 0 or sigma > 8.0:
217+
raise ValueError("lognormal quantiles: solved sigma invalid or too large")
125218
mu = log(x1) - sigma * z1
126-
return float(mu), float(sigma)
219+
return float(mu), float(sigma)

0 commit comments

Comments
 (0)