From 50cf56833326964edf391b897944727d4c196fa4 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Sat, 27 Jun 2026 21:33:43 +0400 Subject: [PATCH 1/9] Use Click to parse command-line args in bimatrix.py --- src/lemke/bimatrix.py | 204 ++++++++++++++++++++---------------------- 1 file changed, 96 insertions(+), 108 deletions(-) diff --git a/src/lemke/bimatrix.py b/src/lemke/bimatrix.py index b66f339..66720e3 100644 --- a/src/lemke/bimatrix.py +++ b/src/lemke/bimatrix.py @@ -2,22 +2,13 @@ import fractions import random # random.seed -import sys +from functools import wraps +import click import numpy as np from . import columnprint, lemke, randomstart, utils - -# for debugging -def printglobals(): - globs = [x for x in globals() if "__" not in x] - for var in globs: - value = str(globals()[var]) - if "<" not in value: - print(" " + str(var) + "=", value) - - # file format: # # m*n entries of A, separated by blanks / newlines @@ -27,87 +18,6 @@ def printglobals(): # defaults # MAXDIM = 2000 # largest allowed value for m and n; not used yet -gamefilename = "game" -gz0 = False -LHstring = "" # empty means LH not called -seed = -1 -trace = -1 # negative: no tracing -# accuracy = DEFAULT_accuracy = 1000 -accuracy = 1000 - - -# amends defaults -def processArguments(): - global gamefilename, gz0, LHstring, seed, trace, accuracy - arglist = sys.argv[1:] - setLH = False - settrace = False - setaccuracy = False - setseed = False - setdecimals = False - showhelp = False - for s in arglist: - if s[0] == "-": - # discard optional argument-parameters - if setLH or settrace or setseed: - setLH = settrace = setseed = False - if s == "-LH": - setLH = True - LHstring = "1-" - elif s == "-trace": - settrace = True - trace = 0 - elif s == "-seed": - setseed = True - seed = 0 - elif s == "-accuracy": - setaccuracy = True - elif s == "-decimals": - setdecimals = True - elif s == "-z0": - gz0 = True - elif s == "-?" or s == "-help": - showhelp = True - else: # any other "-" argument - showhelp = True - print("! unknown option: ", repr(s)) - else: # s not starting with "-" - if setLH: - setLH = False - LHstring = s - elif settrace: - settrace = False - trace = int(s) - elif setseed: - setseed = False - seed = int(s) - elif setdecimals: - setdecimals = False - utils.setdecimals(int(s)) - elif setaccuracy: - setaccuracy = False - accuracy = int(s) - else: - gamefilename = s - if showhelp: - helpstring = ( - """usage: bimatrix.py [options] -options: - here: """ - + repr(gamefilename) - + """, must not start with '-' - -LH [] : Lemke-Howson with missing labels, e.g. '1,3-5,7-' ('' = all) - -trace []: tracing procedure, no. of priors, 0 = centroid - -seed [] : random seed, default: None - -accuracy : accuracy prior, =denominator, here """ - + str(accuracy) - + """ - -decimals : allowed payoff digits in input after decimal point, default 4 - -?, -help: show this help and exit""" - ) - print(helpstring) - exit(0) - return # list generated from string s such as "1-3,10,4-7", all not @@ -257,8 +167,6 @@ def runLH(self, droppedlabel): return tuple(getequil(tabl)) def LH(self, LHstring): - if LHstring == "": - return m = self.A.numrows n = self.A.numcolumns lhset = {} # dict of equilibria and list by which label found @@ -284,9 +192,9 @@ def runtrace(self, xprior, yprior): tabl.runlemke() return tuple(getequil(tabl)) - def tracing(self, trace): + def tracing(self, trace, seed=None, accuracy=1000): if trace < 0: - return + raise ValueError("Number of priors must be a positive integer") m = self.A.numrows n = self.A.numcolumns trset = {} # dict of equilibria, how often found @@ -298,7 +206,7 @@ def tracing(self, trace): trace = 1 # for percentage else: for k in range(trace): - if seed >= 0: + if seed is not None: random.seed(10 * trace * seed + k) x = randomstart.randInSimplex(m) xprior = randomstart.roundArray(x, accuracy) @@ -364,15 +272,95 @@ def submatrix(A, rowset, colset): return B +@click.group( + context_settings={"help_option_names": ["-?", "-h", "--help"]}, +) def main(): - processArguments() - printglobals() - - G = bimatrix(gamefilename) - print(G) - G.LH(LHstring) - G.tracing(trace) - - -if __name__ == "__main__": - main() + """Find Nash equilibria of a bimatrix game.""" + pass + + +def common_options(f): + @click.argument( + "filename", + type=click.Path(exists=True, readable=True, file_okay=True, dir_okay=False), + ) + @click.option( + "--decimals", + default=4, + show_default=True, + help="Allowed payoff digits in input after decimal point", + ) + # @click.option( + # "--z0", + # is_flag=True, + # help="Show value of z0 at each step", + # ) + @wraps(f) + def wrapper(*args, decimals, **kwargs): + utils.setdecimals(decimals) + return f(*args, **kwargs) + return wrapper + + +@main.command() +@common_options +@click.option( + "--labels", + default="1-", + help="Missing labels, e.g. 1,3-5,7- " + "[default: all labels]", +) +def lh(filename, labels): + """Find equilibria using the Lemke-Howson algorithm.""" + + # for debugging + print("filename:", filename) + print("labels:", labels) + print() + + G = bimatrix(filename) + G.LH(labels) + + +@main.command() +@common_options +@click.option( + "--priors", + type=click.IntRange(min=1), + help="Number of random priors", +) +@click.option( + "--seed", + type=int, + help="Random seed", +) +@click.option( + "--accuracy", + default=1000, + show_default=True, + help="Denominator x: each random prior is rounded to the nearest multiple of 1/x", +) +def trace(filename, priors, seed, accuracy): + """ + Find equilibria using the tracing procedure. + + Without --priors, uses the centroid. + With --priors N, uses N random starting points. + """ + + # for debugging + print("file:", filename) + print("priors:", priors) + print("seed:", seed) + print("accuracy:", accuracy) + print() + + G = bimatrix(filename) + + if priors is None: + if seed is not None or accuracy != 1000: + raise click.UsageError("--seed and --accuracy require --priors") + G.tracing(0) + else: + G.tracing(priors, seed, accuracy) From 7931cd275d91211b557e7efb542c588d1363e53c Mon Sep 17 00:00:00 2001 From: nataliemes Date: Sun, 26 Jul 2026 23:42:13 +0400 Subject: [PATCH 2/9] Remove debugging prints --- src/lemke/bimatrix.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/lemke/bimatrix.py b/src/lemke/bimatrix.py index 66720e3..74fe780 100644 --- a/src/lemke/bimatrix.py +++ b/src/lemke/bimatrix.py @@ -314,11 +314,6 @@ def wrapper(*args, decimals, **kwargs): def lh(filename, labels): """Find equilibria using the Lemke-Howson algorithm.""" - # for debugging - print("filename:", filename) - print("labels:", labels) - print() - G = bimatrix(filename) G.LH(labels) @@ -349,13 +344,6 @@ def trace(filename, priors, seed, accuracy): With --priors N, uses N random starting points. """ - # for debugging - print("file:", filename) - print("priors:", priors) - print("seed:", seed) - print("accuracy:", accuracy) - print() - G = bimatrix(filename) if priors is None: From f2184d2e706b669d324e58563dfab21dbfb1763e Mon Sep 17 00:00:00 2001 From: nataliemes Date: Mon, 27 Jul 2026 07:05:41 +0400 Subject: [PATCH 3/9] Split tracing() into 2 separate methods --- src/lemke/bimatrix.py | 70 +++++++++++++++++++++++++----------------- tests/test_bimatrix.py | 66 +++++++++++++++++++-------------------- 2 files changed, 72 insertions(+), 64 deletions(-) diff --git a/src/lemke/bimatrix.py b/src/lemke/bimatrix.py index 74fe780..8565d96 100644 --- a/src/lemke/bimatrix.py +++ b/src/lemke/bimatrix.py @@ -192,37 +192,51 @@ def runtrace(self, xprior, yprior): tabl.runlemke() return tuple(getequil(tabl)) - def tracing(self, trace, seed=None, accuracy=1000): - if trace < 0: + def trace_uniform_prior(self): + m = self.A.numrows + n = self.A.numcolumns + + xprior = uniform(m) + yprior = uniform(n) + eq = self.runtrace(xprior, yprior) + + self._print_trace_results( + equilibria={eq: 1}, + total_priors=1, + ) + + def trace_random_priors(self, trace, seed=None, accuracy=1000): + if trace <= 0: raise ValueError("Number of priors must be a positive integer") m = self.A.numrows n = self.A.numcolumns trset = {} # dict of equilibria, how often found - if trace == 0: - xprior = uniform(m) - yprior = uniform(n) + + for k in range(trace): + if seed is not None: + random.seed(10 * trace * seed + k) + x = randomstart.randInSimplex(m) + xprior = randomstart.roundArray(x, accuracy) + y = randomstart.randInSimplex(n) + yprior = randomstart.roundArray(y, accuracy) + # print (f"{k=} {xprior=} {yprior=}") eq = self.runtrace(xprior, yprior) - trset[eq] = 1 - trace = 1 # for percentage - else: - for k in range(trace): - if seed is not None: - random.seed(10 * trace * seed + k) - x = randomstart.randInSimplex(m) - xprior = randomstart.roundArray(x, accuracy) - y = randomstart.randInSimplex(n) - yprior = randomstart.roundArray(y, accuracy) - # print (f"{k=} {xprior=} {yprior=}") - eq = self.runtrace(xprior, yprior) - if eq in trset: - trset[eq] += 1 - else: - print("found eq", str_eq(eq, m, n), "index", self.eqindex(eq, m, n)) - trset[eq] = 1 + if eq in trset: + trset[eq] += 1 + else: + print("found eq", str_eq(eq, m, n), "index", self.eqindex(eq, m, n)) + trset[eq] = 1 + + self._print_trace_results(trset, trace) + + def _print_trace_results(self, equilibria, total_priors): + m = self.A.numrows + n = self.A.numcolumns + print("-------- statistics of equilibria found: --------") - for eq in trset: - print(trset[eq], "times found ", str_eq(eq, m, n)) - print(trace, "total priors,", len(trset), "equilibria found") + for eq, count in equilibria.items(): + print(count, "times found", str_eq(eq, m, n)) + print(total_priors, "total priors,", len(equilibria), "equilibria found") def eqindex(self, eq, m, n): rowset, colset = supports(eq, m, n) @@ -347,8 +361,6 @@ def trace(filename, priors, seed, accuracy): G = bimatrix(filename) if priors is None: - if seed is not None or accuracy != 1000: - raise click.UsageError("--seed and --accuracy require --priors") - G.tracing(0) + G.trace_uniform_prior() else: - G.tracing(priors, seed, accuracy) + G.trace_random_priors(priors, seed, accuracy) diff --git a/tests/test_bimatrix.py b/tests/test_bimatrix.py index c1f1194..3239132 100644 --- a/tests/test_bimatrix.py +++ b/tests/test_bimatrix.py @@ -1,5 +1,7 @@ """ Test Lemke-Howson algorithm and tracing procedure on bimatrix games. +Tracing procedure is tested with a uniform prior, as well as random priors. +Seed is fixed, so that random priors are reproducible. Conditions checked: For games with unique equilibrium: @@ -15,13 +17,14 @@ import random import typing from fractions import Fraction as Fr +from functools import partial from pathlib import Path import pygambit import pytest from lemke import randomstart -from lemke.bimatrix import bimatrix, uniform +from lemke.bimatrix import bimatrix FIXTURES_DIR = Path("tests/fixtures/bimatrix") @@ -33,40 +36,33 @@ def lh_solver(G: bimatrix) -> list[list[Fr]]: return [list(eq_key) for eq_key in lh_eqs_dict] -def tracing_solver( - G: bimatrix, - trace: int = 10, - seed: int = 0, - accuracy: int = 1000, +# trace = 10, seed = 0 +def trace_random_priors( + game: bimatrix, + trace, + seed=None, + accuracy=1000, ) -> list[list[Fr]]: - """ Run tracing & return the list of equilibria found. """ - - if trace < 0: - return [] - - m = G.A.numrows - n = G.A.numcolumns - trset = {} - - if trace == 0: - xprior = uniform(m) - yprior = uniform(n) - eq = G.runtrace(xprior, yprior) - trset[eq] = 1 - trace = 1 - else: - for k in range(trace): - if seed >= 0: - random.seed(10 * trace * seed + k) - x = randomstart.randInSimplex(m) - xprior = randomstart.roundArray(x, accuracy) - y = randomstart.randInSimplex(n) - yprior = randomstart.roundArray(y, accuracy) - eq = G.runtrace(xprior, yprior) - if eq in trset: - trset[eq] += 1 - else: - trset[eq] = 1 + """Copied from bimatrix.py, but explicitly returns equilibria.""" + + if trace <= 0: + raise ValueError("Number of priors must be a positive integer") + m = game.A.numrows + n = game.A.numcolumns + trset = {} # dict of equilibria, how often found + + for k in range(trace): + if seed is not None: + random.seed(10 * trace * seed + k) + x = randomstart.randInSimplex(m) + xprior = randomstart.roundArray(x, accuracy) + y = randomstart.randInSimplex(n) + yprior = randomstart.roundArray(y, accuracy) + eq = game.runtrace(xprior, yprior) + if eq in trset: + trset[eq] += 1 + else: + trset[eq] = 1 return [list(eq) for eq in trset] @@ -187,7 +183,7 @@ class GameTestCase: SOLVERS = [ pytest.param(lh_solver, id="LH"), - pytest.param(tracing_solver, id="tracing"), + pytest.param(partial(trace_random_priors, trace=10, seed=0), id="tracing"), ] From 705269cdf7e958829ebce97c824249a9718b17f3 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Mon, 27 Jul 2026 07:13:15 +0400 Subject: [PATCH 4/9] Add testing for tracing with uniform prior --- tests/test_bimatrix.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/test_bimatrix.py b/tests/test_bimatrix.py index 3239132..b0b96f3 100644 --- a/tests/test_bimatrix.py +++ b/tests/test_bimatrix.py @@ -24,7 +24,7 @@ import pytest from lemke import randomstart -from lemke.bimatrix import bimatrix +from lemke.bimatrix import bimatrix, uniform FIXTURES_DIR = Path("tests/fixtures/bimatrix") @@ -36,6 +36,19 @@ def lh_solver(G: bimatrix) -> list[list[Fr]]: return [list(eq_key) for eq_key in lh_eqs_dict] +def trace_uniform_prior(game: bimatrix) -> list[list[Fr]]: + """Copied from bimatrix.py, but explicitly returns equilibria.""" + + m = game.A.numrows + n = game.A.numcolumns + + xprior = uniform(m) + yprior = uniform(n) + eq = game.runtrace(xprior, yprior) + + return [list(eq)] + + # trace = 10, seed = 0 def trace_random_priors( game: bimatrix, @@ -183,7 +196,8 @@ class GameTestCase: SOLVERS = [ pytest.param(lh_solver, id="LH"), - pytest.param(partial(trace_random_priors, trace=10, seed=0), id="tracing"), + pytest.param(trace_uniform_prior, id="trace_uniform"), + pytest.param(partial(trace_random_priors, trace=10, seed=0), id="trace_random"), ] From 4e17e5cd0896f36cf5de14aa413a62915775b432 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Mon, 27 Jul 2026 07:15:41 +0400 Subject: [PATCH 5/9] Split trace command into 2 separate subcommands --- src/lemke/bimatrix.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/lemke/bimatrix.py b/src/lemke/bimatrix.py index 8565d96..7a5196b 100644 --- a/src/lemke/bimatrix.py +++ b/src/lemke/bimatrix.py @@ -332,11 +332,29 @@ def lh(filename, labels): G.LH(labels) -@main.command() +@main.group() +def trace(): + """Find equilibria using the tracing procedure.""" + pass + + +@trace.command(name="uniform") +@common_options +def trace_uniform_cmd(filename): + """Trace using a uniform prior.""" + + G = bimatrix(filename) + G.trace_uniform_prior() + + +@trace.command(name="random") @common_options @click.option( "--priors", + default=1, + show_default=True, type=click.IntRange(min=1), + metavar="INTEGER", help="Number of random priors", ) @click.option( @@ -350,17 +368,8 @@ def lh(filename, labels): show_default=True, help="Denominator x: each random prior is rounded to the nearest multiple of 1/x", ) -def trace(filename, priors, seed, accuracy): - """ - Find equilibria using the tracing procedure. - - Without --priors, uses the centroid. - With --priors N, uses N random starting points. - """ +def trace_random_cmd(filename, priors, seed, accuracy): + """Trace using random prior(s).""" G = bimatrix(filename) - - if priors is None: - G.trace_uniform_prior() - else: - G.trace_random_priors(priors, seed, accuracy) + G.trace_random_priors(priors, seed, accuracy) From 89e751639b2699018ba5d4cb6435ee0dcbf736fd Mon Sep 17 00:00:00 2001 From: nataliemes Date: Mon, 27 Jul 2026 07:22:10 +0400 Subject: [PATCH 6/9] Add range checks for --decimals, --accuracy --- src/lemke/bimatrix.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lemke/bimatrix.py b/src/lemke/bimatrix.py index 7a5196b..ae044fb 100644 --- a/src/lemke/bimatrix.py +++ b/src/lemke/bimatrix.py @@ -8,6 +8,8 @@ import numpy as np from . import columnprint, lemke, randomstart, utils +from .randomstart import MAX_ACCURACY +from .utils import MAXDECIMALS # file format: # @@ -303,6 +305,8 @@ def common_options(f): "--decimals", default=4, show_default=True, + type=click.IntRange(min=0, max=MAXDECIMALS), + metavar="INTEGER", help="Allowed payoff digits in input after decimal point", ) # @click.option( @@ -366,7 +370,9 @@ def trace_uniform_cmd(filename): "--accuracy", default=1000, show_default=True, - help="Denominator x: each random prior is rounded to the nearest multiple of 1/x", + type=click.IntRange(1, MAX_ACCURACY), + metavar="INTEGER", + help="Denominator x: each coordinate of the prior is rounded to the nearest 1/x", ) def trace_random_cmd(filename, priors, seed, accuracy): """Trace using random prior(s).""" From ddf5134ecc549a3fe7e1ec571e4bff1cc9ad5083 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Mon, 27 Jul 2026 07:34:06 +0400 Subject: [PATCH 7/9] Add tests --- tests/test_bimatrix_units.py | 336 +++++++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 tests/test_bimatrix_units.py diff --git a/tests/test_bimatrix_units.py b/tests/test_bimatrix_units.py new file mode 100644 index 0000000..27a5a77 --- /dev/null +++ b/tests/test_bimatrix_units.py @@ -0,0 +1,336 @@ +import textwrap +from fractions import Fraction + +import pytest +from click.testing import CliRunner + +from lemke import utils +from lemke.bimatrix import ( + bimatrix, + lh, + payoffmatrix, + rangesplit, + submatrix, + supports, + trace_random_cmd, + trace_uniform_cmd, + uniform, +) +from lemke.randomstart import MAX_ACCURACY +from lemke.utils import MAXDECIMALS + + +# --- PAYOFF MATRIX -------------------------------------------------- +@pytest.fixture +def small_payoff_matrix(): + return payoffmatrix([ + [1, 2], + [3, 4], + ]) + + +def test_payoff_matrix_init(small_payoff_matrix): + pm = small_payoff_matrix + assert pm.numrows == 2 + assert pm.numcolumns == 2 + + assert all( + isinstance(pm.matrix[i][j], Fraction) + for i in range(2) for j in range(2) + ) + assert pm.matrix[1][1] == Fraction(4) + + +def test_payoff_matrix_max_min(small_payoff_matrix): + pm = small_payoff_matrix + assert pm.max == Fraction(4) + assert pm.min == Fraction(1) + + +def test_payoff_matrix_negshift_negmatrix(small_payoff_matrix): + pm = small_payoff_matrix + + assert pm.negshift == 5 # int(max) + 1 + + expected = [ + [pm.negshift - pm.matrix[0][0], pm.negshift - pm.matrix[0][1]], + [pm.negshift - pm.matrix[1][0], pm.negshift - pm.matrix[1][1]], + ] + + for i, row in enumerate(expected): + for j, value in enumerate(row): + assert pm.negmatrix[i][j] == value + + +def test_addrow_updates_shape_max_min(small_payoff_matrix): + pm = small_payoff_matrix + pm.addrow([10, -10]) + assert pm.numrows == 3 + assert pm.matrix[2][0] == Fraction(10) + assert pm.max == Fraction(10) + assert pm.min == Fraction(-10) + + +def test_addcolumn_updates_shape_max_min(small_payoff_matrix): + pm = small_payoff_matrix + pm.addcolumn([-5, 20]) + assert pm.numcolumns == 3 + assert pm.matrix[0][2] == Fraction(-5) + assert pm.max == Fraction(20) + assert pm.min == Fraction(-5) + + +# --- BIMATRIX INIT -------------------------------------------------- +@pytest.fixture +def small_game_file(tmp_path): + utils.setdecimals(4) + content = textwrap.dedent(""" + 2 2 + + # A= + 1 0.75 + -1/3 2/4 + + # B= + .4 3/1 + -2 1.0 + """) + path = tmp_path / "small_game.txt" + path.write_text(content) + return str(path) + + +def test_bimatrix_dimensions(small_game_file): + G = bimatrix(small_game_file) + assert G.A.numrows == 2 + assert G.A.numcolumns == 2 + assert G.B.numrows == 2 + assert G.B.numcolumns == 2 + + +def test_bimatrix_payoff_values(small_game_file): + G = bimatrix(small_game_file) + + expected = [ + [Fraction(1), Fraction(3, 4)], + [Fraction(-1, 3), Fraction(1, 2)] + ] + for i, row in enumerate(expected): + for j, value in enumerate(row): + assert G.A.matrix[i][j] == value + + expected = [ + [Fraction(2, 5), Fraction(3)], + [Fraction(-2), Fraction(1)] + ] + for i, row in enumerate(expected): + for j, value in enumerate(row): + assert G.B.matrix[i][j] == value + + +def test_bimatrix_invalid_file_exits(tmp_path): + bad_content = "2 2\n1 2 3\n" + path = tmp_path / "bad_game.txt" + path.write_text(bad_content) + with pytest.raises(SystemExit): + bimatrix(str(path)) + + +# --- BIMATRIX LCP ---------------------------------------------------- +def test_q_last_two_entries_are_minus_one(small_game_file): + G = bimatrix(small_game_file) + lcp = G.createLCP() + assert lcp.q[-1] == -1 + assert lcp.q[-2] == -1 + + +def test_d_defaults_to_all_ones(small_game_file): + G = bimatrix(small_game_file) + lcp = G.createLCP() + assert all(d == 1 for d in lcp.d) + + +def test_player_block_signs(small_game_file): + G = bimatrix(small_game_file) + lcp = G.createLCP() + m, n = G.A.numrows, G.A.numcolumns + lcpdim = m + n + 2 + + for i in range(m): + assert lcp.M[i][lcpdim - 2] == -1 + for i in range(m): + assert lcp.M[lcpdim - 2][i] == 1 + + for j in range(m, m + n): + assert lcp.M[j][lcpdim - 1] == -1 + for j in range(m, m + n): + assert lcp.M[lcpdim - 1][j] == 1 + + +def test_payoff_blocks_use_negmatrix(small_game_file): + G = bimatrix(small_game_file) + lcp = G.createLCP() + m, n = G.A.numrows, G.A.numcolumns + for i in range(m): + for j in range(n): + assert lcp.M[i][j + m] == G.A.negmatrix[i][j] + for j in range(n): + for i in range(m): + assert lcp.M[j + m][i] == G.B.negmatrix[i][j] + + +# --- TRACE WITH RANDOM PRIORS ------------------------------------------------- +@pytest.mark.parametrize("bad_priors", [0, -1, -100]) +def test_rejects_non_positive_num_priors(small_game_file, bad_priors): + G = bimatrix(small_game_file) + with pytest.raises(ValueError): + G.trace_random_priors(bad_priors) + + +# --- HELPER FUNCTIONS -------------------------------------------------------- +@pytest.mark.parametrize( + "input, expected", + [ + ("1,3,5", [1, 3, 5]), # single numbers + ("1, 3, 5", [1, 3, 5]), # single numbers with space + ("1-3", [1, 2, 3]), # simple range + ("1-3,10,4-7", [1, 2, 3, 10, 4, 5, 6, 7]), # mixed + ("8-", [8, 9, 10]), # open-ended range + ("1,8-", [1, 8, 9, 10]), # mixed with open-ended range + ("20-30", []), # range beyond "endrange" + ("1,,3", [1, 3]), # empty parts + ("", []), # empty input string + ] +) +def test_rangesplit(input, expected): + assert rangesplit(input, endrange=10) == expected + + +@pytest.mark.parametrize("n", [1, 2, 3, 7]) +def test_uniform(n): + result = uniform(n) + assert len(result) == n + assert sum(result) == Fraction(1, 1) + assert all(isinstance(x, Fraction) for x in result) + + +@pytest.mark.parametrize( + "eq, row_expected, col_expected", + [ + ((0, 0, 0, 0), [], []), + ((1, 1, 1, 1), [0, 1], [0, 1]), + ((1, 2, 0, 3), [0, 1], [1]), + ] +) +def test_supports(eq, row_expected, col_expected): + row_result, col_result = supports(eq, m=2, n=2) + assert row_result == row_expected + assert col_result == col_expected + + +def test_submatrix(): + result = submatrix( + A=[ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ], + rowset=[0, 2], + colset=[1, 2], + ) + expected = [ + [2, 3], + [8, 9], + ] + + for i, row in enumerate(expected): + for j, value in enumerate(row): + assert result[i][j] == value + + +# --- CLI ------------------------------------------------------------ +@pytest.mark.parametrize( + "command, args", + [ + (lh, []), + (lh, [ + "--decimals", "10", + "--labels", "1-2", + ]), + + (trace_uniform_cmd, []), + (trace_uniform_cmd, [ + "--decimals", "10", + ]), + + (trace_random_cmd, []), + (trace_random_cmd, [ + "--decimals", "10", + "--priors", "10", + "--seed", "42", + "--accuracy", "100", + ]), + ] +) +def test_cli_runs_without_error(command, args, small_game_file): + runner = CliRunner() + result = runner.invoke(command, [str(small_game_file)] + args) + + assert result.exit_code == 0 + + +@pytest.mark.parametrize("command", [ + lh, + trace_uniform_cmd, + trace_random_cmd, +]) +def test_cli_rejects_missing_file(command, tmp_path): + missing_path = tmp_path / "missing" + + runner = CliRunner() + result = runner.invoke(command, [str(missing_path)]) + + assert result.exit_code == 2 + assert "does not exist" in result.output.lower() + + +@pytest.mark.parametrize("command", [ + lh, + trace_uniform_cmd, + trace_random_cmd, +]) +def test_cli_rejects_directory(command, tmp_path): + runner = CliRunner() + result = runner.invoke(command, [str(tmp_path)]) + + assert result.exit_code == 2 + assert "is a directory" in result.output.lower() + + +@pytest.mark.parametrize("command", [ + lh, + trace_uniform_cmd, + trace_random_cmd, +]) +@pytest.mark.parametrize("decimals", ["-1", f"{MAXDECIMALS + 1}"]) +def test_cli_invalid_decimals(command, decimals, tmp_path): + runner = CliRunner() + result = runner.invoke(command, [str(tmp_path), "--decimals", decimals]) + assert result.exit_code != 0 + assert "Invalid value" in result.output + + +@pytest.mark.parametrize("priors", ["-1", "0"]) +def test_trace_random_invalid_priors(priors, tmp_path): + runner = CliRunner() + result = runner.invoke(trace_random_cmd, [str(tmp_path), "--priors", priors]) + assert result.exit_code != 0 + assert "Invalid value" in result.output + + +@pytest.mark.parametrize("accuracy", ["-1", "0", f"{MAX_ACCURACY + 1}"]) +def test_trace_random_invalid_accuracy(accuracy, tmp_path): + runner = CliRunner() + result = runner.invoke(trace_random_cmd, ["--accuracy", accuracy]) + assert result.exit_code != 0 + assert "Invalid value" in result.output From f3f4627c96f1817c18cb727530b5222868f9f826 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Mon, 27 Jul 2026 08:18:03 +0400 Subject: [PATCH 8/9] Rename lemke.py unit tests file for consistency --- tests/{test_lemke.py => test_lcp_units.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_lemke.py => test_lcp_units.py} (100%) diff --git a/tests/test_lemke.py b/tests/test_lcp_units.py similarity index 100% rename from tests/test_lemke.py rename to tests/test_lcp_units.py From e230758a586747e456b00312ed3cd01b1c747b51 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Thu, 30 Jul 2026 08:52:38 +0400 Subject: [PATCH 9/9] Update tests --- tests/test_bimatrix_units.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_bimatrix_units.py b/tests/test_bimatrix_units.py index 27a5a77..130573e 100644 --- a/tests/test_bimatrix_units.py +++ b/tests/test_bimatrix_units.py @@ -101,7 +101,7 @@ def small_game_file(tmp_path): def test_bimatrix_dimensions(small_game_file): - G = bimatrix(small_game_file) + G = bimatrix.from_file(small_game_file) assert G.A.numrows == 2 assert G.A.numcolumns == 2 assert G.B.numrows == 2 @@ -109,7 +109,7 @@ def test_bimatrix_dimensions(small_game_file): def test_bimatrix_payoff_values(small_game_file): - G = bimatrix(small_game_file) + G = bimatrix.from_file(small_game_file) expected = [ [Fraction(1), Fraction(3, 4)], @@ -133,25 +133,25 @@ def test_bimatrix_invalid_file_exits(tmp_path): path = tmp_path / "bad_game.txt" path.write_text(bad_content) with pytest.raises(SystemExit): - bimatrix(str(path)) + bimatrix.from_file(str(path)) # --- BIMATRIX LCP ---------------------------------------------------- def test_q_last_two_entries_are_minus_one(small_game_file): - G = bimatrix(small_game_file) + G = bimatrix.from_file(small_game_file) lcp = G.createLCP() assert lcp.q[-1] == -1 assert lcp.q[-2] == -1 def test_d_defaults_to_all_ones(small_game_file): - G = bimatrix(small_game_file) + G = bimatrix.from_file(small_game_file) lcp = G.createLCP() assert all(d == 1 for d in lcp.d) def test_player_block_signs(small_game_file): - G = bimatrix(small_game_file) + G = bimatrix.from_file(small_game_file) lcp = G.createLCP() m, n = G.A.numrows, G.A.numcolumns lcpdim = m + n + 2 @@ -168,7 +168,7 @@ def test_player_block_signs(small_game_file): def test_payoff_blocks_use_negmatrix(small_game_file): - G = bimatrix(small_game_file) + G = bimatrix.from_file(small_game_file) lcp = G.createLCP() m, n = G.A.numrows, G.A.numcolumns for i in range(m): @@ -182,7 +182,7 @@ def test_payoff_blocks_use_negmatrix(small_game_file): # --- TRACE WITH RANDOM PRIORS ------------------------------------------------- @pytest.mark.parametrize("bad_priors", [0, -1, -100]) def test_rejects_non_positive_num_priors(small_game_file, bad_priors): - G = bimatrix(small_game_file) + G = bimatrix.from_file(small_game_file) with pytest.raises(ValueError): G.trace_random_priors(bad_priors)