From 4af5572ce74e4eb37408ed13b4374ea2e056c305 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Wed, 26 Aug 2026 06:34:31 +0400 Subject: [PATCH 1/5] Add docstrings to randomstart.py --- src/lemke/randomstart.py | 108 +++++++++++++++++++++++++++------------ 1 file changed, 74 insertions(+), 34 deletions(-) diff --git a/src/lemke/randomstart.py b/src/lemke/randomstart.py index c7aca63..9445886 100644 --- a/src/lemke/randomstart.py +++ b/src/lemke/randomstart.py @@ -7,8 +7,8 @@ MAX_ACCURACY = 10_000_000 -# give random n-tuple uniformly from unit simplex def randInSimplex(n, naive=False): + """Generate a random n-tuple uniformly distributed on the unit simplex.""" x = [0.0] * n if naive: # random numbers re-normalized sum = 0 @@ -30,9 +30,11 @@ def randInSimplex(n, naive=False): return x -# round an array of probabilities to fractions with -# denominator def roundArray(x, accuracy=10000): + """ + Round each entry of an array of probabilities `x` + to the nearest multiple of 1 / `accuracy`. + """ if not 1 <= accuracy <= MAX_ACCURACY: raise ValueError(f"accuracy must be between 1 and {MAX_ACCURACY}") @@ -57,22 +59,83 @@ def roundArray(x, accuracy=10000): return [fractions.Fraction(k, accuracy) for k in numerator] -# renormalize list x to sum to one def renormalize(x): + """Rescale a list of numbers so that it sums to one.""" s = sum(x) if s == 0: return x return [k / s for k in x] -# map triple of unit triangle to pair in 2D -# with corners [0,0] [1,0] [0.5,sqrt(3)/2] def maptotriangle(vec): + """Map a point on the unit triangle (3D) to 2D coordinates + in a triangle with corners at (0, 0), (1, 0), (0.5, sqrt(3)/2). + """ x = vec[1] + 0.5 * vec[2] y = 3 ** .5 / 2 * vec[2] return x, y +def plot_simplex(numpoints=200, accuracy=20, higherdim=3, naiveplot=False): + """Generate a simplex sampling plot. + + Samples `numpoints` random points from the simplex of dimension `higherdim` + and projects them onto a 2D triangle (if `higherdim` is greater than 3, + only the middle 3 components of each point are used, renormalized to sum to 1). + Plots the raw sampled points in green and their rounded approximations in red. + + Parameters + ---------- + numpoints : int + Number of points to plot. Default is 200. + accuracy : int + Denominator x; each coordinate is rounded to the nearest multiple of 1/x. + Default is 20. Must be between 1 and 10,000,000. + higherdim : int + Dimension from which the middle 3 components will be sampled. + Default is 3. Must be between 3 and 10. + naiveplot : bool + Sample naively by normalizing random uniforms (biased toward center). + Default is False. + + Raises + ------ + ValueError + If `accuracy` or `higherdim` is out of range. + """ + if not 3 <= higherdim <= 10: + raise ValueError("higherdim must be between 3 and 10") + print( + f"numpoints={numpoints} accuracy={accuracy} higherdim={higherdim} naiveplot={naiveplot}" + ) + if higherdim > 3: + segmentstart = (higherdim - 2) // 2 + print("show positions", segmentstart, "..", + segmentstart + 2, "of 0 ..", higherdim - 1) + fig1, ax = plt.subplots() + ax.set_box_aspect(.866) + # plt.axis('square') + x1, y1 = maptotriangle([1, 0, 0]) + x2, y2 = maptotriangle([0, 1, 0]) + x3, y3 = maptotriangle([0, 0, 1]) + plt.plot([x1, x2, x3, x1], [y1, y2, y3, y1], "black") + + roundedpoints = [] + for _ in range(numpoints): + point = randInSimplex(higherdim, naiveplot) + if higherdim > 3: + segmentstart = (higherdim - 2) // 2 + point = renormalize(point[segmentstart:segmentstart + 3]) + roundedpoints.append(roundArray(point, accuracy)) + x, y = maptotriangle(point) + plt.plot([x], [y], "g.") + for circ in roundedpoints: + x, y = maptotriangle(circ) + plt.scatter([x], [y], s=10000 // accuracy, facecolors="none", + edgecolors="r") + plt.show() + + @click.command( context_settings={"help_option_names": ["-?", "-h", "--help"]}, ) @@ -104,35 +167,12 @@ def maptotriangle(vec): help="Sample naively by normalizing random uniforms (biased toward center)", ) def main(numpoints, accuracy, higherdim, naiveplot): - print( - f"numpoints={numpoints} accuracy={accuracy} higherdim={higherdim} naiveplot={naiveplot}" - ) - if higherdim > 3: - segmentstart = (higherdim - 2) // 2 - print("show positions", segmentstart, "..", - segmentstart + 2, "of 0 ..", higherdim - 1) - fig1, ax = plt.subplots() - ax.set_box_aspect(.866) - # plt.axis('square') - x1, y1 = maptotriangle([1, 0, 0]) - x2, y2 = maptotriangle([0, 1, 0]) - x3, y3 = maptotriangle([0, 0, 1]) - plt.plot([x1, x2, x3, x1], [y1, y2, y3, y1], "black") + """Plot random points on the 2-simplex, rounded to rational coordinates. - roundedpoints = [] - for _ in range(numpoints): - point = randInSimplex(higherdim, naiveplot) - if higherdim > 3: - segmentstart = (higherdim - 2) // 2 - point = renormalize(point[segmentstart:segmentstart + 3]) - roundedpoints.append(roundArray(point, accuracy)) - x, y = maptotriangle(point) - plt.plot([x], [y], "g.") - for circ in roundedpoints: - x, y = maptotriangle(circ) - plt.scatter([x], [y], s=10000 // accuracy, facecolors="none", - edgecolors="r") - plt.show() + Green dots are the raw sampled points; + red circles are their rounded rational approximations. + """ + plot_simplex(numpoints, accuracy, higherdim, naiveplot) if __name__ == "__main__": From 0f510fecb66a170dc4589c9926f142828e3fb6e5 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Fri, 4 Sep 2026 13:40:08 +0400 Subject: [PATCH 2/5] Update error-checking in roundArray() --- src/lemke/randomstart.py | 4 +++- tests/test_randomstart.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lemke/randomstart.py b/src/lemke/randomstart.py index 9445886..db12b52 100644 --- a/src/lemke/randomstart.py +++ b/src/lemke/randomstart.py @@ -35,8 +35,10 @@ def roundArray(x, accuracy=10000): Round each entry of an array of probabilities `x` to the nearest multiple of 1 / `accuracy`. """ + if not isinstance(accuracy, int): + raise TypeError(f"accuracy must be an integer, got {type(accuracy).__name__}") if not 1 <= accuracy <= MAX_ACCURACY: - raise ValueError(f"accuracy must be between 1 and {MAX_ACCURACY}") + raise ValueError(f"accuracy must be between 1 and {MAX_ACCURACY}, got {accuracy}") n = len(x) sum = 0 diff --git a/tests/test_randomstart.py b/tests/test_randomstart.py index 9a2eb6d..d13b501 100644 --- a/tests/test_randomstart.py +++ b/tests/test_randomstart.py @@ -79,6 +79,10 @@ def test_accuracy_out_of_bounds(self, bad_accuracy): with pytest.raises(ValueError, match="accuracy must be between"): roundArray([0.5, 0.5], accuracy=bad_accuracy) + def test_accuracy_not_integer(self): + with pytest.raises(TypeError, match="accuracy must be an integer"): + roundArray([0.5, 0.5], accuracy=2.5) + def test_invalid_probabilities(self): with pytest.raises(ValueError, match="need probabilities"): roundArray([1.0, 1.0]) From 0059b649df598eaf0a488e303600fd03a10d0350 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Fri, 4 Sep 2026 13:45:11 +0400 Subject: [PATCH 3/5] Update roundArray() docstring --- src/lemke/randomstart.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lemke/randomstart.py b/src/lemke/randomstart.py index db12b52..948e7dc 100644 --- a/src/lemke/randomstart.py +++ b/src/lemke/randomstart.py @@ -34,6 +34,12 @@ def roundArray(x, accuracy=10000): """ Round each entry of an array of probabilities `x` to the nearest multiple of 1 / `accuracy`. + + The probabilities are multiplied by `accuracy`, then rounded down to their integer parts, + which will be the numerators, augmented by 1 in order of decreasing size of the remainders + (which are less than 1) until they sum to `accuracy`. + + Example: accuracy=10, x=[0.18, .35, .47] becomes [2/10, 3/10, 5/10]. """ if not isinstance(accuracy, int): raise TypeError(f"accuracy must be an integer, got {type(accuracy).__name__}") From 2c34c6d92f0c357bf11f492c17ec497b604efd78 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Fri, 4 Sep 2026 16:52:10 +0400 Subject: [PATCH 4/5] Fix scatter marker sizing --- src/lemke/randomstart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lemke/randomstart.py b/src/lemke/randomstart.py index 948e7dc..829ff58 100644 --- a/src/lemke/randomstart.py +++ b/src/lemke/randomstart.py @@ -139,7 +139,7 @@ def plot_simplex(numpoints=200, accuracy=20, higherdim=3, naiveplot=False): plt.plot([x], [y], "g.") for circ in roundedpoints: x, y = maptotriangle(circ) - plt.scatter([x], [y], s=10000 // accuracy, facecolors="none", + plt.scatter([x], [y], s=max(20, 10000 // accuracy), facecolors="none", edgecolors="r") plt.show() From 2699bfcf9e1f1e656d051885b57ae8ba4fe2a50b Mon Sep 17 00:00:00 2001 From: nataliemes Date: Fri, 4 Sep 2026 17:03:10 +0400 Subject: [PATCH 5/5] Update error-checking in plot_simplex() --- src/lemke/randomstart.py | 4 +++- tests/test_randomstart.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/lemke/randomstart.py b/src/lemke/randomstart.py index 829ff58..930c249 100644 --- a/src/lemke/randomstart.py +++ b/src/lemke/randomstart.py @@ -111,8 +111,10 @@ def plot_simplex(numpoints=200, accuracy=20, higherdim=3, naiveplot=False): ValueError If `accuracy` or `higherdim` is out of range. """ + if not isinstance(higherdim, int): + raise TypeError(f"higherdim must be an integer, got {type(higherdim).__name__}") if not 3 <= higherdim <= 10: - raise ValueError("higherdim must be between 3 and 10") + raise ValueError(f"higherdim must be between 3 and 10, got {higherdim}") print( f"numpoints={numpoints} accuracy={accuracy} higherdim={higherdim} naiveplot={naiveplot}" ) diff --git a/tests/test_randomstart.py b/tests/test_randomstart.py index d13b501..3e16f28 100644 --- a/tests/test_randomstart.py +++ b/tests/test_randomstart.py @@ -10,6 +10,7 @@ MAX_ACCURACY, main, maptotriangle, + plot_simplex, randInSimplex, renormalize, roundArray, @@ -120,6 +121,17 @@ def test_known_points(self, vec, expected): assert maptotriangle(vec) == pytest.approx(expected) +class TestPlotSimplex: + @pytest.mark.parametrize("bad_higherdim", [-1, 2, 11]) + def test_higherdim_out_of_bounds(self, bad_higherdim): + with pytest.raises(ValueError, match="higherdim must be between"): + plot_simplex(higherdim=bad_higherdim) + + def test_higherdim_not_integer(self): + with pytest.raises(TypeError, match="higherdim must be an integer"): + plot_simplex(higherdim=3.5) + + class TestCLI: @pytest.mark.parametrize( "arguments",