Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How would I solve a linear Diophantine congruence in Python?

I was given a challenge where the solution involves solving a series of linear modular equations in 14 variables. The following is a selection of these equations:

3a + 3b + 3c + 3d + 3e + 3f + 3g + h + i + j + k + l + m + n = 15
7a + 9b + 17c + 11d + 6e + 5f + g = 3
13a + 2b + 9c + 8d + 12f + 13g = 17
5a + 2b + 16c + 12d + 5e + 7f + g = 11
6a + 4b + 9c + 6d + 4e + 9f + 6g + h + 7i + 11j + k + 7l + 11m + n = 8
10a + 15b + 13c + 10d + 15e + 13f + 10g + 12h + 18i + 8j + 12k + 18l + 8m + 12n = 18
9a + 12b + 14c + 4d + 9e + 16f + 3g + 7h + 17i + 11j + 14k + 3l + 18m + n = 15
9a + 12b + 16c + 15d + e + 14f + 6g + 11h + 2i + 9j + 12k + 16l + 15m + n = 14

I ended up copying them into this modular equation solver to get a parameterized solution. However, I want to be able to do this automatically in a Python program, without depending on that website.

Preferably, I should be able to do this with an arbitrary group of (linear) equations, not just these specific ones. Part of my solution for the challenge required me to write a few different equations for different scenarios, and swap them out as needed.

I'm aware that SymPy has a Diophantine equation solver that almost does what I want it to do, but in the docs I didn't see a way to get it to enforce a certain modulus (mod 19, mod 23, etc).

like image 582
Josiah Winslow Avatar asked Oct 21 '25 00:10

Josiah Winslow


1 Answers

The word diophantine is misleading here. What you want to do is linear algebra over Z_p meaning the integers modulo a prime p. SymPy can do this if you use DomainMatrix instead of Matrix. This is how to do it in SymPy 1.12:

In [1]: from sympy import *

In [2]: from sympy.polys.matrices import DomainMatrix

In [3]: a, b, c, d, e, f, g, h, i, j, k, l, m, n = syms = symbols('a:n')

In [4]: eqs = '''
   ...: 3a + 3b + 3c + 3d + 3e + 3f + 3g + h + i + j + k + l + m + n = 15
   ...: 7a + 9b + 17c + 11d + 6e + 5f + g = 3
   ...: 13a + 2b + 9c + 8d + 12f + 13g = 17
   ...: 5a + 2b + 16c + 12d + 5e + 7f + g = 11
   ...: 6a + 4b + 9c + 6d + 4e + 9f + 6g + h + 7i + 11j + k + 7l + 11m + n = 8
   ...: 10a + 15b + 13c + 10d + 15e + 13f + 10g + 12h + 18i + 8j + 12k + 18l + 8m + 12n = 18
   ...: 9a + 12b + 14c + 4d + 9e + 16f + 3g + 7h + 17i + 11j + 14k + 3l + 18m + n = 15
   ...: 9a + 12b + 16c + 15d + e + 14f + 6g + 11h + 2i + 9j + 12k + 16l + 15m + n = 14
   ...: '''

In [5]: eqs = [parse_expr(eq, transformations='all').subs(I, i) for eq in eqs.strip().splitlines()]

In [6]: M, b = linear_eq_to_matrix(eqs, syms)

In [7]: M
Out[7]: 
⎡3   3   3   3   3   3   3   1   1   1  1   1   1   1 ⎤
⎢                                                     ⎥
⎢7   9   17  11  6   5   1   0   0   0  0   0   0   0 ⎥
⎢                                                     ⎥
⎢13  2   9   8   0   12  13  0   0   0  0   0   0   0 ⎥
⎢                                                     ⎥
⎢5   2   16  12  5   7   1   0   0   0  0   0   0   0 ⎥
⎢                                                     ⎥
⎢6   4   9   6   4   9   6   1   18  0  1   7   11  1 ⎥
⎢                                                     ⎥
⎢10  15  13  10  15  13  10  12  26  0  12  18  8   12⎥
⎢                                                     ⎥
⎢9   12  14  4   9   16  3   7   28  0  14  3   18  1 ⎥
⎢                                                     ⎥
⎣9   12  16  15  1   14  6   11  11  0  12  16  15  1 ⎦

In [8]: b
Out[8]: 
⎡15⎤
⎢  ⎥
⎢3 ⎥
⎢  ⎥
⎢17⎥
⎢  ⎥
⎢11⎥
⎢  ⎥
⎢8 ⎥
⎢  ⎥
⎢18⎥
⎢  ⎥
⎢15⎥
⎢  ⎥
⎣14⎦

In [9]: Z_17 = GF(17, symmetric=False)

In [12]: dM = DomainMatrix.from_Matrix(M).convert_to(Z_17).to_dense()

In [13]: db = DomainMatrix.from_Matrix(b).convert_to(Z_17).to_dense()

In [14]: dM
Out[14]: DomainMatrix([[3 mod 17, 3 mod 17, 3 mod 17, 3 mod 17, 3 mod 17, 3 mod 17, 3 mod 17, 1 mod 17, 1 mod 17, 1 mod 17, 1 mod 17, 1 mod 17, 1 mod 17, 1 mod 17], [7 mod 17, 9 mod 17, 0 mod 17, 11 mod 17, 6 mod 17, 5 mod 17, 1 mod 17, 0 mod 17, 0 mod 17, 0 mod 17, 0 mod 17, 0 mod 17, 0 mod 17, 0 mod 17], [13 mod 17, 2 mod 17, 9 mod 17, 8 mod 17, 0 mod 17, 12 mod 17, 13 mod 17, 0 mod 17, 0 mod 17, 0 mod 17, 0 mod 17, 0 mod 17, 0 mod 17, 0 mod 17], [5 mod 17, 2 mod 17, 16 mod 17, 12 mod 17, 5 mod 17, 7 mod 17, 1 mod 17, 0 mod 17, 0 mod 17, 0 mod 17, 0 mod 17, 0 mod 17, 0 mod 17, 0 mod 17], [6 mod 17, 4 mod 17, 9 mod 17, 6 mod 17, 4 mod 17, 9 mod 17, 6 mod 17, 1 mod 17, 1 mod 17, 0 mod 17, 1 mod 17, 7 mod 17, 11 mod 17, 1 mod 17], [10 mod 17, 15 mod 17, 13 mod 17, 10 mod 17, 15 mod 17, 13 mod 17, 10 mod 17, 12 mod 17, 9 mod 17, 0 mod 17, 12 mod 17, 1 mod 17, 8 mod 17, 12 mod 17], [9 mod 17, 12 mod 17, 14 mod 17, 4 mod 17, 9 mod 17, 16 mod 17, 3 mod 17, 7 mod 17, 11 mod 17, 0 mod 17, 14 mod 17, 3 mod 17, 1 mod 17, 1 mod 17], [9 mod 17, 12 mod 17, 16 mod 17, 15 mod 17, 1 mod 17, 14 mod 17, 6 mod 17, 11 mod 17, 11 mod 17, 0 mod 17, 12 mod 17, 16 mod 17, 15 mod 17, 1 mod 17]], (8, 14), GF(17))

In [15]: db
Out[15]: DomainMatrix([[15 mod 17], [3 mod 17], [0 mod 17], [11 mod 17], [8 mod 17], [1 mod 17], [15 mod 17], [14 mod 17]], (8, 1), GF(17))

Now you have dM and db as matrices over Z_17 and you can use whatever matrix operations you want and convert back to Matrix with to_Matrix:

In [16]: dM.hstack(db).rref()[0].to_Matrix() # RREF of augmented matrix
Out[16]: 
⎡1  0  0  0  0  0  0  0  11  11  5   2   2   6   0 ⎤
⎢                                                  ⎥
⎢0  1  0  0  0  0  0  0  14  2   11  12  14  13  6 ⎥
⎢                                                  ⎥
⎢0  0  1  0  0  0  0  0  3   7   10  11  15  0   5 ⎥
⎢                                                  ⎥
⎢0  0  0  1  0  0  0  0  2   2   1   16  16  13  5 ⎥
⎢                                                  ⎥
⎢0  0  0  0  1  0  0  0  13  13  16  7   14  0   15⎥
⎢                                                  ⎥
⎢0  0  0  0  0  1  0  0  16  10  5   11  15  11  7 ⎥
⎢                                                  ⎥
⎢0  0  0  0  0  0  1  0  8   10  6   13  1   0   7 ⎥
⎢                                                  ⎥
⎣0  0  0  0  0  0  0  1  4   6   9   6   8   8   16⎦

In [17]: dM.nullspace().to_Matrix() # nullspace of M
Out[17]: 
⎡15  16  1   12  10  11  14  7   11  0   0   0   0   0 ⎤
⎢                                                      ⎥
⎢15  12  8   12  10  9   9   2   0   11  0   0   0   0 ⎥
⎢                                                      ⎥
⎢13  15  9   6   11  13  2   3   0   0   11  0   0   0 ⎥
⎢                                                      ⎥
⎢12  4   15  11  8   15  10  2   0   0   0   11  0   0 ⎥
⎢                                                      ⎥
⎢12  16  5   11  16  5   6   14  0   0   0   0   11  0 ⎥
⎢                                                      ⎥
⎣2   10  0   10  0   15  0   14  0   0   0   0   0   11⎦

You can use these to construct your parametric solution:

In [30]: p = dM.hstack(db).rref()[0].to_Matrix()[:,-1]

In [31]: p = Matrix.vstack(p, p.zeros(6, 1))

In [32]: p.transpose()
Out[32]: [0  6  5  5  15  7  7  16  0  0  0  0  0  0]

In [33]: (M*p - b).applyfunc(lambda e: e % 17).is_zero_matrix
Out[33]: True

In [34]: N = dM.nullspace().to_Matrix()

In [35]: N = dM.nullspace().transpose().to_Matrix()

In [36]: (M*N).applyfunc(lambda e: e % 17).is_zero_matrix
Out[36]: True

The parametric solution is generated from the particular solution and nullspace:

In [38]: sol = p + N*Matrix(symbols('alpha:6'))

In [39]: sol
Out[39]: 
⎡  15⋅α₀ + 15⋅α₁ + 13⋅α₂ + 12⋅α₃ + 12⋅α₄ + 2⋅α₅  ⎤
⎢                                                ⎥
⎢16⋅α₀ + 12⋅α₁ + 15⋅α₂ + 4⋅α₃ + 16⋅α₄ + 10⋅α₅ + 6⎥
⎢                                                ⎥
⎢      α₀ + 8⋅α₁ + 9⋅α₂ + 15⋅α₃ + 5⋅α₄ + 5       ⎥
⎢                                                ⎥
⎢12⋅α₀ + 12⋅α₁ + 6⋅α₂ + 11⋅α₃ + 11⋅α₄ + 10⋅α₅ + 5⎥
⎢                                                ⎥
⎢   10⋅α₀ + 10⋅α₁ + 11⋅α₂ + 8⋅α₃ + 16⋅α₄ + 15    ⎥
⎢                                                ⎥
⎢11⋅α₀ + 9⋅α₁ + 13⋅α₂ + 15⋅α₃ + 5⋅α₄ + 15⋅α₅ + 7 ⎥
⎢                                                ⎥
⎢     14⋅α₀ + 9⋅α₁ + 2⋅α₂ + 10⋅α₃ + 6⋅α₄ + 7     ⎥
⎢                                                ⎥
⎢ 7⋅α₀ + 2⋅α₁ + 3⋅α₂ + 2⋅α₃ + 14⋅α₄ + 14⋅α₅ + 16 ⎥
⎢                                                ⎥
⎢                     11⋅α₀                      ⎥
⎢                                                ⎥
⎢                     11⋅α₁                      ⎥
⎢                                                ⎥
⎢                     11⋅α₂                      ⎥
⎢                                                ⎥
⎢                     11⋅α₃                      ⎥
⎢                                                ⎥
⎢                     11⋅α₄                      ⎥
⎢                                                ⎥
⎣                     11⋅α₅                      ⎦

Let's check that:

In [47]: (M*sol)._rep.convert_to(Z_17[symbols('alpha:6')]).to_Matrix() == b.applyfunc(lambda e: e % 17)
Out[47]: True
like image 126
Oscar Benjamin Avatar answered Oct 22 '25 13:10

Oscar Benjamin



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!