|
662 | 662 | " \"Black-box sub-stepped SDE, partial observation, particle filter (PFConfig)\",\n", |
663 | 663 | ")" |
664 | 664 | ] |
665 | | - }, |
666 | | - { |
667 | | - "cell_type": "markdown", |
668 | | - "id": "64e249ae", |
669 | | - "metadata": {}, |
670 | | - "source": [ |
671 | | - "## 7. A minimal black-box example\n", |
672 | | - "\n", |
673 | | - "Section 6's black box wraps a genuine sub-stepped SDE integration -- fairly involved. Here's the\n", |
674 | | - "smallest possible black box: a one-line nonlinear transition and a non-Gaussian observation, with\n", |
675 | | - "no solver calls at all.\n", |
676 | | - "\n", |
677 | | - "$$\n", |
678 | | - "\\begin{aligned}\n", |
679 | | - "x_{k+1} &= (x_k + \\eta_k)^2 + u_k, \\qquad \\eta_k \\sim \\mathcal{N}(0, \\sigma^2 I) \\\\\n", |
680 | | - "y_k &= H x_k + \\varepsilon_k, \\qquad \\varepsilon_k \\sim \\mathrm{Laplace}(0, b)\n", |
681 | | - "\\end{aligned}\n", |
682 | | - "$$\n", |
683 | | - "\n", |
684 | | - "The noise enters *inside* the square, so $x_{k+1}$ is non-Gaussian even though $\\eta_k$ itself is\n", |
685 | | - "simple Gaussian -- there's no `mean + additive noise` form to exploit, so the transition has to be\n", |
686 | | - "a black box again: a tiny class exposing only `.sample(key)`/`.shape()`, exactly like\n", |
687 | | - "`SubSteppedSDEStep` above, just without any sub-stepping.\n", |
688 | | - "\n", |
689 | | - "The observation noise is explicitly **non-Gaussian** (Laplace) to make the point that the\n", |
690 | | - "observation model isn't restricted to Gaussian noise either -- but note this one *can't* be a bare\n", |
691 | | - "`.sample()`-only black box like the transition: cuthbert's particle filter weights particles via\n", |
692 | | - "`observation_model(x, u, t).log_prob(y)`, so it needs a real `numpyro.distributions.Distribution`\n", |
693 | | - "with a working density, not just a sampler." |
694 | | - ] |
695 | | - }, |
696 | | - { |
697 | | - "cell_type": "code", |
698 | | - "execution_count": 25, |
699 | | - "id": "41a85aee", |
700 | | - "metadata": { |
701 | | - "execution": { |
702 | | - "iopub.execute_input": "2026-08-05T18:22:45.882623Z", |
703 | | - "iopub.status.busy": "2026-08-05T18:22:45.882561Z", |
704 | | - "iopub.status.idle": "2026-08-05T18:22:45.892487Z", |
705 | | - "shell.execute_reply": "2026-08-05T18:22:45.892168Z" |
706 | | - } |
707 | | - }, |
708 | | - "outputs": [], |
709 | | - "source": [ |
710 | | - "class MinimalBlackBoxTransition:\n", |
711 | | - " \"\"\"f(x, u, t_now, t_next) = (x + noise)^2 + u -- noise enters *inside*\n", |
712 | | - " the square, so x_next is non-Gaussian even though the noise itself is\n", |
713 | | - " simple Gaussian. Only .sample()/.shape() are exposed, same minimal\n", |
714 | | - " contract as SubSteppedSDEStep above.\"\"\"\n", |
715 | | - "\n", |
716 | | - " def __init__(self, x, u, noise_std):\n", |
717 | | - " self._x, self._u, self._noise_std = x, u, noise_std\n", |
718 | | - "\n", |
719 | | - " def sample(self, key):\n", |
720 | | - " noise = self._noise_std * jr.normal(key, self._x.shape)\n", |
721 | | - " return (self._x + noise) ** 2 + self._u\n", |
722 | | - "\n", |
723 | | - " def shape(self):\n", |
724 | | - " return self._x.shape\n", |
725 | | - "\n", |
726 | | - "\n", |
727 | | - "def minimal_black_box_transition(x, u, t_now, t_next):\n", |
728 | | - " return MinimalBlackBoxTransition(x, u, noise_std=0.1)\n", |
729 | | - "\n", |
730 | | - "\n", |
731 | | - "def minimal_black_box_observation(x, u, t):\n", |
732 | | - " # Laplace noise -- genuinely non-Gaussian, unlike every other observation\n", |
733 | | - " # model in this notebook. Needs a real Distribution (not a bare .sample()\n", |
734 | | - " # black box like the transition above): PF weights particles via\n", |
735 | | - " # observation_model(x, u, t).log_prob(y).\n", |
736 | | - " return dist.Independent(\n", |
737 | | - " dist.Laplace(loc=jnp.eye(obs_dim_2d, state_dim_2d) @ x, scale=0.1), 1\n", |
738 | | - " )\n", |
739 | | - "\n", |
740 | | - "\n", |
741 | | - "minimal_dynamics = DynamicalModel(\n", |
742 | | - " initial_condition=dist.MultivariateNormal(\n", |
743 | | - " jnp.array([3.0, 2.0]), 0.05 * jnp.eye(state_dim_2d)\n", |
744 | | - " ),\n", |
745 | | - " state_evolution=minimal_black_box_transition,\n", |
746 | | - " observation_model=minimal_black_box_observation,\n", |
747 | | - " control_dim=control_dim_2d,\n", |
748 | | - ")" |
749 | | - ] |
750 | | - }, |
751 | | - { |
752 | | - "cell_type": "code", |
753 | | - "execution_count": 26, |
754 | | - "id": "6e1d0eb3", |
755 | | - "metadata": { |
756 | | - "execution": { |
757 | | - "iopub.execute_input": "2026-08-05T18:22:45.893499Z", |
758 | | - "iopub.status.busy": "2026-08-05T18:22:45.893436Z", |
759 | | - "iopub.status.idle": "2026-08-05T18:22:46.526938Z", |
760 | | - "shell.execute_reply": "2026-08-05T18:22:46.526646Z" |
761 | | - } |
762 | | - }, |
763 | | - "outputs": [], |
764 | | - "source": [ |
765 | | - "# Squaring the state each step is explosive -- no bounded control can cancel\n", |
766 | | - "# quadratic growth once x drifts away from 0, so this stays finite only for a\n", |
767 | | - "# short horizon (a handful of steps already overflows float32).\n", |
768 | | - "minimal_predict_times = jnp.arange(0.0, 0.3, 0.1)\n", |
769 | | - "\n", |
770 | | - "minimal_result = dsx.simulate(\n", |
771 | | - " minimal_dynamics,\n", |
772 | | - " rng_key=jr.PRNGKey(0),\n", |
773 | | - " predict_times=minimal_predict_times,\n", |
774 | | - " control_policy=LinearPolicy(K=0.5 * jnp.eye(control_dim_2d)),\n", |
775 | | - " filter_config=PFConfig(n_particles=500),\n", |
776 | | - ")\n", |
777 | | - "assert jnp.all(jnp.isfinite(minimal_result.states))" |
778 | | - ] |
779 | | - }, |
780 | | - { |
781 | | - "cell_type": "code", |
782 | | - "execution_count": null, |
783 | | - "id": "17de102b", |
784 | | - "metadata": {}, |
785 | | - "outputs": [], |
786 | | - "source": [] |
787 | | - }, |
788 | | - { |
789 | | - "cell_type": "code", |
790 | | - "execution_count": null, |
791 | | - "id": "11e0c759", |
792 | | - "metadata": {}, |
793 | | - "outputs": [], |
794 | | - "source": [] |
795 | 665 | } |
796 | 666 | ], |
797 | 667 | "metadata": { |
|
0 commit comments