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()
New contributor
Nothing special is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$
0
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

\$\endgroup\$
1
  • \$\begingroup\$ Your answer made me realize... I need to study tkinter in details before trying to do these kinda programs... Thanks a lot.... \$\endgroup\$ 7 hours ago

Your Answer

Nothing special is a new contributor. Be nice, and check out our Code of Conduct.

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.