Contents

import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
from IPython.display import display, clear_output

class LinearSystemSolverNotebook:
    def __init__(self):
        self.default_eqs = [
            [20, 50, 700],
            [1, 1, 20],
            [50, 20, 700]
        ]
        self.equation_widgets = []
        self.third_eq_shown = False

        # UI elements
        self.output_area = widgets.Output()
        self.plot_area = widgets.Output()
        self.toggle_button = widgets.ToggleButton(
            value=False,
            description='[+]',
            tooltip='Toggle third equation',
            layout=widgets.Layout(width='50px')
        )
        self.toggle_button.observe(self.toggle_third_equation, names='value')

        self.create_equation_rows(2)

        # Display layout
        self.input_area = widgets.VBox(
            self.equation_widgets + [self.toggle_button, self.output_area]
        )

        self.container = widgets.HBox([self.input_area, self.plot_area])
        display(self.container)

        self.update_plot()

    def create_equation_rows(self, count):
        for i in range(count):
            self.add_equation_row(i)

    def add_equation_row(self, idx):
        default = self.default_eqs[idx]
        a = widgets.Text(value=str(default[0]), layout=widgets.Layout(width='60px'))
        c = widgets.Text(value=str(default[1]), layout=widgets.Layout(width='60px'))
        b = widgets.Text(value=str(default[2]), layout=widgets.Layout(width='60px'))
        for box in (a, c, b):
            box.observe(lambda change: self.update_plot(), names='value')

        row = widgets.HBox([
            a, widgets.Label("c +"),
            c, widgets.Label("t ="),
            b
        ])
        row._entries = [a, c, b]
        self.equation_widgets.append(row)

    def toggle_third_equation(self, change):
        if change['new']:
            self.add_equation_row(2)
            self.input_area.children = self.equation_widgets + [self.toggle_button, self.output_area]
            self.toggle_button.description = '[-]'
            self.third_eq_shown = True
        else:
            self.equation_widgets.pop()
            self.input_area.children = self.equation_widgets + [self.toggle_button, self.output_area]
            self.toggle_button.description = '[+]'
            self.third_eq_shown = False
        self.update_plot()

    def parse_equations(self):
        A, b = [], []
        for row in self.equation_widgets:
            a, c, b_ = row._entries
            try:
                a_val = float(a.value) if a.value.strip() else 1.0
                c_val = float(c.value) if c.value.strip() else 1.0
                b_val = float(b_.value) if b_.value.strip() else 1.0
                A.append([a_val, c_val])
                b.append(b_val)
            except ValueError:
                continue
        return np.array(A), np.array(b)

    def update_plot(self):
        A, b = self.parse_equations()

        with self.output_area:
            clear_output()
            if len(A) < 2:
                print("Enter at least 2 valid equations.")
                return

            print("     A         x    =   b")
            for i in range(len(A)):
                a_str = f"[{A[i,0]:>4.0f} {A[i,1]:>4.0f}]"
                x_str = "[c]" if i == 0 else "[t]" if i == 1 else "   "
                b_str = f"[{b[i]:>5.0f}]"
                print(f"{a_str}   {x_str}   {b_str}")

            # Initialize flags
            sol = None
            show_dot = False
            msg = ""
            title_color = 'black'

            try:
                rank_A = np.linalg.matrix_rank(A)
                Ab = np.hstack([A, b.reshape(-1, 1)])
                rank_Ab = np.linalg.matrix_rank(Ab)

                if rank_Ab != rank_A:
                    msg = "System is inconsistent: No solution."
                    title_color = 'red'
                elif rank_A < A.shape[1]:
                    msg = "System is dependent: Infinite solutions."
                    title_color = 'blue'
                else:
                    if A.shape[0] == A.shape[1]:
                        sol = np.linalg.solve(A, b)
                        msg = f"Unique solution: c = {sol[0]:.2f}, t = {sol[1]:.2f}" # Include t in the output
                        show_dot = True
                        title_color = 'green'
                    else:
                        sol, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None)
                        msg = f"Least squares solution: c = {sol[0]:.2f}, t = {sol[1]:.2f}" # Include t in the output
                        show_dot = True
                        title_color = 'gray'
            except Exception as e:
                msg = f"Error solving system: {e}"

            print("\n" + msg)

            try:
                cond = np.linalg.cond(A)
                print(f"Condition number: {cond:.2e}")
            except:
                print("Condition number: N/A")

        # Plotting
        with self.plot_area:
            clear_output(wait=True)
            fig, ax = plt.subplots(figsize=(6, 6))

            # Center on solution if available
            if sol is not None:
                x_center, y_center = sol
                x_min, x_max = x_center - 20, x_center + 20
                y_min, y_max = y_center - 20, y_center + 20
            else:
                x_min, x_max = -40, 40
                y_min, y_max = -40, 40

            x_vals = np.linspace(x_min, x_max, 1000)

            for i in range(len(A)):
                a, c = A[i]
                rhs = b[i]
                label = f"{a:.0f}c + {c:.0f}t = {rhs:.0f}"

                if c != 0:
                    y_vals = (rhs - a * x_vals) / c
                    ax.plot(x_vals, y_vals, label=label)
                elif a != 0:
                    ax.axvline(x=rhs / a, label=label)
                else:
                    ax.axhline(y=rhs, label=label)

            if sol is not None and show_dot:
                ax.plot(*sol, 'ro', label="Solution")

            ax.set_title(msg, color=title_color)
            ax.set_xlim(x_min, x_max)
            ax.set_ylim(y_min, y_max)
            ax.axhline(0, color='black', lw=0.5)
            ax.axvline(0, color='black', lw=0.5)
            ax.set_xlabel("c")
            ax.set_ylabel("t")
            ax.legend()
            plt.grid(True)
            plt.tight_layout()
            plt.show()

LinearSystemSolverNotebook()
<__main__.LinearSystemSolverNotebook at 0x7f24081e6490>