4
\$\begingroup\$

It's a simple program, the user is prompted to enter number of variables.

enter image description here

Then the user is prompted to enter coefficients and constants. That many equations will be there as many variables.

enter image description here

Once filled up, submit button is clicked and the solutions are displayed.

enter image description here

I've accomplished this much successfully. However I'm new to Tkinter. I would appreciate if someone can help me improve the user interface and the code in general. The code is here. No need to make any change to the solving algorithm. I'm not allowed to use numpy for this assignment :( I want to improve the interface and the design language, that's it.

def minor(matrix, i, j):
 return [[matrix[r][c] for c in range(len(matrix[r])) if c != j]
 for r in range(len(matrix)) if r != i]
def det(matrix):
 if len(matrix) == len(matrix[0]) == 1:
 return matrix[0][0]
 return sum(matrix[0][i] * cofac(matrix, 0, i) for i in range(len(matrix[0])))
cofac = lambda matrix, i, j: (-1) ** ((i + j) % 2) * det(minor(matrix, i, j))
transpose = lambda matrix: [[matrix[r][c] for r in range(len(matrix))] for c in range(len(matrix[0]))]
def adj(matrix):
 return transpose([[cofac(matrix, r, c) for c in range(len(matrix[r]))] for r in range(len(matrix))])
def div_and_store(a, d):
 toPrint = []
 for i in a:
 toPrint.append([])
 for j in i:
 if j % d == 0:
 toPrint[-1].append(f'{j//d}')
 else:
 h = hcf(j, d)
 denominator = d//h
 numerator = j//h
 if denominator > 0:
 toPrint[-1].append(f'{numerator}/{denominator}')
 else:
 toPrint[-1].append(f'{-numerator}/{-denominator}')
 return toPrint
hcf = lambda x, y: y if x == 0 else hcf(y % x, x)
def product(A, B):
 if len(A[0]) != len(B):
 return
 Bt = transpose(B)
 return [[sum(a * b for a, b in zip(i, j)) for j in Bt] for i in A]
from tkinter import *
root = Tk()
root.resizable(width=False, height=False) # not resizable in both directions
root.title('Simultaneous liner equation Solver')
my_label = Label(root, text='How many variables?')
my_label.grid(row=0, column=0)
e = Entry(root, width=10, borderwidth=5)
def done():
 global row, n
 try:
 A = []
 S = []
 for record in entries:
 A.append([])
 for i in range(0, len(record)-1):
 entry = record[i].get()
 if entry:
 A[-1].append(int(entry))
 else:
 A[-1].append(0)
 entry = record[-1].get()
 S.append([int(entry)])
 except ValueError:
 new_label = Label(root, text='Invalid. Try again!')
 new_label.grid(row=row, columnspan=n * 3, sticky='W')
 new_label.after(1000, lambda: new_label.destroy())
 return
 for record in entries:
 for entry in record:
 entry['state'] = DISABLED
 new_button['state'] = DISABLED
 determinant = det(A)
 if determinant == 0:
 new_label = Label(root, text='No unique solution set!')
 new_label.grid(row=row, columnspan=n * 3, sticky='W')
 return
 adjoin = adj(A)
 solution = div_and_store(product(adjoin, S), determinant)
 for i in range(n):
 new_label = Label(root, text=chr(97+i) + ' = '+solution[i][0])
 new_label.grid(row=row, columnspan=2, sticky='W')
 row += 1
def submit():
 try:
 global n
 n = int(e.get())
 except ValueError:
 label = Label(root, text="You're supposed to enter a number, Try again")
 label.grid(row=1, column=0, columnspan=3)
 label.after(1000, lambda: label.destroy())
 return
 if n < 2:
 label = Label(root, text="At least two variables are required!")
 label.grid(row=1, column=0, columnspan=3)
 label.after(1000, lambda: label.destroy())
 return
 e['state'] = DISABLED
 my_label.grid_forget()
 my_button.grid_forget()
 e.grid_forget()
 global row, entries
 row = 0
 entries = []
 for i in range(n):
 Label(root, text='').grid(row=row, columnspan=3*n+1)
 row += 1
 entries.append([])
 col = 0
 for j in map(chr, range(97, 97+n)):
 entry = Entry(root, width=5, borderwidth=2, justify='right')
 entries[i].append(entry)
 entry.grid(row=row, column=col)
 col += 1
 Label(root, text=j).grid(row=row, column=col, padx=5, sticky='W')
 col += 1
 if col == 3*n-1:
 Label(root, text='=').grid(row=row, column=col)
 col += 1
 entry = Entry(root, width=5, borderwidth=2, justify='right')
 entries[i].append(entry)
 entry.grid(row=row, column=col)
 else:
 Label(root, text='+').grid(row=row, column=col)
 col += 1
 row += 1
 Label(root, text='').grid(row=row)
 row += 1
 txt = 'May leave the blank empty if the coefficient is 0!'
 Label(root, text=txt).grid(row=row, columnspan=3*n-1, sticky='W')
 row += 1
 global new_button
 new_button = Button(root, text='Submit', command=done)
 new_button.grid(row=row, column=3*n)
 row += 1
my_button = Button(root, text='Submit', command=submit)
e.grid(row=0, column=2)
my_button.grid(row=0, column=3)
root.mainloop()
asked Sep 14, 2021 at 23:21
\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$
  • Always put your imports at the top of the file
  • Avoid importing splat *; either import specific symbols, or import the module (optionally with an alias) like import tkinter as tk and refer to symbols within its namespace like tk.Button.
  • Strongly consider using Sympy. It can natively do fractional matrix math, as well as more advanced stuff like thorough treatment of solution sets and degrees of freedom.
  • PEP484 type-hint your method signatures.
  • Expand your one-liner nested comprehensions, which are very difficult to read, into multi-line formatting with nested indentation
  • Do not use lambdas when functions suffice.
  • Variables like toPrint should be to_print by PEP8
  • Avoid globals like root, my_label etc. being in the global namespace. Pass them around by function parameter or as a class.
  • e, my_button and my_label are not good variable names.
  • Do not call mainloop from the global namespace; call it from a main function so that it's possible for other people to reuse or test your code in pieces
  • Your interface decision of having a "disposable program" that can only solve one system, after which all controls are disabled, is strange. I would instead expect
    • do not have a Submit button at all
    • do not disable anything upon solution
    • update the output solution set whenever any field is edited, so long as the inputs are valid
    • if the inputs are invalid, rather than temporarily showing an error message and then hiding it after a timer, immediately show an error message that persists until the inputs have been edited to be valid
    • do not have a two-step UI that asks you for the number of parameters. Instead, have a one-step UI with a spinbox control that allows selection of the number of parameters, adjusting the appropriate input controls in real time.
  • You currently reject float inputs. It's not necessary to do this - you can have a conversion stage e.g. from 3.7 to 37/10, and preserve your exact fractional math.

A light and partial refactor that touches on only a few of the above suggestions, particularly type hinting, is

from numbers import Real
from typing import List, Sequence
from tkinter import Button, Entry, Label, Tk, DISABLED
def minor(matrix: Sequence[Sequence[Real]], i: int, j: int) -> List[List[Real]]:
 return [
 [
 matrix[r][c] for c in range(len(matrix[r]))
 if c != j
 ]
 for r in range(len(matrix)) if r != i
 ]
def det(matrix: Sequence[Sequence[Real]]) -> Real:
 if len(matrix) == len(matrix[0]) == 1:
 return matrix[0][0]
 return sum(
 matrix[0][i] * cofac(matrix, 0, i)
 for i in range(len(matrix[0]))
 )
def cofac(matrix: Sequence[Sequence[Real]], i: int, j: int) -> Real:
 return (-1) ** ((i + j) % 2) * det(minor(matrix, i, j))
def transpose(matrix: Sequence[Sequence[Real]]) -> List[List[Real]]:
 return [
 [
 matrix[r][c] for r in range(len(matrix))
 ] for c in range(len(matrix[0]))
 ]
def adj(matrix: Sequence[Sequence[Real]]) -> List[List[Real]]:
 return transpose(
 [
 [
 cofac(matrix, r, c) for c in range(len(matrix[r]))
 ] for r in range(len(matrix))
 ]
 )
def div_and_store(a: Sequence[Sequence[Real]], d: Real) -> List[List[Real]]:
 to_print = []
 for i in a:
 to_print.append([])
 for j in i:
 if j % d == 0:
 to_print[-1].append(f'{j//d}')
 else:
 h = hcf(j, d)
 denominator = d//h
 numerator = j//h
 if denominator > 0:
 to_print[-1].append(f'{numerator}/{denominator}')
 else:
 to_print[-1].append(f'{-numerator}/{-denominator}')
 return to_print
def hcf(x: Real, y: Real) -> Real:
 return y if x == 0 else hcf(y % x, x)
def product(A: Sequence[Sequence[Real]], B: Sequence[Sequence[Real]]) -> List[List[Real]]:
 if len(A[0]) != len(B):
 raise ValueError()
 Bt = transpose(B)
 return [
 [
 sum(a * b for a, b in zip(i, j)) for j in Bt
 ] for i in A
 ]
def done() -> None:
 global row, n
 try:
 A = []
 S = []
 for record in entries:
 A.append([])
 for i in range(0, len(record)-1):
 entry = record[i].get()
 if entry:
 A[-1].append(int(entry))
 else:
 A[-1].append(0)
 entry = record[-1].get()
 S.append([int(entry)])
 except ValueError:
 new_label = Label(root, text='Invalid. Try again!')
 new_label.grid(row=row, columnspan=n * 3, sticky='W')
 new_label.after(1000, lambda: new_label.destroy())
 return
 for record in entries:
 for entry in record:
 entry['state'] = DISABLED
 new_button['state'] = DISABLED
 determinant = det(A)
 if determinant == 0:
 new_label = Label(root, text='No unique solution set!')
 new_label.grid(row=row, columnspan=n * 3, sticky='W')
 return
 adjoin = adj(A)
 solution = div_and_store(product(adjoin, S), determinant)
 for i in range(n):
 new_label = Label(root, text=chr(97+i) + ' = '+solution[i][0])
 new_label.grid(row=row, columnspan=2, sticky='W')
 row += 1
def submit() -> None:
 try:
 global n
 n = int(e.get())
 except ValueError:
 label = Label(root, text="You're supposed to enter a number, Try again")
 label.grid(row=1, column=0, columnspan=3)
 label.after(1000, lambda: label.destroy())
 return
 if n < 2:
 label = Label(root, text="At least two variables are required!")
 label.grid(row=1, column=0, columnspan=3)
 label.after(1000, lambda: label.destroy())
 return
 e['state'] = DISABLED
 my_label.grid_forget()
 my_button.grid_forget()
 e.grid_forget()
 global row, entries
 row = 0
 entries = []
 for i in range(n):
 Label(root, text='').grid(row=row, columnspan=3*n+1)
 row += 1
 entries.append([])
 col = 0
 for j in map(chr, range(97, 97+n)):
 entry = Entry(root, width=5, borderwidth=2, justify='right')
 entries[i].append(entry)
 entry.grid(row=row, column=col)
 col += 1
 Label(root, text=j).grid(row=row, column=col, padx=5, sticky='W')
 col += 1
 if col == 3*n-1:
 Label(root, text='=').grid(row=row, column=col)
 col += 1
 entry = Entry(root, width=5, borderwidth=2, justify='right')
 entries[i].append(entry)
 entry.grid(row=row, column=col)
 else:
 Label(root, text='+').grid(row=row, column=col)
 col += 1
 row += 1
 Label(root, text='').grid(row=row)
 row += 1
 txt = 'May leave the blank empty if the coefficient is 0!'
 Label(root, text=txt).grid(row=row, columnspan=3*n-1, sticky='W')
 row += 1
 global new_button
 new_button = Button(root, text='Submit', command=done)
 new_button.grid(row=row, column=3*n)
 row += 1
root = Tk()
e = Entry(root, width=10, borderwidth=5)
my_label = Label(root, text='How many variables?')
my_button = Button(root, text='Submit', command=submit)
def main() -> None:
 root.resizable(width=False, height=False) # not resizable in both directions
 root.title('Simultaneous linear equation Solver')
 my_label.grid(row=0, column=0)
 e.grid(row=0, column=2)
 my_button.grid(row=0, column=3)
 root.mainloop()
if __name__ == '__main__':
 main()

With all suggestions, a refactor looks like

from fractions import Fraction
from string import ascii_lowercase
from typing import List, Optional, Callable, Dict
from sympy import Matrix, linsolve, FiniteSet
import tkinter as tk
MAX_VARS = len(ascii_lowercase)
ComplainCB = Callable[[str, Optional[str]], None]
class UICell:
 def __init__(
 self, parent: tk.Widget, complain: ComplainCB, row: int, col: int,
 justify: str = tk.LEFT,
 ) -> None:
 self.complain = complain
 self.var_name = f'cell_{row}_{col}'
 # not DoubleVar - it needs to be Fraction-parseable
 self.var = tk.StringVar(
 master=parent,
 name=self.var_name,
 value='0',
 )
 self.trace_id = self.var.trace_add('write', self.changed)
 self.entry = tk.Entry(
 master=parent,
 textvariable=self.var,
 width=6,
 justify=justify,
 )
 self.entry.grid(row=row, column=col)
 self.value: Optional[Fraction] = Fraction(0)
 def destroy(self) -> None:
 self.var.trace_remove('write', self.trace_id)
 self.entry.destroy()
 def changed(self, name: str, index: str, mode: str) -> None:
 try:
 self.value = Fraction(self.var.get())
 except ValueError:
 self.value = None
 if self.value is None:
 complaint = 'Invalid number'
 colour = '#FCD0D0'
 else:
 complaint = None
 colour = 'white'
 self.entry.configure(background=colour)
 self.complain(self.var_name, complaint)
class UIVarCell(UICell):
 def __init__(self, parent: tk.Widget, complain: ComplainCB, row: int, col: int, letter: str) -> None:
 super().__init__(parent, complain, row, col, tk.RIGHT)
 self.letter = letter
 self.separator = tk.Label(master=parent)
 self.separator.grid(sticky=tk.W, row=row, column=1 + col)
 def destroy(self) -> None:
 super().destroy()
 self.separator.destroy()
 def set_separator(self, rightmost: bool) -> None:
 if rightmost:
 sep = '='
 else:
 sep = '+'
 self.separator.config(text=f'{self.letter} {sep}')
class UIVariable:
 def __init__(self, parent: tk.Widget, complain: ComplainCB, index: int) -> None:
 self.parent, self.complain, self.index = parent, complain, index
 self.letter = ascii_lowercase[index]
 self.cells: List[UIVarCell] = []
 self.result_var = tk.StringVar(
 master=parent,
 value='',
 name=f'result_{index}',
 )
 self.result_entry = tk.Entry(
 master=parent,
 state='readonly',
 textvariable=self.result_var,
 width=6,
 justify=tk.RIGHT,
 )
 self.result_label = tk.Label(master=parent, text=f'={self.letter}')
 self.result_entry.grid(sticky=tk.E, row=1 + MAX_VARS, column=2*index)
 self.result_label.grid(sticky=tk.W, row=1 + MAX_VARS, column=2*index + 1)
 for _ in range(index + 1):
 self.grow()
 def grow(self) -> None:
 self.cells.append(UIVarCell(
 parent=self.parent,
 complain=self.complain,
 row=1 + len(self.cells),
 col=2*self.index,
 letter=self.letter,
 ))
 def shrink(self) -> None:
 self.cells.pop().destroy()
 def destroy(self) -> None:
 for widget in (self.result_label, self.result_entry):
 widget.destroy()
 while self.cells:
 self.shrink()
 def set_result(self, s: str) -> None:
 self.result_var.set(s)
class UIFrame:
 def __init__(self, parent: tk.Tk):
 self.root = root = tk.Frame(master=parent)
 self.variables: List[UIVariable] = []
 self.sums: List[UICell] = []
 self.complaints: Dict[str, str] = {}
 self.count = tk.IntVar(master=root, name='count', value=2)
 self.count.trace_add('write', self.count_changed)
 tk.Label(
 master=root, text='Variables'
 ).grid(row=0, column=0, sticky=tk.E)
 tk.Spinbox(
 master=root,
 from_=2,
 to=MAX_VARS,
 increment=1,
 width=4, # characters
 textvariable=self.count, # triggers a first call to count_changed
 ).grid(row=0, column=1, columnspan=2, sticky=tk.W)
 self.error_label = tk.Label(master=root, text='', foreground='red')
 # Row indices do not need to be contiguous, so choose one that will
 # always be at the bottom - one for the spinbox, max vars, and one for
 # the result row.
 self.error_label.grid(row=1 + MAX_VARS + 1, column=0, columnspan=MAX_VARS + 1)
 root.pack()
 def count_changed(self, name: str, index: str, mode: str) -> None:
 try:
 requested = self.count.get()
 except tk.TclError:
 return
 if 2 <= requested <= MAX_VARS:
 for var in self.variables:
 var.set_result('')
 while len(self.variables) < requested:
 self.grow()
 while len(self.variables) > requested:
 self.shrink()
 def grow(self) -> None:
 for var in self.variables:
 var.grow()
 for cell in var.cells:
 cell.set_separator(rightmost=False)
 var = UIVariable(
 parent=self.root, complain=self.complain,
 index=len(self.variables),
 )
 for cell in var.cells:
 cell.set_separator(rightmost=True)
 self.variables.append(var)
 self.sums.append(UICell(
 parent=self.root, complain=self.complain,
 row=1 + len(self.sums), col=2*MAX_VARS,
 ))
 def shrink(self) -> None:
 self.sums.pop().destroy()
 self.variables.pop().destroy()
 for var in self.variables:
 var.shrink()
 for cell in self.variables[-1].cells:
 cell.set_separator(rightmost=True)
 def complain(self, var_name: str, complaint: Optional[str]) -> None:
 if complaint is None:
 self.complaints.pop(var_name, None)
 else:
 self.complaints[var_name] = complaint
 self.error_text = ', '.join(self.complaints.values())
 if not self.complaints:
 self.solve()
 @property
 def error_text(self) -> str:
 return self.error_label.cget('text')
 @error_text.setter
 def error_text(self, s: str) -> None:
 self.error_label.configure(text=s)
 @property
 def matrix(self) -> Matrix:
 return Matrix([
 [c.value for c in var.cells]
 for var in self.variables
 ]).T
 @property
 def sum_vector(self) -> Matrix:
 return Matrix([c.value for c in self.sums])
 def solve(self) -> None:
 res_set = linsolve((self.matrix, self.sum_vector))
 if not isinstance(res_set, FiniteSet):
 self.error_text = 'No finite set of solutions'
 elif len(res_set) < 1:
 self.error_text = 'No solution found'
 elif len(res_set) > 1:
 self.error_text = 'Non-unique solution space'
 else:
 self.error_text = ''
 result, = res_set
 for var, res in zip(self.variables, result):
 var.set_result(res)
 return
 for var in self.variables:
 var.set_result('')
def main() -> None:
 parent = tk.Tk()
 parent.title('Simultaneous linear equation solver')
 UIFrame(parent)
 parent.mainloop()
if __name__ == '__main__':
 main()

Error highlighting:

errors

Underdetermined systems:

tau

Systems with no solution:

no solution

Solution:

solution

answered Sep 15, 2021 at 16:02
\$\endgroup\$
2
  • \$\begingroup\$ Your answer made me realize... I need to study tkinter in details before trying to do these kinda programs... Thanks a lot.... \$\endgroup\$ Commented Sep 17, 2021 at 6:39
  • 1
    \$\begingroup\$ On the contrary! I think studying while you do these kinda programs is a great way to learn. Basically ask "it would be nice if I could do X. How is that done in tkinter?" \$\endgroup\$ Commented Sep 17, 2021 at 13:40

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.