srossi93 commited on
Commit
7bfa00b
Β·
1 Parent(s): 8e64584

feat(cholesky): improve layout and controls for Cholesky decomposition and solving linear systems

Browse files
pages/linear_algebra/02_cholesky.py CHANGED
@@ -4,20 +4,20 @@ import numpy as np
4
 
5
  st.title("Cholesky Decomposition")
6
 
7
- st.markdown(
8
- r"""
9
- For a **symmetric positive definite** matrix $A$, Cholesky gives $A = LL^\top$
10
- where $L$ is lower triangular. The algorithm computes $L$ element by element.
11
- """
12
- )
13
-
14
- # Size selection
15
- col_size, col_preset = st.columns(2)
16
-
17
- with col_size:
 
18
  n = st.radio("Matrix size", [3, 4], horizontal=True)
19
 
20
- with col_preset:
21
  preset = st.selectbox(
22
  "Example matrix",
23
  ["Simple diagonal-dominant", "Correlation matrix", "Random SPD"],
@@ -111,30 +111,6 @@ def cholesky_steps(A):
111
  steps = cholesky_steps(A)
112
  total_steps = len(steps)
113
 
114
- # Buttons
115
- col_prev, col_next, col_reset = st.columns([1, 1, 1])
116
-
117
- with col_prev:
118
- if st.button("← Previous", disabled=st.session_state.chol_step == 0):
119
- st.session_state.chol_step -= 1
120
- st.rerun()
121
-
122
- with col_next:
123
- if st.button("Next β†’", disabled=st.session_state.chol_step >= total_steps):
124
- st.session_state.chol_step += 1
125
- st.rerun()
126
-
127
- with col_reset:
128
- if st.button("Reset"):
129
- st.session_state.chol_step = 0
130
- st.rerun()
131
-
132
- current_step = st.session_state.chol_step
133
-
134
- # Progress
135
- st.progress(current_step / total_steps if total_steps > 0 else 0)
136
- st.caption(f"Step {current_step} of {total_steps}")
137
-
138
 
139
  # Helper to format matrix as LaTeX
140
  def matrix_to_latex(M, highlight=None, computed_mask=None):
@@ -160,93 +136,129 @@ def matrix_to_latex(M, highlight=None, computed_mask=None):
160
  return r"\begin{pmatrix} " + r" \\ ".join(rows) + r" \end{pmatrix}"
161
 
162
 
163
- # Display matrices side by side
164
- st.markdown("---")
165
-
166
- col_A, col_eq, col_L, col_Lt = st.columns([2, 0.5, 2, 2])
167
-
168
- # Get current L state
169
- if current_step == 0:
170
- L_current = np.zeros_like(A)
171
- computed = np.zeros((n, n), dtype=bool)
172
- highlight = None
173
- else:
174
- i, j, formula, val, L_current = steps[current_step - 1]
175
- computed = np.abs(L_current) > 1e-10
176
- # Upper triangle is always "computed" as 0
177
- for ii in range(n):
178
- for jj in range(ii + 1, n):
179
- computed[ii, jj] = True
180
- highlight = (i, j)
181
-
182
- with col_A:
183
- st.markdown("**Matrix $A$:**")
184
- st.latex(f"A = {matrix_to_latex(A)}")
185
-
186
- with col_eq:
187
- st.markdown(" ")
188
- st.latex("=")
189
-
190
- with col_L:
191
- st.markdown("**Cholesky $L$:**")
192
- # Create mask for computed entries
193
- mask = np.zeros((n, n), dtype=bool)
194
- for ii in range(n):
195
- for jj in range(ii + 1, n):
196
- mask[ii, jj] = True # Upper triangle always shown as 0
197
- if current_step > 0:
198
- for step_idx in range(current_step):
199
- si, sj, _, _, _ = steps[step_idx]
200
- mask[si, sj] = True
201
- st.latex(
202
- f"L = {matrix_to_latex(L_current, highlight=highlight, computed_mask=mask)}"
203
- )
204
 
205
- with col_Lt:
206
- st.markdown("**Transpose $L^\\top$:**")
207
- L_T = L_current.T
208
- mask_T = mask.T
209
- highlight_T = (highlight[1], highlight[0]) if highlight else None
210
- st.latex(
211
- f"L^\\top = {matrix_to_latex(L_T, highlight=highlight_T, computed_mask=mask_T)}"
212
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
- # Show current step formula
215
- st.markdown("---")
216
 
217
- if current_step == 0:
218
- st.markdown(
219
- "**Cholesky Algorithm:** For each column $j$, compute elements from top to bottom:"
220
- )
221
- st.latex(
222
- r"L_{jj} = \sqrt{A_{jj} - \sum_{k=1}^{j-1} L_{jk}^2} \qquad L_{ij} = \frac{A_{ij} - \sum_{k=1}^{j-1} L_{ik} L_{jk}}{L_{jj}} \text{ for } i > j"
223
- )
224
- st.info("Click **Next β†’** to start computing elements of $L$")
225
- elif current_step <= total_steps:
226
- i, j, formula, val, _ = steps[current_step - 1]
227
- if i == j:
228
- st.markdown(f"**Computing diagonal element $L_{{{i + 1}{j + 1}}}$:**")
229
  else:
230
- st.markdown(f"**Computing off-diagonal element $L_{{{i + 1}{j + 1}}}$:**")
231
- st.latex(formula + f" = {val:.4f}")
 
 
 
 
 
 
 
 
 
232
 
233
- # Verification at the end
234
- if current_step == total_steps:
235
- st.markdown("---")
236
- st.success("**βœ“ Cholesky decomposition complete!**")
237
- L_final = steps[-1][4]
238
- LLT = L_final @ L_final.T
239
- st.markdown("**Verification:** $LL^\\top = A$")
240
-
241
- col_v1, col_v2, col_v3 = st.columns([2, 0.5, 2])
242
- with col_v1:
243
- st.latex(f"LL^\\top = {matrix_to_latex(LLT)}")
244
- with col_v2:
245
  st.latex("=")
246
- with col_v3:
247
- st.latex(f"A = {matrix_to_latex(A)}")
248
 
249
- # Check numerical accuracy
250
- error = np.max(np.abs(LLT - A))
251
- if error < 1e-10:
252
- st.markdown(f"Maximum error: ${error:.2e}$ βœ“")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  st.title("Cholesky Decomposition")
6
 
7
+ # st.markdown(
8
+ # r"""
9
+ # For a **symmetric positive definite** matrix $A$, Cholesky gives $A = LL^\top$
10
+ # where $L$ is lower triangular. The algorithm computes $L$ element by element.
11
+ # """
12
+ # )
13
+
14
+ # Controls and visualizations layout
15
+ col_controls, col_content = st.columns([1, 3])
16
+
17
+ # LEFT COLUMN: Controls
18
+ with col_controls:
19
  n = st.radio("Matrix size", [3, 4], horizontal=True)
20
 
 
21
  preset = st.selectbox(
22
  "Example matrix",
23
  ["Simple diagonal-dominant", "Correlation matrix", "Random SPD"],
 
111
  steps = cholesky_steps(A)
112
  total_steps = len(steps)
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  # Helper to format matrix as LaTeX
116
  def matrix_to_latex(M, highlight=None, computed_mask=None):
 
136
  return r"\begin{pmatrix} " + r" \\ ".join(rows) + r" \end{pmatrix}"
137
 
138
 
139
+ # Add buttons and progress to left column
140
+ with col_controls:
141
+ st.markdown("---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
+ # Buttons
144
+ col_prev, col_next, col_reset = st.columns([1, 1, 1])
145
+
146
+ with col_prev:
147
+ if st.button(
148
+ "← Prev", disabled=st.session_state.chol_step == 0, use_container_width=True
149
+ ):
150
+ st.session_state.chol_step -= 1
151
+ st.rerun()
152
+
153
+ with col_next:
154
+ if st.button(
155
+ "Next β†’",
156
+ disabled=st.session_state.chol_step >= total_steps,
157
+ use_container_width=True,
158
+ ):
159
+ st.session_state.chol_step += 1
160
+ st.rerun()
161
+
162
+ with col_reset:
163
+ if st.button("Reset", use_container_width=True):
164
+ st.session_state.chol_step = 0
165
+ st.rerun()
166
+
167
+ # Progress
168
+ st.progress(st.session_state.chol_step / total_steps if total_steps > 0 else 0)
169
+ st.caption(f"Step {st.session_state.chol_step} of {total_steps}")
170
+
171
+ # RIGHT COLUMN: Content
172
+ with col_content:
173
+ current_step = st.session_state.chol_step
174
+
175
+ # Display matrices side by side
176
+ st.markdown("---")
177
 
178
+ col_A, col_eq, col_L, col_Lt = st.columns([2, 0.5, 2, 2])
 
179
 
180
+ # Get current L state
181
+ if current_step == 0:
182
+ L_current = np.zeros_like(A)
183
+ computed = np.zeros((n, n), dtype=bool)
184
+ highlight = None
 
 
 
 
 
 
 
185
  else:
186
+ i, j, formula, val, L_current = steps[current_step - 1]
187
+ computed = np.abs(L_current) > 1e-10
188
+ # Upper triangle is always "computed" as 0
189
+ for ii in range(n):
190
+ for jj in range(ii + 1, n):
191
+ computed[ii, jj] = True
192
+ highlight = (i, j)
193
+
194
+ with col_A:
195
+ st.markdown("**Matrix $A$:**")
196
+ st.latex(f"A = {matrix_to_latex(A)}")
197
 
198
+ with col_eq:
199
+ st.markdown("&nbsp;")
 
 
 
 
 
 
 
 
 
 
200
  st.latex("=")
 
 
201
 
202
+ with col_L:
203
+ st.markdown("**Cholesky $L$:**")
204
+ # Create mask for computed entries
205
+ mask = np.zeros((n, n), dtype=bool)
206
+ for ii in range(n):
207
+ for jj in range(ii + 1, n):
208
+ mask[ii, jj] = True # Upper triangle always shown as 0
209
+ if current_step > 0:
210
+ for step_idx in range(current_step):
211
+ si, sj, _, _, _ = steps[step_idx]
212
+ mask[si, sj] = True
213
+ st.latex(
214
+ f"L = {matrix_to_latex(L_current, highlight=highlight, computed_mask=mask)}"
215
+ )
216
+
217
+ with col_Lt:
218
+ st.markdown("**Transpose $L^\\top$:**")
219
+ L_T = L_current.T
220
+ mask_T = mask.T
221
+ highlight_T = (highlight[1], highlight[0]) if highlight else None
222
+ st.latex(
223
+ f"L^\\top = {matrix_to_latex(L_T, highlight=highlight_T, computed_mask=mask_T)}"
224
+ )
225
+
226
+ # Show current step formula
227
+ st.markdown("---")
228
+
229
+ if current_step == 0:
230
+ st.markdown(
231
+ "**Cholesky Algorithm:** For each column $j$, compute elements from top to bottom:"
232
+ )
233
+ st.latex(
234
+ r"L_{jj} = \sqrt{A_{jj} - \sum_{k=1}^{j-1} L_{jk}^2} \qquad L_{ij} = \frac{A_{ij} - \sum_{k=1}^{j-1} L_{ik} L_{jk}}{L_{jj}} \text{ for } i > j"
235
+ )
236
+ st.info("Click **Next β†’** to start computing elements of $L$")
237
+ elif current_step <= total_steps:
238
+ i, j, formula, val, _ = steps[current_step - 1]
239
+ if i == j:
240
+ st.markdown(f"**Computing diagonal element $L_{{{i + 1}{j + 1}}}$:**")
241
+ else:
242
+ st.markdown(f"**Computing off-diagonal element $L_{{{i + 1}{j + 1}}}$:**")
243
+ st.latex(formula + f" = {val:.4f}")
244
+
245
+ # Verification at the end
246
+ if current_step == total_steps:
247
+ st.markdown("---")
248
+ st.success("**βœ“ Cholesky decomposition complete!**")
249
+ # L_final = steps[-1][4]
250
+ # LLT = L_final @ L_final.T
251
+ # st.markdown("**Verification:** $LL^\\top = A$")
252
+
253
+ # col_v1, col_v2, col_v3 = st.columns([2, 0.5, 2])
254
+ # with col_v1:
255
+ # st.latex(f"LL^\\top = {matrix_to_latex(LLT)}")
256
+ # with col_v2:
257
+ # st.latex("=")
258
+ # with col_v3:
259
+ # st.latex(f"A = {matrix_to_latex(A)}")
260
+
261
+ # # Check numerical accuracy
262
+ # error = np.max(np.abs(LLT - A))
263
+ # if error < 1e-10:
264
+ # st.markdown(f"Maximum error: ${error:.2e}$ βœ“")
pages/linear_algebra/03_cholesky_solve.py CHANGED
@@ -4,22 +4,12 @@ import numpy as np
4
 
5
  st.title("Solving Linear Systems with Cholesky")
6
 
7
- st.markdown(
8
- r"""
9
- To solve $A\mathbf{x} = \mathbf{b}$ where $A$ is symmetric positive definite:
10
- 1. Compute Cholesky: $A = LL^\top$
11
- 2. Solve $L\mathbf{y} = \mathbf{b}$ (forward substitution)
12
- 3. Solve $L^\top\mathbf{x} = \mathbf{y}$ (backward substitution)
13
- """
14
- )
15
-
16
- # Size selection
17
- col_size, col_preset = st.columns(2)
18
-
19
- with col_size:
20
- n = st.radio("System size", [3, 4], horizontal=True)
21
 
22
- with col_preset:
 
 
23
  preset = st.selectbox(
24
  "Example",
25
  ["Simple system", "Unit RHS", "Random"],
@@ -29,8 +19,8 @@ with col_preset:
29
  np.random.seed(123)
30
  if preset == "Simple system":
31
  if n == 3:
32
- A = np.array([[4.0, 2.0, 1.0], [2.0, 5.0, 2.0], [1.0, 2.0, 6.0]])
33
- b = np.array([1.0, 2.0, 3.0])
34
  else:
35
  A = np.array(
36
  [
@@ -138,46 +128,58 @@ if st.session_state.solve_n != n or st.session_state.solve_preset != preset:
138
  st.session_state.solve_n = n
139
  st.session_state.solve_preset = preset
140
 
141
- # Buttons
142
- col_prev, col_next, col_reset = st.columns([1, 1, 1])
143
-
144
- with col_prev:
145
- if st.button("← Previous", disabled=st.session_state.solve_step == 0):
146
- st.session_state.solve_step -= 1
147
- st.rerun()
148
-
149
- with col_next:
150
- if st.button("Next β†’", disabled=st.session_state.solve_step >= total_steps):
151
- st.session_state.solve_step += 1
152
- st.rerun()
153
 
154
- with col_reset:
155
- if st.button("Reset"):
156
- st.session_state.solve_step = 0
157
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
  current_step = st.session_state.solve_step
160
 
161
- # Progress
162
- st.progress(current_step / total_steps if total_steps > 0 else 0)
163
-
164
- # Determine phase
165
- if current_step == 0:
166
- phase = "setup"
167
- elif current_step <= total_forward:
168
- phase = "forward"
169
- else:
170
- phase = "backward"
171
-
172
- if phase == "setup":
173
- st.caption("Step 0: Setup")
174
- elif phase == "forward":
175
- st.caption(f"Forward substitution: Step {current_step} of {total_forward}")
176
- else:
177
- st.caption(
178
- f"Backward substitution: Step {current_step - total_forward} of {total_backward}"
179
- )
180
-
181
 
182
  # Helper to format vector as LaTeX
183
  def vector_to_latex(v, highlight=None, computed_mask=None):
@@ -204,115 +206,96 @@ def matrix_to_latex(M):
204
  return r"\begin{pmatrix} " + r" \\ ".join(rows) + r" \end{pmatrix}"
205
 
206
 
207
- # Display system
208
- st.markdown("---")
209
-
210
- # Show the problem setup
211
- col1, col2, col3 = st.columns([3, 3, 3])
212
-
213
- with col1:
214
- st.markdown("**System $A\\mathbf{x} = \\mathbf{b}$:**")
215
- st.latex(f"A = {matrix_to_latex(A)}")
216
- st.latex(f"\\mathbf{{b}} = {vector_to_latex(b)}")
217
-
218
- with col2:
219
- st.markdown("**Cholesky factor $L$:**")
220
- st.latex(f"L = {matrix_to_latex(L)}")
221
-
222
- # Compute current state of y and x
223
- if current_step == 0:
224
- y_current = np.zeros(n)
225
- x_current = np.zeros(n)
226
- y_mask = np.zeros(n, dtype=bool)
227
- x_mask = np.zeros(n, dtype=bool)
228
- y_highlight = None
229
- x_highlight = None
230
- elif current_step <= total_forward:
231
- # In forward substitution
232
- _, _, _, y_current = forward_steps[current_step - 1]
233
- x_current = np.zeros(n)
234
- y_mask = np.zeros(n, dtype=bool)
235
- for s in range(current_step):
236
- idx = forward_steps[s][0]
237
- y_mask[idx] = True
238
- x_mask = np.zeros(n, dtype=bool)
239
- y_highlight = forward_steps[current_step - 1][0]
240
- x_highlight = None
241
- else:
242
- # In backward substitution
243
- y_current = y_final.copy()
244
- backward_idx = current_step - total_forward - 1
245
- _, _, _, x_current = backward_steps[backward_idx]
246
- y_mask = np.ones(n, dtype=bool)
247
- x_mask = np.zeros(n, dtype=bool)
248
- for s in range(backward_idx + 1):
249
- idx = backward_steps[s][0]
250
- x_mask[idx] = True
251
- y_highlight = None
252
- x_highlight = backward_steps[backward_idx][0]
253
-
254
- with col3:
255
- st.markdown("**Intermediate $\\mathbf{y}$ and solution $\\mathbf{x}$:**")
256
- col_y, col_x = st.columns(2)
257
- with col_y:
258
- st.latex(
259
- f"\\mathbf{{y}} = {vector_to_latex(y_current, highlight=y_highlight, computed_mask=y_mask)}"
260
- )
261
- with col_x:
262
- st.latex(
263
- f"\\mathbf{{x}} = {vector_to_latex(x_current, highlight=x_highlight, computed_mask=x_mask)}"
264
- )
265
 
266
- # Show current computation
267
- st.markdown("---")
 
 
268
 
269
- if phase == "setup":
270
- st.markdown("**Step 1: Forward Substitution** β€” Solve $L\\mathbf{y} = \\mathbf{b}$")
271
- st.latex(f"{matrix_to_latex(L)} \\mathbf{{y}} = {vector_to_latex(b)}")
272
- st.markdown(
273
- "Start from the top row and work down. Each $y_i$ depends only on previously computed values."
274
- )
275
- st.info("Click **Next β†’** to begin forward substitution")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
- elif phase == "forward":
278
- step_idx = current_step - 1
279
- i, formula, val, _ = forward_steps[step_idx]
280
- st.markdown(f"**Forward substitution:** Computing $y_{{{i + 1}}}$")
281
- st.latex(formula + f" = {val:.4f}")
282
 
283
- if current_step == total_forward:
284
- st.success("βœ“ Forward substitution complete!")
285
  st.markdown(
286
- "**Next:** Backward substitution to solve $L^\\top \\mathbf{x} = \\mathbf{y}$"
287
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
- else: # backward
290
- step_idx = current_step - total_forward - 1
291
- i, formula, val, _ = backward_steps[step_idx]
292
- st.markdown(f"**Backward substitution:** Computing $x_{{{i + 1}}}$")
293
- st.latex(formula + f" = {val:.4f}")
294
-
295
- # Final verification
296
- if current_step == total_steps:
297
- st.markdown("---")
298
- st.success("**βœ“ System solved!**")
299
-
300
- x_final = backward_steps[-1][3]
301
- Ax = A @ x_final
302
-
303
- st.markdown("**Verification:** $A\\mathbf{x} = \\mathbf{b}$")
304
-
305
- col_v1, col_v2, col_v3, col_v4, col_v5 = st.columns([2, 0.3, 1.5, 0.3, 1.5])
306
- with col_v1:
307
- st.latex(f"A\\mathbf{{x}} = {matrix_to_latex(A)} {vector_to_latex(x_final)}")
308
- with col_v2:
309
- st.latex("=")
310
- with col_v3:
311
- st.latex(f"{vector_to_latex(Ax)}")
312
- with col_v4:
313
- st.latex("=")
314
- with col_v5:
315
- st.latex(f"\\mathbf{{b}} = {vector_to_latex(b)}")
316
 
317
- error = np.max(np.abs(Ax - b))
318
- st.markdown(f"Maximum error: ${error:.2e}$ βœ“")
 
 
 
4
 
5
  st.title("Solving Linear Systems with Cholesky")
6
 
7
+ # Create layout
8
+ col_controls, col_content = st.columns([1, 3])
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ # LEFT COLUMN: Controls
11
+ with col_controls:
12
+ n = st.radio("System size", [3, 4], horizontal=True)
13
  preset = st.selectbox(
14
  "Example",
15
  ["Simple system", "Unit RHS", "Random"],
 
19
  np.random.seed(123)
20
  if preset == "Simple system":
21
  if n == 3:
22
+ A = np.array([[1.0, 1.0, 1.0], [1.0, 2.0, 2.0], [1.0, 2.0, 3.0]])
23
+ b = np.array([3.0, 5.0, 6.0])
24
  else:
25
  A = np.array(
26
  [
 
128
  st.session_state.solve_n = n
129
  st.session_state.solve_preset = preset
130
 
131
+ # Continue LEFT COLUMN: Add buttons and progress
132
+ with col_controls:
133
+ st.markdown("---")
 
 
 
 
 
 
 
 
 
134
 
135
+ # Buttons
136
+ col_prev, col_next, col_reset = st.columns([1, 1, 1])
137
+
138
+ with col_prev:
139
+ if st.button(
140
+ "← Prev",
141
+ disabled=st.session_state.solve_step == 0,
142
+ use_container_width=True,
143
+ ):
144
+ st.session_state.solve_step -= 1
145
+ st.rerun()
146
+
147
+ with col_next:
148
+ if st.button(
149
+ "Next β†’",
150
+ disabled=st.session_state.solve_step >= total_steps,
151
+ use_container_width=True,
152
+ ):
153
+ st.session_state.solve_step += 1
154
+ st.rerun()
155
+
156
+ with col_reset:
157
+ if st.button("Reset", use_container_width=True):
158
+ st.session_state.solve_step = 0
159
+ st.rerun()
160
+
161
+ # Progress
162
+ st.progress(st.session_state.solve_step / total_steps if total_steps > 0 else 0)
163
+
164
+ # Caption
165
+ phase = (
166
+ "setup"
167
+ if st.session_state.solve_step == 0
168
+ else ("forward" if st.session_state.solve_step <= total_forward else "backward")
169
+ )
170
+ if phase == "setup":
171
+ st.caption("Step 0: Setup")
172
+ elif phase == "forward":
173
+ st.caption(
174
+ f"Forward substitution: Step {st.session_state.solve_step} of {total_forward}"
175
+ )
176
+ else:
177
+ st.caption(
178
+ f"Backward substitution: Step {st.session_state.solve_step - total_forward} of {total_backward}"
179
+ )
180
 
181
  current_step = st.session_state.solve_step
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
  # Helper to format vector as LaTeX
185
  def vector_to_latex(v, highlight=None, computed_mask=None):
 
206
  return r"\begin{pmatrix} " + r" \\ ".join(rows) + r" \end{pmatrix}"
207
 
208
 
209
+ # RIGHT COLUMN: Content
210
+ with col_content:
211
+ # Show the problem setup
212
+ col1, col2, col3 = st.columns([3, 3, 3])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
+ with col1:
215
+ st.markdown("**System $A\\mathbf{x} = \\mathbf{b}$:**")
216
+ st.latex(f"A = {matrix_to_latex(A)}")
217
+ st.latex(f"\\mathbf{{b}} = {vector_to_latex(b)}")
218
 
219
+ with col2:
220
+ st.markdown("**Cholesky factor $L$:**")
221
+ st.latex(f"L = {matrix_to_latex(L)}")
222
+
223
+ # Compute current state of y and x
224
+ if current_step == 0:
225
+ y_current = np.zeros(n)
226
+ x_current = np.zeros(n)
227
+ y_mask = np.zeros(n, dtype=bool)
228
+ x_mask = np.zeros(n, dtype=bool)
229
+ y_highlight = None
230
+ x_highlight = None
231
+ elif current_step <= total_forward:
232
+ # In forward substitution
233
+ _, _, _, y_current = forward_steps[current_step - 1]
234
+ x_current = np.zeros(n)
235
+ y_mask = np.zeros(n, dtype=bool)
236
+ for s in range(current_step):
237
+ idx = forward_steps[s][0]
238
+ y_mask[idx] = True
239
+ x_mask = np.zeros(n, dtype=bool)
240
+ y_highlight = forward_steps[current_step - 1][0]
241
+ x_highlight = None
242
+ else:
243
+ # In backward substitution
244
+ y_current = y_final.copy()
245
+ backward_idx = current_step - total_forward - 1
246
+ _, _, _, x_current = backward_steps[backward_idx]
247
+ y_mask = np.ones(n, dtype=bool)
248
+ x_mask = np.zeros(n, dtype=bool)
249
+ for s in range(backward_idx + 1):
250
+ idx = backward_steps[s][0]
251
+ x_mask[idx] = True
252
+ y_highlight = None
253
+ x_highlight = backward_steps[backward_idx][0]
254
+
255
+ with col3:
256
+ st.markdown("**Intermediate $\\mathbf{y}$ and solution $\\mathbf{x}$:**")
257
+ col_y, col_x = st.columns(2)
258
+ with col_y:
259
+ st.latex(
260
+ f"\\mathbf{{y}} = {vector_to_latex(y_current, highlight=y_highlight, computed_mask=y_mask)}"
261
+ )
262
+ with col_x:
263
+ st.latex(
264
+ f"\\mathbf{{x}} = {vector_to_latex(x_current, highlight=x_highlight, computed_mask=x_mask)}"
265
+ )
266
 
267
+ # Show current computation
268
+ st.markdown("---")
 
 
 
269
 
270
+ if phase == "setup":
 
271
  st.markdown(
272
+ "**Step 1: Forward Substitution** β€” Solve $L\\mathbf{y} = \\mathbf{b}$"
273
  )
274
+ st.latex(f"{matrix_to_latex(L)} \\mathbf{{y}} = {vector_to_latex(b)}")
275
+ st.markdown(
276
+ "Start from the top row and work down. Each $y_i$ depends only on previously computed values."
277
+ )
278
+ st.info("Click **Next β†’** to begin forward substitution")
279
+
280
+ elif phase == "forward":
281
+ step_idx = current_step - 1
282
+ i, formula, val, _ = forward_steps[step_idx]
283
+ st.markdown(f"**Forward substitution:** Computing $y_{{{i + 1}}}$")
284
+ st.latex(formula + f" = {val:.4f}")
285
+
286
+ if current_step == total_forward:
287
+ st.success("βœ“ Forward substitution complete!")
288
+ st.markdown(
289
+ "**Next:** Backward substitution to solve $L^\\top \\mathbf{x} = \\mathbf{y}$"
290
+ )
291
 
292
+ else: # backward
293
+ step_idx = current_step - total_forward - 1
294
+ i, formula, val, _ = backward_steps[step_idx]
295
+ st.markdown(f"**Backward substitution:** Computing $x_{{{i + 1}}}$")
296
+ st.latex(formula + f" = {val:.4f}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
 
298
+ # Final verification
299
+ if current_step == total_steps:
300
+ st.markdown("---")
301
+ st.success("**βœ“ System solved!**")