From 659e6b284fc2af9ec7ddfad13638d73976031098 Mon Sep 17 00:00:00 2001 From: Dennis Sweeney Date: Fri, 20 Feb 2026 16:01:31 -0500 Subject: [PATCH 1/9] cleanup and break ties by number of bits --- _mutable_lattice.c | 186 ++++++++++++++++++++++++++++----------------- 1 file changed, 117 insertions(+), 69 deletions(-) diff --git a/_mutable_lattice.c b/_mutable_lattice.c index 91ee140..dca82d0 100644 --- a/_mutable_lattice.c +++ b/_mutable_lattice.c @@ -84,6 +84,18 @@ PyLong_FromIntptr(intptr_t x) return PyLong_FromSsize_t(x); } +static Py_ssize_t +pylong_bit_length(PyObject *x) +{ + // This could be faster using a currently unstable API. + PyObject *numbits_o = PyObject_CallMethod(x, "bit_length", NULL); + if (numbits_o == NULL) { + return -1; + } + Py_ssize_t numbits = PyLong_AsSsize_t(numbits_o); + Py_DECREF(numbits_o); + return numbits; +} /*********************************************************************/ /* Compiler-specific magic to detect overflows */ @@ -333,6 +345,24 @@ TagInt_to_object(TagInt t) } } +static Py_ssize_t +TagInt_bit_length(TagInt t) +{ + if (TagInt_is_pointer(t)) { + return pylong_bit_length(untag_pointer(t)); + } + intptr_t x = unpack_integer(t); + if (x < 0) { + x = -x; + } + Py_ssize_t res = 0; + while (x) { + x >>= 1; + res++; + } + return res; +} + // Puts a new reference in *c. static bool _TagInt_add_with_objects(TagInt a, TagInt b, TagInt *c) @@ -1985,8 +2015,15 @@ Lattice_new_impl(PyTypeObject *type, Py_ssize_t N, int HNF_policy, Py_ssize_t ma } static Py_ssize_t * -counting_sort(Py_ssize_t *inputs, Py_ssize_t length, Py_ssize_t upper_bound) +counting_sort_order(Py_ssize_t *inputs, Py_ssize_t length) { + Py_ssize_t upper_bound = 1; + for (Py_ssize_t i = 0; i < length; i++) { + assert(inputs[i] >= 0); + if (inputs[i] >= upper_bound) { + upper_bound = inputs[i] + 1; + } + } Py_ssize_t *count = PyMem_Calloc(upper_bound, sizeof(Py_ssize_t)); if (count == NULL) { PyErr_NoMemory(); @@ -2038,7 +2075,7 @@ insertion_ordering(Py_ssize_t N, PyObject *data) } index_of_first_nonzero[i] = index; } - Py_ssize_t *order = counting_sort(index_of_first_nonzero, R, N+1); + Py_ssize_t *order = counting_sort_order(index_of_first_nonzero, R); PyMem_Free(index_of_first_nonzero); return order; } @@ -4033,116 +4070,127 @@ relations_among(PyObject *Py_UNUSED(mod), PyObject *arg) // Z-linear combination of the input rows. The right portion keeps // track of the coefficients needed to do so. + Py_ssize_t *nonzero_in_column = PyMem_Calloc(N, sizeof(Py_ssize_t)); + Py_ssize_t *bits_in_column = PyMem_Calloc(N, sizeof(Py_ssize_t)); + Py_ssize_t *column_order = NULL; + Py_ssize_t *index_of_first_nonzero = PyMem_Malloc(R * sizeof(Py_ssize_t)); + Py_ssize_t *rows_by_index_of_first_nonzero = NULL; + TagInt *scratch = PyMem_Malloc((N + R) * sizeof(TagInt *)); + Lattice *L = NULL; + Lattice *relations = NULL; + PyObject *result = NULL; + if (nonzero_in_column == NULL || bits_in_column == NULL || index_of_first_nonzero == NULL || scratch == NULL) { + PyErr_NoMemory(); + goto error; + } + // A first tweak: // Earlier columns influence what row operations happen on what that follows them, // so we reshuffle the columns so that less populated columns come first. - Py_ssize_t *column_weights = PyMem_Calloc(N, sizeof(Py_ssize_t)); - if (column_weights == NULL) { - PyErr_NoMemory(); - return NULL; - } for (Py_ssize_t i = 0; i < R; i++) { TagInt *vec = Vector_get_vec(PyList_GET_ITEM(arg, i)); for (Py_ssize_t j = 0; j < N; j++) { - TagInt t = vec[j]; - if (!TagInt_is_zero(t)) { - column_weights[j] += 1 + TagInt_is_pointer(t); + if (TagInt_is_zero(vec[j])) { + continue; + } + nonzero_in_column[j]++; + Py_ssize_t numbits = TagInt_bit_length(vec[j]); + if (numbits == -1) { + goto error; } + if (bits_in_column[j] >= PY_SSIZE_T_MAX - numbits) { + bits_in_column[j] = PY_SSIZE_T_MAX; + break; + } + bits_in_column[j] += numbits; } } - Py_ssize_t *columns_by_increasing_weight = counting_sort(column_weights, N, 2*R + 1); - if (columns_by_increasing_weight == NULL) { - PyErr_NoMemory(); - PyMem_Free(column_weights); - return NULL; + column_order = counting_sort_order(nonzero_in_column, N); + if (column_order == NULL) { + goto error; } - Py_ssize_t k0 = 0; - while (k0 < N && column_weights[columns_by_increasing_weight[k0]] == 0) { - // trim away zeros columns + Py_ssize_t k0 = 0; // the first nonzero column + while (k0 < N && nonzero_in_column[column_order[k0]] == 0) { k0++; } - PyMem_Free(column_weights); +#if 0 // not sure how much this actually helps... + // Break nonzero_in_column ties using the total numbers of bits + for (Py_ssize_t lo = k0; lo < N; ) { + Py_ssize_t nz = nonzero_in_column[column_order[lo]]; + Py_ssize_t hi = lo; + while (hi < N && nonzero_in_column[column_order[hi]] == nz) { + hi++; + }; + // insertion sort within each nonzero_in_column value band + for (Py_ssize_t k = lo + 1; k < hi; k++) { + Py_ssize_t bits = bits_in_column[column_order[k]]; + Py_ssize_t k_new = k; + while (k_new > lo && bits_in_column[column_order[k]] > bits) { + column_order[k_new] = column_order[k_new - 1]; + k_new--; + } + column_order[k_new] = column_order[k]; + } + lo = hi; + } +#endif // A second tweak: - // Sort rows by increasing index of first nonzero entry. This allows + // Sort rows by decreasing index of first nonzero entry. This allows // the bottom right corner to stay HNF while new rows are added above it. - Py_ssize_t *index_of_first_nonzero = PyMem_Malloc(R * sizeof(Py_ssize_t)); - if (index_of_first_nonzero == NULL) { - PyMem_Free(columns_by_increasing_weight); - PyErr_NoMemory(); - return NULL; - } for (Py_ssize_t i = 0; i < R; i++) { - Py_ssize_t index = N; TagInt *vec = Vector_get_vec(PyList_GET_ITEM(arg, i)); - for (Py_ssize_t k = k0; k < N; k++) { - if (!TagInt_is_zero(vec[columns_by_increasing_weight[k]])) { - index = k; - break; - } + Py_ssize_t k = k0; + while (k < N && TagInt_is_zero(vec[column_order[k]])) { + k++; } - index_of_first_nonzero[i] = index; + index_of_first_nonzero[i] = k; } - Py_ssize_t *rows_by_index_of_first_nonzero = counting_sort(index_of_first_nonzero, R, N + 1); - PyMem_Free(index_of_first_nonzero); + rows_by_index_of_first_nonzero = counting_sort_order(index_of_first_nonzero, R); if (rows_by_index_of_first_nonzero == NULL) { - PyMem_Free(columns_by_increasing_weight); - PyErr_NoMemory(); - return NULL; + goto error; } - // Now add everything in in an appropriate order - TagInt *scratch = (TagInt *)PyMem_Malloc((N - k0 + R) * sizeof(TagInt *)); - if (scratch == NULL) { - PyMem_Free(rows_by_index_of_first_nonzero); - PyMem_Free(columns_by_increasing_weight); - PyErr_NoMemory(); - return NULL; - } - Lattice *L = (Lattice *)Lattice_new_impl(&Lattice_Type, N - k0 + R, 1, R); + L = (Lattice *)Lattice_new_impl(&Lattice_Type, N - k0 + R, 1, R); if (L == NULL) { - PyMem_Free(rows_by_index_of_first_nonzero); - PyMem_Free(columns_by_increasing_weight); - PyMem_Free(scratch); - return NULL; + goto error; } for (Py_ssize_t ii = R-1; ii >= 0; ii--) { Py_ssize_t i = rows_by_index_of_first_nonzero[ii]; TagInt *vec = Vector_get_vec(PyList_GET_ITEM(arg, i)); for (Py_ssize_t k = k0; k < N; k++) { - scratch[k - k0] = vec[columns_by_increasing_weight[k]]; + scratch[k - k0] = vec[column_order[k]]; } memset(scratch + N - k0, 0, R*sizeof(TagInt *)); scratch[N - k0 + i] = TagInt_ONE; if (Lattice_add_vector_impl(L, scratch)) { - PyMem_Free(rows_by_index_of_first_nonzero); - PyMem_Free(columns_by_increasing_weight); - PyMem_Free(scratch); - Py_DECREF(L); - return NULL; + goto error; } } - PyMem_Free(rows_by_index_of_first_nonzero); - PyMem_Free(columns_by_increasing_weight); - PyMem_Free(scratch); - Lattice *result = (Lattice *)Lattice_new_impl(&Lattice_Type, R, 1, R); - if (result == NULL) { - Py_DECREF(L); - return NULL; + relations = (Lattice *)Lattice_new_impl(&Lattice_Type, R, 1, R); + if (relations == NULL) { + goto error; } assert(L->rank == R); for (Py_ssize_t i = R - 1; i >= 0; i--) { if (L->row_to_pivot[i] >= N - k0) { - if (Lattice_add_vector_impl(result, L->basis[i] + N - k0)) { - Py_DECREF(L); - Py_DECREF(result); - return NULL; + if (Lattice_add_vector_impl(relations, L->basis[i] + N - k0)) { + goto error; } } } + result = Py_NewRef((PyObject *)relations); +error: + PyMem_Free(nonzero_in_column); + PyMem_Free(bits_in_column); + PyMem_Free(column_order); + PyMem_Free(index_of_first_nonzero); + PyMem_Free(rows_by_index_of_first_nonzero); + PyMem_Free(scratch); Py_DECREF(L); - return (PyObject *)result; + Py_DECREF(relations); + return result; } static PyObject * From 9df1057ed37605a4922067968fbea60b832c06c5 Mon Sep 17 00:00:00 2001 From: Dennis Sweeney Date: Fri, 20 Feb 2026 16:05:13 -0500 Subject: [PATCH 2/9] remove sorting by numbers of bits --- _mutable_lattice.c | 64 +--------------------------------------------- 1 file changed, 1 insertion(+), 63 deletions(-) diff --git a/_mutable_lattice.c b/_mutable_lattice.c index dca82d0..7e22610 100644 --- a/_mutable_lattice.c +++ b/_mutable_lattice.c @@ -84,18 +84,6 @@ PyLong_FromIntptr(intptr_t x) return PyLong_FromSsize_t(x); } -static Py_ssize_t -pylong_bit_length(PyObject *x) -{ - // This could be faster using a currently unstable API. - PyObject *numbits_o = PyObject_CallMethod(x, "bit_length", NULL); - if (numbits_o == NULL) { - return -1; - } - Py_ssize_t numbits = PyLong_AsSsize_t(numbits_o); - Py_DECREF(numbits_o); - return numbits; -} /*********************************************************************/ /* Compiler-specific magic to detect overflows */ @@ -345,24 +333,6 @@ TagInt_to_object(TagInt t) } } -static Py_ssize_t -TagInt_bit_length(TagInt t) -{ - if (TagInt_is_pointer(t)) { - return pylong_bit_length(untag_pointer(t)); - } - intptr_t x = unpack_integer(t); - if (x < 0) { - x = -x; - } - Py_ssize_t res = 0; - while (x) { - x >>= 1; - res++; - } - return res; -} - // Puts a new reference in *c. static bool _TagInt_add_with_objects(TagInt a, TagInt b, TagInt *c) @@ -4071,7 +4041,6 @@ relations_among(PyObject *Py_UNUSED(mod), PyObject *arg) // track of the coefficients needed to do so. Py_ssize_t *nonzero_in_column = PyMem_Calloc(N, sizeof(Py_ssize_t)); - Py_ssize_t *bits_in_column = PyMem_Calloc(N, sizeof(Py_ssize_t)); Py_ssize_t *column_order = NULL; Py_ssize_t *index_of_first_nonzero = PyMem_Malloc(R * sizeof(Py_ssize_t)); Py_ssize_t *rows_by_index_of_first_nonzero = NULL; @@ -4079,7 +4048,7 @@ relations_among(PyObject *Py_UNUSED(mod), PyObject *arg) Lattice *L = NULL; Lattice *relations = NULL; PyObject *result = NULL; - if (nonzero_in_column == NULL || bits_in_column == NULL || index_of_first_nonzero == NULL || scratch == NULL) { + if (nonzero_in_column == NULL || index_of_first_nonzero == NULL || scratch == NULL) { PyErr_NoMemory(); goto error; } @@ -4094,15 +4063,6 @@ relations_among(PyObject *Py_UNUSED(mod), PyObject *arg) continue; } nonzero_in_column[j]++; - Py_ssize_t numbits = TagInt_bit_length(vec[j]); - if (numbits == -1) { - goto error; - } - if (bits_in_column[j] >= PY_SSIZE_T_MAX - numbits) { - bits_in_column[j] = PY_SSIZE_T_MAX; - break; - } - bits_in_column[j] += numbits; } } column_order = counting_sort_order(nonzero_in_column, N); @@ -4113,27 +4073,6 @@ relations_among(PyObject *Py_UNUSED(mod), PyObject *arg) while (k0 < N && nonzero_in_column[column_order[k0]] == 0) { k0++; } -#if 0 // not sure how much this actually helps... - // Break nonzero_in_column ties using the total numbers of bits - for (Py_ssize_t lo = k0; lo < N; ) { - Py_ssize_t nz = nonzero_in_column[column_order[lo]]; - Py_ssize_t hi = lo; - while (hi < N && nonzero_in_column[column_order[hi]] == nz) { - hi++; - }; - // insertion sort within each nonzero_in_column value band - for (Py_ssize_t k = lo + 1; k < hi; k++) { - Py_ssize_t bits = bits_in_column[column_order[k]]; - Py_ssize_t k_new = k; - while (k_new > lo && bits_in_column[column_order[k]] > bits) { - column_order[k_new] = column_order[k_new - 1]; - k_new--; - } - column_order[k_new] = column_order[k]; - } - lo = hi; - } -#endif // A second tweak: // Sort rows by decreasing index of first nonzero entry. This allows @@ -4183,7 +4122,6 @@ relations_among(PyObject *Py_UNUSED(mod), PyObject *arg) result = Py_NewRef((PyObject *)relations); error: PyMem_Free(nonzero_in_column); - PyMem_Free(bits_in_column); PyMem_Free(column_order); PyMem_Free(index_of_first_nonzero); PyMem_Free(rows_by_index_of_first_nonzero); From af4adc2815b51e16e98180959c50f2324879f949 Mon Sep 17 00:00:00 2001 From: Dennis Sweeney Date: Fri, 20 Feb 2026 18:15:22 -0500 Subject: [PATCH 3/9] add test_refleaks --- _mutable_lattice.c | 4 ++-- mutable_lattice/test.py | 2 +- mutable_lattice/test_refleaks.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 mutable_lattice/test_refleaks.py diff --git a/_mutable_lattice.c b/_mutable_lattice.c index 7e22610..d056acf 100644 --- a/_mutable_lattice.c +++ b/_mutable_lattice.c @@ -4126,8 +4126,8 @@ relations_among(PyObject *Py_UNUSED(mod), PyObject *arg) PyMem_Free(index_of_first_nonzero); PyMem_Free(rows_by_index_of_first_nonzero); PyMem_Free(scratch); - Py_DECREF(L); - Py_DECREF(relations); + Py_XDECREF(L); + Py_XDECREF(relations); return result; } diff --git a/mutable_lattice/test.py b/mutable_lattice/test.py index bd6b65f..c848dac 100644 --- a/mutable_lattice/test.py +++ b/mutable_lattice/test.py @@ -1592,4 +1592,4 @@ def test_errors(self): self.assertEqual(L.decompose([[2]]), ([[0],[1],[2]], [Lattice.full(1)]*3)) if __name__ == "__main__": - unittest.main(exit=False) + unittest.main() diff --git a/mutable_lattice/test_refleaks.py b/mutable_lattice/test_refleaks.py new file mode 100644 index 0000000..d526c39 --- /dev/null +++ b/mutable_lattice/test_refleaks.py @@ -0,0 +1,14 @@ +import sys +try: + rc = sys.gettotalrefcount() +except AttributeError as e: + raise RuntimeError("hunt_refleaks must be run on a debug build of Python") from e + +def main(): + import mutable_lattice.test + sys.modules["test.test_mutable_lattice_dynamic"] = mutable_lattice.test + from test.libregrtest.main import main as _main + _main(["test_mutable_lattice_dynamic"], huntrleaks=(3,3,"reflog.txt")) + +if __name__ == "__main__": + main() From 04df79dc7fadf5d04feeaf1bd754adc96367389d Mon Sep 17 00:00:00 2001 From: Dennis Sweeney Date: Sat, 21 Feb 2026 15:55:53 -0500 Subject: [PATCH 4/9] add refleaks github action --- .github/workflows/hunt_refleaks.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/hunt_refleaks.yml diff --git a/.github/workflows/hunt_refleaks.yml b/.github/workflows/hunt_refleaks.yml new file mode 100644 index 0000000..885ac38 --- /dev/null +++ b/.github/workflows/hunt_refleaks.yml @@ -0,0 +1,21 @@ +name: Check for refleaks + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + hunt_refleaks: + runs-on: ubuntu-latest + name: Check for refleaks + steps: + - uses: actions/checkout@v2 + - uses: deadsnakes/action@v3.2.0 + with: + python-version: 3.10-dev + debug: true + - run: python -m mutable_lattice.test_refleaks \ No newline at end of file From 94f0856f932f944d6358d9819de3aefe8e767fb2 Mon Sep 17 00:00:00 2001 From: Dennis Sweeney Date: Sat, 21 Feb 2026 16:01:16 -0500 Subject: [PATCH 5/9] nits --- .github/workflows/hunt_refleaks.yml | 2 +- _mutable_lattice.c | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/hunt_refleaks.yml b/.github/workflows/hunt_refleaks.yml index 885ac38..b19017d 100644 --- a/.github/workflows/hunt_refleaks.yml +++ b/.github/workflows/hunt_refleaks.yml @@ -18,4 +18,4 @@ jobs: with: python-version: 3.10-dev debug: true - - run: python -m mutable_lattice.test_refleaks \ No newline at end of file + - run: python -m mutable_lattice.test_refleaks diff --git a/_mutable_lattice.c b/_mutable_lattice.c index d056acf..d6e4bcf 100644 --- a/_mutable_lattice.c +++ b/_mutable_lattice.c @@ -1987,6 +1987,8 @@ Lattice_new_impl(PyTypeObject *type, Py_ssize_t N, int HNF_policy, Py_ssize_t ma static Py_ssize_t * counting_sort_order(Py_ssize_t *inputs, Py_ssize_t length) { + // sorted(range(length), + // key=inputs.__getitem__) Py_ssize_t upper_bound = 1; for (Py_ssize_t i = 0; i < length; i++) { assert(inputs[i] >= 0); @@ -2005,6 +2007,7 @@ counting_sort_order(Py_ssize_t *inputs, Py_ssize_t length) PyErr_NoMemory(); return NULL; } + // https://en.wikipedia.org/wiki/Counting_sort for (Py_ssize_t i = 0; i < length; i++) { // make a histogram assert(inputs[i] < upper_bound); @@ -4059,10 +4062,9 @@ relations_among(PyObject *Py_UNUSED(mod), PyObject *arg) for (Py_ssize_t i = 0; i < R; i++) { TagInt *vec = Vector_get_vec(PyList_GET_ITEM(arg, i)); for (Py_ssize_t j = 0; j < N; j++) { - if (TagInt_is_zero(vec[j])) { - continue; + if (!TagInt_is_zero(vec[j])) { + nonzero_in_column[j]++; } - nonzero_in_column[j]++; } } column_order = counting_sort_order(nonzero_in_column, N); From 81e7f9a00fd6a4ddea938c8249a0ef52edc557ac Mon Sep 17 00:00:00 2001 From: Dennis Sweeney Date: Sat, 21 Feb 2026 16:09:45 -0500 Subject: [PATCH 6/9] fix hunt_refleaks action --- .github/workflows/hunt_refleaks.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/hunt_refleaks.yml b/.github/workflows/hunt_refleaks.yml index b19017d..e9a6b1c 100644 --- a/.github/workflows/hunt_refleaks.yml +++ b/.github/workflows/hunt_refleaks.yml @@ -18,4 +18,7 @@ jobs: with: python-version: 3.10-dev debug: true - - run: python -m mutable_lattice.test_refleaks + - name: Build package + run: python -m pip install ./ -v + - name: Check for reference leaks + run: python -m mutable_lattice.test_refleaks From 32e4ae71c63d81c54a630a7681baad269068ab8e Mon Sep 17 00:00:00 2001 From: Dennis Sweeney Date: Sat, 21 Feb 2026 16:28:44 -0500 Subject: [PATCH 7/9] use py3.11 --- .github/workflows/hunt_refleaks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hunt_refleaks.yml b/.github/workflows/hunt_refleaks.yml index e9a6b1c..53f0196 100644 --- a/.github/workflows/hunt_refleaks.yml +++ b/.github/workflows/hunt_refleaks.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v2 - uses: deadsnakes/action@v3.2.0 with: - python-version: 3.10-dev + python-version: 3.11-dev debug: true - name: Build package run: python -m pip install ./ -v From 80087169694bd235988a62455513e02d320fa202 Mon Sep 17 00:00:00 2001 From: Dennis Sweeney Date: Sat, 21 Feb 2026 18:31:20 -0500 Subject: [PATCH 8/9] fix three actual leaks --- _mutable_lattice.c | 6 +++--- mutable_lattice/test.py | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/_mutable_lattice.c b/_mutable_lattice.c index d6e4bcf..10d9a77 100644 --- a/_mutable_lattice.c +++ b/_mutable_lattice.c @@ -59,6 +59,7 @@ pylong_lt(PyObject *a, PyObject *b) return -1; } assert(res == Py_True || res == Py_False); + Py_DECREF(res); return (res == Py_True); } @@ -680,9 +681,7 @@ Vector_sq_ass_item(PyObject *self, Py_ssize_t j, PyObject *xo) return -1; } TagInt t; - Py_INCREF(xo); - if (object_to_TagInt_steal(xo, &t)) { - Py_DECREF(xo); + if (object_to_TagInt_steal(Py_NewRef(xo), &t)) { return -1; } TagInt *vec = Vector_get_vec(self); @@ -3155,6 +3154,7 @@ make_pivots_positive(Lattice *L) } } } + Py_DECREF(zero); return false; error: Py_DECREF(zero); diff --git a/mutable_lattice/test.py b/mutable_lattice/test.py index c848dac..4f1af42 100644 --- a/mutable_lattice/test.py +++ b/mutable_lattice/test.py @@ -240,11 +240,6 @@ def test_iter(self): data = [10, 20, 30] self.assertEqual([x for x in Vector(data)], data) - def test_length(self): - self.assertEqual(len(Vector([])), 0) - self.assertEqual(len(Vector([10])), 1) - self.assertEqual(len(Vector([10, 20, 30])), 3) - def test_setitem_simple(self): v = Vector([10, 20, 30]) v[1] = 999 From 8272b434f0fa6418520b5bd81aded8abd06f5f49 Mon Sep 17 00:00:00 2001 From: Dennis Sweeney Date: Sat, 21 Feb 2026 18:44:33 -0500 Subject: [PATCH 9/9] bump to v0.3.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 06d964b..c1a52fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ test-command = "python -m mutable_lattice.test -v" [project] name = "mutable_lattice" -version = "0.2.1" +version = "0.3.0" authors = [ { name="Dennis Sweeney", email="sweeney.427@osu.edu" }, ]