%23%20%2F%2F%2F%20script%0A%23%20requires-python%20%3D%20%22%3E%3D3.10%22%0A%23%20dependencies%20%3D%20%5B%0A%23%20%20%20%22marimo%22%2C%0A%23%20%20%20%22torch%22%2C%0A%23%20%20%20%22numpy%22%2C%0A%23%20%20%20%22plotly%22%2C%0A%23%20%20%20%22parx%20%40%20file%3A%2F%2F%2Fhome%2Fjohan%2FDocuments%2Fphd%2Fparx%22%2C%0A%23%20%5D%0A%23%20%2F%2F%2F%0A%0Aimport%20marimo%0A%0A__generated_with%20%3D%20%220.23.8%22%0Aapp%20%3D%20marimo.App(width%3D%22medium%22)%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20marimo%20as%20mo%0A%0A%20%20%20%20return%20(mo%2C)%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20%23%20juliacall%20must%20be%20imported%20before%20torch%20%E2%80%94%20parx%20handles%20this%20at%20import%20time%0A%20%20%20%20import%20parx%0A%0A%20%20%20%20import%20copy%0A%20%20%20%20import%20tempfile%0A%0A%20%20%20%20import%20numpy%20as%20np%0A%20%20%20%20import%20torch%0A%20%20%20%20import%20torch.nn%20as%20nn%0A%0A%20%20%20%20from%20parx%20import%20(%0A%20%20%20%20%20%20%20%20compute_partition%2C%0A%20%20%20%20%20%20%20%20complexity_profile%2C%0A%20%20%20%20%20%20%20%20dead_neurons%2C%0A%20%20%20%20%20%20%20%20region_size_summary%2C%0A%20%20%20%20%20%20%20%20animate_epochs%2C%0A%20%20%20%20%20%20%20%20save_partition%2C%0A%20%20%20%20%20%20%20%20load_partition%2C%0A%20%20%20%20)%0A%20%20%20%20from%20parx.viz%20import%20(%0A%20%20%20%20%20%20%20%20plot_partition_2d%2C%0A%20%20%20%20%20%20%20%20plot_region_counts%2C%0A%20%20%20%20%20%20%20%20plot_halfspaces%2C%0A%20%20%20%20%20%20%20%20plot_partition_slice%2C%0A%20%20%20%20%20%20%20%20plot_partition_projection%2C%0A%20%20%20%20%20%20%20%20affine_spectral%2C%0A%20%20%20%20)%0A%20%20%20%20from%20parx.verify%20import%20(%0A%20%20%20%20%20%20%20%20check_no_overlaps%2C%0A%20%20%20%20%20%20%20%20check_covers_space%2C%0A%20%20%20%20%20%20%20%20check_regions_nonempty%2C%0A%20%20%20%20)%0A%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20affine_spectral%2C%0A%20%20%20%20%20%20%20%20animate_epochs%2C%0A%20%20%20%20%20%20%20%20check_covers_space%2C%0A%20%20%20%20%20%20%20%20check_no_overlaps%2C%0A%20%20%20%20%20%20%20%20check_regions_nonempty%2C%0A%20%20%20%20%20%20%20%20complexity_profile%2C%0A%20%20%20%20%20%20%20%20compute_partition%2C%0A%20%20%20%20%20%20%20%20copy%2C%0A%20%20%20%20%20%20%20%20dead_neurons%2C%0A%20%20%20%20%20%20%20%20load_partition%2C%0A%20%20%20%20%20%20%20%20nn%2C%0A%20%20%20%20%20%20%20%20np%2C%0A%20%20%20%20%20%20%20%20parx%2C%0A%20%20%20%20%20%20%20%20plot_halfspaces%2C%0A%20%20%20%20%20%20%20%20plot_partition_2d%2C%0A%20%20%20%20%20%20%20%20plot_partition_projection%2C%0A%20%20%20%20%20%20%20%20plot_partition_slice%2C%0A%20%20%20%20%20%20%20%20plot_region_counts%2C%0A%20%20%20%20%20%20%20%20region_size_summary%2C%0A%20%20%20%20%20%20%20%20save_partition%2C%0A%20%20%20%20%20%20%20%20tempfile%2C%0A%20%20%20%20%20%20%20%20torch%2C%0A%20%20%20%20)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20parx%20quick%20tour%20%E2%80%94%20Plotly%20backend%0A%0A%20%20%20%20A%20ReLU%20network%20is%20piecewise-affine%3A%20fix%20which%20neurons%20fire%20(its%0A%20%20%20%20%22activation%20pattern%22)%20and%20the%20rest%20of%20the%20function%20is%20just%20an%20affine%20map%0A%20%20%20%20%60f(x)%20%3D%20A%20%40%20x%20%2B%20b%60.%20%60parx%60%20**exactly%20enumerates**%20these%20pieces%20%E2%80%94%20the%0A%20%20%20%20maximal%20convex%20%22linear%20regions%22%20of%20input%20space%20on%20which%20the%20activation%0A%20%20%20%20pattern%2C%20and%20therefore%20the%20affine%20map%2C%20is%20constant.%0A%0A%20%20%20%20This%20matters%20for%20a%20few%20reasons%3A%0A%0A%20%20%20%20-%20**Interpretability**%20%E2%80%94%20the%20affine%20map%20on%20any%20single%20region%20tells%20you%0A%20%20%20%20%20%20exactly%20what%20the%20network%20computes%20there%2C%20with%20no%20approximation.%0A%20%20%20%20-%20**Complexity%20%2F%20expressivity**%20%E2%80%94%20the%20number%20of%20regions%20is%20a%20standard%0A%20%20%20%20%20%20proxy%20for%20how%20expressive%20a%20network%20is%3B%20%60parx%60%20can%20track%20it%20across%0A%20%20%20%20%20%20training%20checkpoints.%0A%20%20%20%20-%20**Verification**%20%E2%80%94%20geometric%20properties%20(region%20volume%2C%20boundedness%2C%0A%20%20%20%20%20%20Lipschitz%20constant%20per%20region)%20are%20computable%20exactly%20rather%20than%0A%20%20%20%20%20%20estimated%2C%20and%20%60parx.verify%60%20can%20sanity-check%20the%20result%20itself.%0A%0A%20%20%20%20This%20notebook%20walks%20the%20public%20Python%20API%20end-to-end%20using%20a%20single%0A%20%20%20%20running%20example%3A%20a%20small%20classifier%20trained%20on%20the%20**two%20moons**%0A%20%20%20%20dataset.%20We%20compute%20both%20the%20**sparse**%20and%20**exact**%20partitions%0A%20%20%20%20**before%2C%20during%2C%20and%20after%20training**%2C%20look%20at%20the%20partition%20**layer%20by%0A%20%20%20%20layer**%2C%20animate%20the%20whole%20training%20run%2C%20then%20continue%20into%20the%20rest%20of%0A%20%20%20%20the%20API%20%E2%80%94%20inspecting%20a%20%60Partition%60%2C%20running%20analysis%2Fverification%0A%20%20%20%20functions%2C%20visualizing%20a%20higher-dimensional%20example%2C%20and%20saving%2Freloading%0A%20%20%20%20a%20partition.%20All%20figures%20use%20%60parx.viz%60's%20**Plotly%20backend**%0A%20%20%20%20(%60backend%3D%22plotly%22%60%2C%20the%20default)%20%E2%80%94%20interactive%20figures%20with%20hover%0A%20%20%20%20tooltips%20and%2C%20for%20the%20epoch%20animation%2C%20a%20play%20button%20and%20slider.%0A%0A%20%20%20%20**Run%20interactively%3A**%20%60uv%20run%20notebooks%2Fdemo_plotly.py%60%20opens%20this%0A%20%20%20%20notebook%20in%20marimo's%20editor%20with%20both%20code%20and%20outputs%20visible%20(the%0A%20%20%20%20inline%20%60%23%20%2F%2F%2F%20script%60%20block%20at%20the%20top%20declares%20its%20dependencies%2C%20so%0A%20%20%20%20%60uv%60%20builds%20an%20isolated%20environment%20automatically).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20The%20dataset%3A%20two%20interleaving%20moons%0A%0A%20%20%20%20Two%20interleaving%20half-circles%20of%20points%2C%20each%20labelled%20by%20which%20moon%20it%0A%20%20%20%20belongs%20to%20%E2%80%94%20a%20standard%20nonlinear%20classification%20benchmark%3A%20no%20straight%0A%20%20%20%20line%20separates%20the%20classes%2C%20so%20a%20network%20needs%20at%20least%20one%20hidden%20layer%0A%20%20%20%20to%20fit%20it.%20Generated%20here%20with%20plain%20NumPy%20(no%20extra%20dependency).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(np)%3A%0A%20%20%20%20def%20make_two_moons(n_samples%3D240%2C%20noise%3D0.15%2C%20seed%3D0)%3A%0A%20%20%20%20%20%20%20%20rng%20%3D%20np.random.default_rng(seed)%0A%20%20%20%20%20%20%20%20n0%20%3D%20n_samples%20%2F%2F%202%0A%20%20%20%20%20%20%20%20n1%20%3D%20n_samples%20-%20n0%0A%20%20%20%20%20%20%20%20theta0%20%3D%20rng.uniform(0%2C%20np.pi%2C%20n0)%0A%20%20%20%20%20%20%20%20upper%20%3D%20np.stack(%5Bnp.cos(theta0)%2C%20np.sin(theta0)%5D%2C%20axis%3D1)%0A%20%20%20%20%20%20%20%20theta1%20%3D%20rng.uniform(0%2C%20np.pi%2C%20n1)%0A%20%20%20%20%20%20%20%20lower%20%3D%20np.stack(%5B1%20-%20np.cos(theta1)%2C%201%20-%20np.sin(theta1)%20-%200.5%5D%2C%20axis%3D1)%0A%20%20%20%20%20%20%20%20pts%20%3D%20np.concatenate(%5Bupper%2C%20lower%5D%2C%20axis%3D0)%0A%20%20%20%20%20%20%20%20pts%20%3D%20pts%20%2B%20rng.normal(0.0%2C%20noise%2C%20size%3Dpts.shape)%0A%20%20%20%20%20%20%20%20labels%20%3D%20np.concatenate(%5Bnp.zeros(n0)%2C%20np.ones(n1)%5D)%0A%20%20%20%20%20%20%20%20perm%20%3D%20rng.permutation(n_samples)%0A%20%20%20%20%20%20%20%20return%20pts%5Bperm%5D.astype(np.float32)%2C%20labels%5Bperm%5D.astype(np.float32)%0A%0A%20%20%20%20X_moons%2C%20y_moons%20%3D%20make_two_moons()%0A%20%20%20%20return%20X_moons%2C%20y_moons%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20The%20network%3A%20a%20small%20classifier%0A%0A%20%20%20%20A%202-input%20MLP%20with%20two%20ReLU%20hidden%20layers%20of%20width%208%20(16%20neurons%20total)%0A%20%20%20%20and%20a%20single%20logit%20output%20%E2%80%94%20small%20enough%20to%20enumerate%20exactly%20in%0A%20%20%20%20milliseconds%2C%20expressive%20enough%20to%20fit%20the%20moons.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(nn%2C%20torch)%3A%0A%20%20%20%20torch.manual_seed(0)%0A%20%20%20%20model%20%3D%20nn.Sequential(%0A%20%20%20%20%20%20%20%20nn.Linear(2%2C%208)%2C%20nn.ReLU()%2C%0A%20%20%20%20%20%20%20%20nn.Linear(8%2C%208)%2C%20nn.ReLU()%2C%0A%20%20%20%20%20%20%20%20nn.Linear(8%2C%201)%2C%0A%20%20%20%20)%0A%20%20%20%20return%20(model%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Training%20with%20checkpoints%0A%0A%20%20%20%20%60parx%60%20doesn't%20run%20any%20training%20itself%20%E2%80%94%20the%20caller%20trains%20however%20it%0A%20%20%20%20likes%20and%20hands%20a%20%60state_dict%60%20(or%20several)%20to%20%60compute_partition%60.%20Here%0A%20%20%20%20we%20train%20the%20classifier%20for%20real%20with%20Adam%20%2B%20BCE%20loss%2C%20and%20save%20three%0A%20%20%20%20checkpoints%3A%20the%20random%20**initialization**%20(%22before%22)%2C%20an%20early%2C%0A%20%20%20%20partially-fit%20epoch%20(%22during%22)%2C%20and%20the%20final%2C%20converged%20network%0A%20%20%20%20(%22after%22).%20Everything%20below%20computes%20both%20partitioning%20methods%20for%20all%0A%20%20%20%20three.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(X_moons%2C%20copy%2C%20mo%2C%20model%2C%20nn%2C%20torch%2C%20y_moons)%3A%0A%20%20%20%20_Xt%20%3D%20torch.tensor(X_moons)%0A%20%20%20%20_yt%20%3D%20torch.tensor(y_moons).unsqueeze(1)%0A%20%20%20%20_opt%20%3D%20torch.optim.Adam(model.parameters()%2C%20lr%3D0.05)%0A%20%20%20%20_loss_fn%20%3D%20nn.BCEWithLogitsLoss()%0A%0A%20%20%20%20n_epochs%20%3D%20300%0A%20%20%20%20mid_epoch%20%3D%2010%0A%20%20%20%20checkpoints%20%3D%20%7B%22before%22%3A%20copy.deepcopy(model.state_dict())%7D%0A%20%20%20%20for%20_epoch%20in%20range(n_epochs)%3A%0A%20%20%20%20%20%20%20%20_opt.zero_grad()%0A%20%20%20%20%20%20%20%20_out%20%3D%20model(_Xt)%0A%20%20%20%20%20%20%20%20_loss%20%3D%20_loss_fn(_out%2C%20_yt)%0A%20%20%20%20%20%20%20%20_loss.backward()%0A%20%20%20%20%20%20%20%20_opt.step()%0A%20%20%20%20%20%20%20%20if%20_epoch%20%3D%3D%20mid_epoch%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20checkpoints%5B%22during%22%5D%20%3D%20copy.deepcopy(model.state_dict())%0A%20%20%20%20checkpoints%5B%22after%22%5D%20%3D%20copy.deepcopy(model.state_dict())%0A%0A%20%20%20%20with%20torch.no_grad()%3A%0A%20%20%20%20%20%20%20%20final_acc%20%3D%20((model(_Xt)%20%3E%200).float()%20%3D%3D%20_yt).float().mean().item()%0A%0A%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20Trained%20for%20**%7Bn_epochs%7D**%20epochs%20(Adam%2C%20BCE%20loss)%20%E2%86%92%20final%20training%0A%20%20%20%20accuracy%20**%7Bfinal_acc%3A.1%25%7D**.%20Checkpoints%20captured%20at%20epoch%20**0**%0A%20%20%20%20(before)%2C%20epoch%20**%7Bmid_epoch%7D**%20(during)%2C%20and%20epoch%20**%7Bn_epochs%7D**%0A%20%20%20%20(after).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%20checkpoints%2C%20mid_epoch%2C%20n_epochs%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Sparse%20vs.%20exact%2C%20at%20every%20checkpoint%0A%0A%20%20%20%20One%20shared%20sample%20grid%20%E2%80%94%20a%20padded%20box%20around%20the%20moons'%20bounding%20box%20%E2%80%94%0A%20%20%20%20is%20handed%20to%20both%20%60%22sparse_julia%22%60%20(scans%20the%20sample%20for%20distinct%0A%20%20%20%20activation%20patterns)%20and%20%60%22exact_julia_fast%22%60%20(a%20complete%20DFS%20%2B%0A%20%20%20%20facet-flip%20enumeration)%20for%20each%20of%20the%20three%20checkpoints.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(X_moons%2C%20np)%3A%0A%20%20%20%20rng%20%3D%20np.random.default_rng(1)%0A%20%20%20%20_pad%20%3D%200.6%0A%20%20%20%20x_range%20%3D%20(float(X_moons%5B%3A%2C%200%5D.min()%20-%20_pad)%2C%20float(X_moons%5B%3A%2C%200%5D.max()%20%2B%20_pad))%0A%20%20%20%20y_range%20%3D%20(float(X_moons%5B%3A%2C%201%5D.min()%20-%20_pad)%2C%20float(X_moons%5B%3A%2C%201%5D.max()%20%2B%20_pad))%0A%20%20%20%20domain%20%3D%20(x_range%2C%20y_range)%0A%20%20%20%20X%20%3D%20rng.uniform(%0A%20%20%20%20%20%20%20%20%5Bx_range%5B0%5D%2C%20y_range%5B0%5D%5D%2C%20%5Bx_range%5B1%5D%2C%20y_range%5B1%5D%5D%2C%20size%3D(1500%2C%202)%0A%20%20%20%20)%0A%20%20%20%20return%20X%2C%20domain%2C%20rng%0A%0A%0A%40app.cell%0Adef%20_(X%2C%20checkpoints%2C%20compute_partition%2C%20mo%2C%20parx)%3A%0A%20%20%20%20with%20mo.status.spinner(title%3D%22Computing%20sparse%20%26%20exact%20partitions%20for%20each%20checkpoint%20%E2%80%A6%22)%3A%0A%20%20%20%20%20%20%20%20partitions_sparse%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20stage%3A%20compute_partition(sd%2C%20X%2C%20method%3D%22sparse_julia%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20stage%2C%20sd%20in%20checkpoints.items()%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20partitions_exact%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20stage%3A%20compute_partition(sd%2C%20X%2C%20method%3D%22exact_julia_fast%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20stage%2C%20sd%20in%20checkpoints.items()%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20_rows%20%3D%20%22%5Cn%22.join(%0A%20%20%20%20%20%20%20%20f%22%7C%20%7Bstage%7D%20%7C%20%7Blen(partitions_sparse%5Bstage%5D)%7D%20%7C%20%7Blen(partitions_exact%5Bstage%5D)%7D%20%7C%22%0A%20%20%20%20%20%20%20%20for%20stage%20in%20(%22before%22%2C%20%22during%22%2C%20%22after%22)%0A%20%20%20%20)%0A%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20%60parx.list_methods()%60%20%E2%86%92%20%60%7Bparx.list_methods()%7D%60%0A%0A%20%20%20%20Same%20%7BX.shape%5B0%5D%7D-point%20sample%20grid%2C%20two%20methods%2C%20three%20checkpoints%3A%0A%0A%20%20%20%20%7C%20checkpoint%20%7C%20%60sparse_julia%60%20regions%20%7C%20%60exact_julia_fast%60%20regions%20%7C%0A%20%20%20%20%7C---%7C---%7C---%7C%0A%20%20%20%20%7B_rows%7D%0A%0A%20%20%20%20The%20sparse%20scan%20consistently%20finds%20fewer%20regions%20than%20the%20exact%0A%20%20%20%20enumeration%20%E2%80%94%20it%20can%20only%20report%20activation%20patterns%20that%20some%20sampled%0A%20%20%20%20point%20actually%20lands%20in%2C%20while%20%60exact_julia_fast%60%20enumerates%20every%0A%20%20%20%20region%20by%20construction%2C%20regardless%20of%20how%20densely%20the%20input%20space%0A%20%20%20%20happens%20to%20be%20sampled.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%20partitions_exact%2C%20partitions_sparse%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20rest%20of%20this%20notebook%20uses%20the%20fully-trained%20(%22after%22)%20exact%0A%20%20%20%20partition%20as%20its%20running%20example.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(partitions_exact)%3A%0A%20%20%20%20partition%20%3D%20partitions_exact%5B%22after%22%5D%0A%20%20%20%20return%20(partition%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Multiple%20layers%3A%20how%20depth%20builds%20the%20partition%0A%0A%20%20%20%20%60plot_partition_2d%60's%20%60layer%3D%60%20argument%20collapses%20every%20leaf%20region%20to%0A%20%20%20%20its%20activation-path%20*prefix*%20of%20that%20length%2C%20showing%20the%20coarser%0A%20%20%20%20partition%20induced%20by%20only%20the%20first%20%60layer%60%20ReLU%20layers.%20Below%3A%20the%0A%20%20%20%20trained%20network's%20partition%20after%20just%20the%20first%20hidden%20layer%0A%20%20%20%20(%60layer%3D1%60%2C%20one%20arrangement%20of%208%20hyperplanes)%20versus%20the%20full%20two-layer%0A%20%20%20%20partition%20(%60layer%3D2%60)%20%E2%80%94%20the%20second%20layer%20visibly%20folds%20the%20coarse%20wedges%0A%20%20%20%20into%20a%20much%20finer%20arrangement%20that%20hugs%20the%20moons'%20decision%20boundary.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(X_moons%2C%20domain%2C%20go%2C%20mo%2C%20partition%2C%20plot_partition_2d%2C%20y_moons)%3A%0A%20%20%20%20_figs%20%3D%20%5B%5D%0A%20%20%20%20for%20_layer%20in%20(1%2C%202)%3A%0A%20%20%20%20%20%20%20%20_fig%20%3D%20plot_partition_2d(%0A%20%20%20%20%20%20%20%20%20%20%20%20partition%2C%20domain%3Ddomain%2C%20layer%3D_layer%2C%20backend%3D%22plotly%22%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_fig.add_trace(%0A%20%20%20%20%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x%3DX_moons%5B%3A%2C%200%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20y%3DX_moons%5B%3A%2C%201%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22markers%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20marker%3Ddict(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20color%3Dy_moons%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20colorscale%3D%5B%5B0%2C%20%22%233b4cc0%22%5D%2C%20%5B1%2C%20%22%23b40426%22%5D%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20size%3D6%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20line%3Ddict(width%3D0.5%2C%20color%3D%22black%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20showlegend%3DFalse%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20hoverinfo%3D%22skip%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_figs.append(_fig)%0A%20%20%20%20mo.hstack(_figs)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Watching%20the%20partition%20emerge%20across%20training%0A%0A%20%20%20%20Same%20domain%2C%20same%20coloring%2C%20one%20exact%20partition%20per%20checkpoint%20%E2%80%94%0A%20%20%20%20scattered%20on%20top%20are%20the%20moons%20themselves.%20At%20initialization%20the%0A%20%20%20%20boundaries%20are%20unrelated%20to%20the%20data%3B%20by%20the%20end%20they've%20organized%0A%20%20%20%20themselves%20densely%20around%20the%20decision%20boundary%20between%20the%20two%0A%20%20%20%20classes.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20X_moons%2C%0A%20%20%20%20domain%2C%0A%20%20%20%20go%2C%0A%20%20%20%20mid_epoch%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20n_epochs%2C%0A%20%20%20%20partitions_exact%2C%0A%20%20%20%20plot_partition_2d%2C%0A%20%20%20%20y_moons%2C%0A)%3A%0A%20%20%20%20_labels%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%22before%22%3A%20%22before%20training%20(random%20init)%22%2C%0A%20%20%20%20%20%20%20%20%22during%22%3A%20f%22during%20training%20(epoch%20%7Bmid_epoch%7D)%22%2C%0A%20%20%20%20%20%20%20%20%22after%22%3A%20f%22after%20training%20(epoch%20%7Bn_epochs%7D%2C%20converged)%22%2C%0A%20%20%20%20%7D%0A%20%20%20%20_figs%20%3D%20%5B%5D%0A%20%20%20%20for%20_stage%20in%20(%22before%22%2C%20%22during%22%2C%20%22after%22)%3A%0A%20%20%20%20%20%20%20%20_fig%20%3D%20plot_partition_2d(%0A%20%20%20%20%20%20%20%20%20%20%20%20partitions_exact%5B_stage%5D%2C%20domain%3Ddomain%2C%20backend%3D%22plotly%22%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_fig.add_trace(%0A%20%20%20%20%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x%3DX_moons%5B%3A%2C%200%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20y%3DX_moons%5B%3A%2C%201%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22markers%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20marker%3Ddict(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20color%3Dy_moons%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20colorscale%3D%5B%5B0%2C%20%22%233b4cc0%22%5D%2C%20%5B1%2C%20%22%23b40426%22%5D%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20size%3D6%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20line%3Ddict(width%3D0.5%2C%20color%3D%22black%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20showlegend%3DFalse%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20hoverinfo%3D%22skip%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_fig.update_layout(title%3D_labels%5B_stage%5D)%0A%20%20%20%20%20%20%20%20_figs.append(_fig)%0A%20%20%20%20mo.hstack(_figs)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20plotly.graph_objects%20as%20go%0A%0A%20%20%20%20return%20(go%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Training-time%20tracking%20with%20%60animate_epochs%60%0A%0A%20%20%20%20The%20same%20three%20checkpoints%2C%20animated.%20%60animate_epochs%60%20shares%20a%20fixed%0A%20%20%20%20color%20scale%20and%20spatial%20range%20across%20frames%20so%20the%20growth%20in%20complexity%0A%20%20%20%20is%20visually%20comparable%3B%20we%20use%20the%20(fast)%20sparse%20partitions%20here%20since%0A%20%20%20%20the%20animation%20is%20illustrative%20rather%20than%20exact-count-critical.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(animate_epochs%2C%20domain%2C%20mid_epoch%2C%20n_epochs%2C%20partitions_sparse)%3A%0A%20%20%20%20_order%20%3D%20%5B%22before%22%2C%20%22during%22%2C%20%22after%22%5D%0A%20%20%20%20_epoch_labels%20%3D%20%5B%0A%20%20%20%20%20%20%20%20%22before%20(epoch%200)%22%2C%0A%20%20%20%20%20%20%20%20f%22during%20(epoch%20%7Bmid_epoch%7D)%22%2C%0A%20%20%20%20%20%20%20%20f%22after%20(epoch%20%7Bn_epochs%7D)%22%2C%0A%20%20%20%20%5D%0A%20%20%20%20fig_anim%20%3D%20animate_epochs(%0A%20%20%20%20%20%20%20%20%5Bpartitions_sparse%5B_stage%5D%20for%20_stage%20in%20_order%5D%2C%0A%20%20%20%20%20%20%20%20epoch_labels%3D_epoch_labels%2C%0A%20%20%20%20%20%20%20%20x_range%3Ddomain%5B0%5D%2C%0A%20%20%20%20%20%20%20%20y_range%3Ddomain%5B1%5D%2C%0A%20%20%20%20%20%20%20%20backend%3D%22plotly%22%2C%0A%20%20%20%20)%0A%20%20%20%20fig_anim%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Inspecting%20the%20%60Partition%60%20object%0A%0A%20%20%20%20A%20%60Partition%60%20is%20a%20flat%20list%20of%20%60Region%60s%20plus%20the%20network%20weights%20needed%0A%20%20%20%20to%20reconstruct%20geometry%20on%20demand%20%E2%80%94%20no%20Julia%20call%20is%20required%20at%20query%0A%20%20%20%20time.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(X%2C%20mo%2C%20partition)%3A%0A%20%20%20%20_region%20%3D%20partition.regions%5B0%5D%0A%20%20%20%20_D%2C%20_g%20%3D%20partition.halfspaces(_region)%0A%20%20%20%20_A%2C%20_b%20%3D%20partition.local_affine(_region)%0A%20%20%20%20_routed%20%3D%20partition.route(X%5B%3A5%5D)%0A%20%20%20%20_region_index%20%3D%20%7Bid(r)%3A%20i%20for%20i%2C%20r%20in%20enumerate(partition.regions)%7D%0A%20%20%20%20_routed_idx%20%3D%20%5B_region_index.get(id(r))%20for%20r%20in%20_routed%5D%0A%0A%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20Looking%20at%20%60partition.regions%5B0%5D%60%2C%20activation%20pattern%0A%20%20%20%20%60%7B%5Bq.astype(int).tolist()%20for%20q%20in%20_region.activation_path%5D%7D%60%3A%0A%0A%20%20%20%20-%20%60partition.halfspaces(region)%60%20%E2%86%92%20%60D%60%20shape%20%60%7B_D.shape%7D%60%2C%20%60g%60%20shape%0A%20%20%20%20%20%20%60%7B_g.shape%7D%60%20%E2%80%94%20the%20system%20%60D%20%40%20x%20%3C%3D%20g%60%20defining%20the%20polytope.%0A%20%20%20%20-%20%60partition.local_affine(region)%60%20%E2%86%92%20%60A%60%20shape%20%60%7B_A.shape%7D%60%2C%20%60b%60%20shape%0A%20%20%20%20%20%20%60%7B_b.shape%7D%60%20%E2%80%94%20the%20local%20affine%20map%20%60f(x)%20%3D%20A%20%40%20x%20%2B%20b%60.%0A%20%20%20%20-%20%60partition.route(X%5B%3A5%5D)%60%20%E2%86%92%20region%20index%20for%20each%20of%20the%20first%205%20points%3A%0A%20%20%20%20%20%20%60%7B_routed_idx%7D%60.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Analysis%20functions%0A%0A%20%20%20%20A%20few%20of%20the%20built-in%20statistics%20%E2%80%94%20no%20Julia%20calls%2C%20cheap%20enough%20to%20run%20on%0A%20%20%20%20every%20partition.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(complexity_profile%2C%20dead_neurons%2C%20mo%2C%20partition%2C%20region_size_summary)%3A%0A%20%20%20%20_profile%20%3D%20complexity_profile(partition)%0A%20%20%20%20_dead%20%3D%20dead_neurons(partition)%0A%20%20%20%20_size%20%3D%20region_size_summary(partition)%0A%0A%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20**%60complexity_profile(partition)%60**%0A%0A%20%20%20%20-%20regions%20per%20layer%20(cumulative%20depth)%3A%20%60%7B_profile%5B'regions_per_layer'%5D%7D%60%0A%20%20%20%20-%20total%20constraints%3A%20%60%7B_profile%5B'total_constraints'%5D%7D%60%0A%20%20%20%20%20%20(mean%20%7B_profile%5B'mean_constraints_per_region'%5D%3A.1f%7D%20per%20region)%0A%0A%20%20%20%20**%60dead_neurons(partition)%60**%20%E2%80%94%20neurons%20never%20active%20in%20any%20region%3A%0A%20%20%20%20%60%7B_dead%20if%20_dead%20else%20'none'%7D%60%0A%0A%20%20%20%20**%60region_size_summary(partition)%60**%20%E2%80%94%20Chebyshev-radius%20statistics%0A%20%20%20%20(one%20LP%20per%20region)%3A%0A%0A%20%20%20%20-%20min%20%60%7B_size%5B'min'%5D%3A.3f%7D%60%2C%20median%20%60%7B_size%5B'median'%5D%3A.3f%7D%60%2C%0A%20%20%20%20%20%20mean%20%60%7B_size%5B'mean'%5D%3A.3f%7D%60%2C%20max%20%60%7B_size%5B'max'%5D%3A.3f%7D%60%0A%20%20%20%20-%20fraction%20bounded%3A%20%60%7B_size%5B'fraction_bounded'%5D%3A.0%25%7D%60%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Visualizing%20with%20the%20Plotly%20backend%0A%0A%20%20%20%20Every%20%60parx.viz%60%20plotting%20function%20accepts%20%60backend%3A%20Literal%5B%22plotly%22%2C%0A%20%20%20%20%22matplotlib%22%5D%20%3D%20%22plotly%22%60.%20Plotly%20is%20the%20default%2C%20so%20it%20is%20omitted%20below%0A%20%20%20%20except%20once%20for%20clarity%20%E2%80%94%20figures%20are%20returned%20directly%20from%20the%20cell%20and%0A%20%20%20%20marimo%20renders%20them%20inline%2C%20interactively.%20These%20use%20the%20same%0A%20%20%20%20fully-trained%20partition%20and%20domain%20as%20above.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(domain%2C%20partition%2C%20plot_partition_2d)%3A%0A%20%20%20%20fig_partition%20%3D%20plot_partition_2d(partition%2C%20domain%3Ddomain%2C%20backend%3D%22plotly%22)%0A%20%20%20%20fig_partition%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(partition%2C%20plot_region_counts)%3A%0A%20%20%20%20fig_counts%20%3D%20plot_region_counts(partition)%0A%20%20%20%20fig_counts%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(domain%2C%20partition%2C%20plot_halfspaces)%3A%0A%20%20%20%20fig_halfspaces%20%3D%20plot_halfspaces(%0A%20%20%20%20%20%20%20%20partition%2C%20partition.regions%5B0%5D%2C%20x_range%3Ddomain%5B0%5D%2C%20y_range%3Ddomain%5B1%5D%0A%20%20%20%20)%0A%20%20%20%20fig_halfspaces%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Custom%20coloring%0A%0A%20%20%20%20%60color_by%60%20accepts%20any%20%60(partition%2C%20region)%20-%3E%20float%60%20callable.%20Built-ins%0A%20%20%20%20in%20%60parx.viz%60%20include%20%60affine_frobenius%60%20(the%20default%2C%20%E2%80%96A%E2%80%96_F)%2C%0A%20%20%20%20%60affine_spectral%60%20(the%20local%20Lipschitz%20constant%20%E2%80%94%20the%20top%20singular%20value%0A%20%20%20%20of%20%60A%60)%2C%20%60affine_det%60%2C%20and%20%60active_neuron_count%60.%20Swap%20the%20colorscale%0A%20%20%20%20with%20%60colorscale%3D%60%2C%20or%20hand%20%60plot_partition_2d%60%20a%20fixed%20palette%20via%0A%20%20%20%20%60colors%3Dregion_palette(partition%2C%20scheme%3D...)%60.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(affine_spectral%2C%20domain%2C%20partition%2C%20plot_partition_2d)%3A%0A%20%20%20%20fig_spectral%20%3D%20plot_partition_2d(%0A%20%20%20%20%20%20%20%20partition%2C%0A%20%20%20%20%20%20%20%20domain%3Ddomain%2C%0A%20%20%20%20%20%20%20%20color_by%3Daffine_spectral%2C%0A%20%20%20%20%20%20%20%20colorscale%3D%22Plasma%22%2C%0A%20%20%20%20%20%20%20%20backend%3D%22plotly%22%2C%0A%20%20%20%20)%0A%20%20%20%20fig_spectral%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Higher-dimensional%20partitions%3A%20slice%20%26%20projection%0A%0A%20%20%20%20%60plot_partition_2d%60%20requires%20%60input_dim%20%3D%3D%202%60.%20For%20higher-dimensional%0A%20%20%20%20networks%2C%20%60plot_partition_slice%60%20fixes%20all%20but%20two%20input%20dimensions%20and%0A%20%20%20%20renders%20the%20induced%202D%20slice%20exactly%3B%20%60plot_partition_projection%60%0A%20%20%20%20instead%20projects%20the%20halfspace%20normals%20onto%20an%20arbitrary%202D%20subspace%20%E2%80%94%0A%20%20%20%20an%20approximation%2C%20since%20projecting%20a%20polytope's%20dual%20representation%0A%20%20%20%20isn't%20generally%20exact%20(see%20the%20function's%20docstring).%20Below%20is%20a%0A%20%20%20%203-input%20network%2C%20exactly%20enumerated%20with%20%60exact_julia_fast%60.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(compute_partition%2C%20mo%2C%20nn%2C%20np%2C%20torch)%3A%0A%20%20%20%20torch.manual_seed(1)%0A%20%20%20%20model_3d%20%3D%20nn.Sequential(%0A%20%20%20%20%20%20%20%20nn.Linear(3%2C%204)%2C%20nn.ReLU()%2C%0A%20%20%20%20%20%20%20%20nn.Linear(4%2C%201)%2C%0A%20%20%20%20)%0A%20%20%20%20partition_3d%20%3D%20compute_partition(%0A%20%20%20%20%20%20%20%20model_3d.state_dict()%2C%20np.zeros((1%2C%203))%2C%20method%3D%22exact_julia_fast%22%0A%20%20%20%20)%0A%20%20%20%20mo.md(f%223-input%20network%20%E2%86%92%20**%7Blen(partition_3d)%7D**%20regions%20in%20%E2%84%9D%C2%B3.%22)%0A%20%20%20%20return%20(partition_3d%2C)%0A%0A%0A%40app.cell%0Adef%20_(partition_3d%2C%20plot_partition_slice)%3A%0A%20%20%20%20fig_slice%20%3D%20plot_partition_slice(%0A%20%20%20%20%20%20%20%20partition_3d%2C%20free_dims%3D(0%2C%201)%2C%20fixed_values%3D%7B2%3A%200.0%7D%2C%20backend%3D%22plotly%22%0A%20%20%20%20)%0A%20%20%20%20fig_slice%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(np%2C%20partition_3d%2C%20plot_partition_projection)%3A%0A%20%20%20%20projection%20%3D%20np.eye(3)%5B%3A%2C%20%3A2%5D%20%20%23%20drop%20the%203rd%20input%20dimension%0A%20%20%20%20fig_proj%20%3D%20plot_partition_projection(partition_3d%2C%20projection%2C%20backend%3D%22plotly%22)%0A%20%20%20%20fig_proj%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Verifying%20correctness%0A%0A%20%20%20%20%60parx.verify%60%20runs%20geometric%20sanity%20checks%20against%20the%20reconstructed%0A%20%20%20%20halfspace%20systems%20%E2%80%94%20useful%20after%20changing%20a%20region-finding%20method%2C%20or%0A%20%20%20%20just%20to%20build%20confidence%20that%20a%20partition%20is%20correct.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20check_covers_space%2C%0A%20%20%20%20check_no_overlaps%2C%0A%20%20%20%20check_regions_nonempty%2C%0A%20%20%20%20domain%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20partition%2C%0A%20%20%20%20rng%2C%0A)%3A%0A%20%20%20%20_X_test%20%3D%20rng.uniform(%0A%20%20%20%20%20%20%20%20%5Bdomain%5B0%5D%5B0%5D%2C%20domain%5B1%5D%5B0%5D%5D%2C%20%5Bdomain%5B0%5D%5B1%5D%2C%20domain%5B1%5D%5B1%5D%5D%2C%20size%3D(2000%2C%202)%0A%20%20%20%20)%0A%20%20%20%20_no_overlap%2C%20_%20%3D%20check_no_overlaps(partition%2C%20_X_test)%0A%20%20%20%20_covers%2C%20_counts%20%3D%20check_covers_space(partition%2C%20_X_test)%0A%20%20%20%20_nonempty%2C%20_bad%2C%20_radii%20%3D%20check_regions_nonempty(partition)%0A%0A%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20-%20%60check_no_overlaps%60%3A%20**%7B%22pass%20%E2%9C%85%22%20if%20_no_overlap%20else%20%22FAIL%20%E2%9C%97%22%7D**%20%E2%80%94%20no%0A%20%20%20%20%20%20sample%20point%20lands%20in%20two%20regions'%20interiors.%0A%20%20%20%20-%20%60check_covers_space%60%3A%20**%7B%22pass%20%E2%9C%85%22%20if%20_covers%20else%20f%22%7Bint((_counts%20%3D%3D%200).sum())%7D%20uncovered%20points%22%7D**%0A%20%20%20%20%20%20%E2%80%94%20every%20sample%20point%20belongs%20to%20exactly%20one%20region%20(sparse%20partitions%0A%20%20%20%20%20%20can%20miss%20coverage%3B%20this%20sample%20is%20dense%20enough%20to%20pass).%0A%20%20%20%20-%20%60check_regions_nonempty%60%3A%20**%7B%22pass%20%E2%9C%85%22%20if%20_nonempty%20else%20f%22%7Blen(_bad)%7D%20degenerate%20regions%22%7D**%0A%20%20%20%20%20%20%E2%80%94%20every%20region%20has%20a%20strictly%20positive%20Chebyshev%20radius%20(min%20radius%0A%20%20%20%20%20%20found%3A%20%60%7B_radii.min()%3A.4f%7D%60).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Save%20%26%20reload%0A%0A%20%20%20%20%60save_partition%60%2F%60load_partition%60%20serialize%20a%20%60Partition%60%20to%20a%20%60.npz%60%0A%20%20%20%20bundle%20%E2%80%94%20NumPy-native%2C%20no%20Julia%20required%20to%20reload.%20A%20saved%20partition%0A%20%20%20%20can%20be%20analyzed%20or%20visualized%20in%20an%20environment%20that%20never%20installed%0A%20%20%20%20Julia%20at%20all.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(load_partition%2C%20mo%2C%20partition%2C%20save_partition%2C%20tempfile)%3A%0A%20%20%20%20_path%20%3D%20tempfile.mktemp(suffix%3D%22.npz%22)%0A%20%20%20%20save_partition(partition%2C%20_path)%0A%20%20%20%20reloaded%20%3D%20load_partition(_path)%0A%0A%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20Saved%20**%7Blen(partition)%7D**%20regions%20to%20%60%7B_path%7D%60%20and%20reloaded%20them%3A%0A%20%20%20%20%60len(reloaded)%20%3D%3D%20len(partition)%60%20%E2%86%92%20**%7Blen(reloaded)%20%3D%3D%20len(partition)%7D**%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Matplotlib%20backend%0A%0A%20%20%20%20An%20identical%20notebook%20using%20the%20matplotlib%20backend%20lives%20at%0A%20%20%20%20%60notebooks%2Fdemo_plt.py%60%20(also%20runnable%20via%20%60uv%20run%20notebooks%2Fdemo_plt.py%60).%0A%20%20%20%20The%20only%20difference%20is%20%60backend%3D%22matplotlib%22%60%20passed%20to%20each%20%60parx.viz%60%0A%20%20%20%20call%3A%0A%0A%20%20%20%20-%20%60plot_partition_2d%60%2C%20%60plot_region_counts%60%2C%20%60plot_halfspaces%60%2C%0A%20%20%20%20%20%20%60plot_partition_slice%60%2C%20%60plot_partition_projection%60%20return%20a%20static%0A%20%20%20%20%20%20%60matplotlib.figure.Figure%60%20instead%20of%20an%20interactive%0A%20%20%20%20%20%20%60plotly.graph_objects.Figure%60%20%E2%80%94%20no%20hover%20tooltips%2C%20but%20easily%20saved%0A%20%20%20%20%20%20with%20%60fig.savefig(...)%60.%0A%20%20%20%20-%20%60animate_epochs%60%20returns%20a%20%60matplotlib.animation.FuncAnimation%60%20instead%0A%20%20%20%20%20%20of%20a%20Plotly%20figure%20with%20a%20play%2Fpause%20button%20and%20slider%20%E2%80%94%20playback%20in%20a%0A%20%20%20%20%20%20notebook%20requires%20%60.to_jshtml()%60%3B%20use%20%60animate_epochs_video(...)%60%20to%0A%20%20%20%20%20%20export%20to%20MP4%2FGIF%20instead.%0A%20%20%20%20-%20The%20matplotlib%20backend%20requires%20%60pip%20install%20%22parx%5Banimate%5D%22%60.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
f3bd1bf6240bbb8d632a2bdc4886243b