{"id":13477235,"url":"https://github.com/pymc-devs/sunode","last_synced_at":"2025-06-23T08:05:38.291Z","repository":{"id":37604984,"uuid":"201343463","full_name":"pymc-devs/sunode","owner":"pymc-devs","description":"Solve ODEs fast, with support for PyMC","archived":false,"fork":false,"pushed_at":"2024-05-13T19:51:41.000Z","size":6318,"stargazers_count":114,"open_issues_count":11,"forks_count":10,"subscribers_count":20,"default_branch":"master","last_synced_at":"2025-06-13T02:29:49.161Z","etag":null,"topics":["autodiff","ordinary-differential-equations","pymc","sundials"],"latest_commit_sha":null,"homepage":"https://sunode.readthedocs.io","language":"Jupyter Notebook","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pymc-devs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"numfocus","custom":["https://numfocus.org/donate-to-pymc"]}},"created_at":"2019-08-08T21:58:11.000Z","updated_at":"2025-06-11T03:16:45.000Z","dependencies_parsed_at":"2024-01-13T19:18:07.433Z","dependency_job_id":"6f6490a5-05cd-4034-8af9-c50874e99d7e","html_url":"https://github.com/pymc-devs/sunode","commit_stats":{"total_commits":160,"total_committers":11,"mean_commits":"14.545454545454545","dds":"0.30000000000000004","last_synced_commit":"69a1c432259bd351b4b1ae048c3cacd82244d719"},"previous_names":["aseyboldt/sunode"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/pymc-devs/sunode","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pymc-devs%2Fsunode","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pymc-devs%2Fsunode/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pymc-devs%2Fsunode/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pymc-devs%2Fsunode/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pymc-devs","download_url":"https://codeload.github.com/pymc-devs/sunode/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pymc-devs%2Fsunode/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260519007,"owners_count":23021390,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["autodiff","ordinary-differential-equations","pymc","sundials"],"created_at":"2024-07-31T16:01:39.957Z","updated_at":"2025-06-23T08:05:33.273Z","avatar_url":"https://github.com/pymc-devs.png","language":"Jupyter Notebook","funding_links":["https://github.com/sponsors/numfocus","https://numfocus.org/donate-to-pymc"],"categories":["Jupyter Notebook","其他_机器学习与深度学习"],"sub_categories":[],"readme":"# Sunode – Solving ODEs in python\n\n[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4058330.svg)](https://doi.org/10.5281/zenodo.4058330)\n\nYou can find the documentation [here](https://sunode.readthedocs.io/en/latest/index.html).\n\nSunode wraps the sundials solvers ADAMS and BDF, and their support for solving\nadjoint ODEs in order to compute gradients of the solutions.  The required\nright-hand-side function and some derivatives are either supplied manually or\nvia sympy, in which case sunode will generate the abstract syntax tree of the\nfunctions using symbolic differentiation and common subexpression elimination.\nIn either case the functions are compiled using numba and the resulting\nC-function is passed to sunode, so that there is no python overhead.\n\n`sunode` comes with an PyTensor wrapper so that parameters of an ODE can be estimated\nusing PyMC.\n\n## Installation\n\nsunode is available on conda-forge. Set up a conda environment to use conda-forge\nand install sunode:\n\n```bash\nconda create -n sunode-env\nconda activate sunode-env\nconda config --add channels conda-forge\nconda config --set channel_priority strict\n\nconda install sunode\n```\n\nYou can also install the development version. On Windows you have to make\nsure the correct Visual Studio version is installed and in the PATH.\n\n```bash\ngit clone git@github.com:pymc-devs/sunode\n# Or if no ssh key is configured:\ngit clone https://github.com/pymc-devs/sunode\n\ncd sunode\nconda install --only-deps sunode\npip install -e .\n```\n\n## Solve an ODE outside a PyMC model\n\nWe will use the Lotka-Volterra equations as an example ODE:\n\n$$\n\\begin{gather}\n\\frac{dH}{dt} = \\alpha H - \\beta LH\\\\\n\\frac{dL}{dt} = \\delta LH - \\gamma L\n\\end{gather}\n$$\n\n```python\ndef lotka_volterra(t, y, p):\n    \"\"\"Right hand side of Lotka-Volterra equation.\n\n    All inputs are dataclasses of sympy variables, or in the case\n    of non-scalar variables numpy arrays of sympy variables.\n    \"\"\"\n    return {\n        'hares': p.alpha * y.hares - p.beta * y.lynx * y.hares,\n        'lynx': p.delta * y.hares * y.lynx - p.gamma * y.lynx,\n    }\n\n\nfrom sunode.symode import SympyProblem\nproblem = SympyProblem(\n    params={\n        # We need to specify the shape of each parameter.\n        # Any empty tuple corresponds to a scalar value.\n        'alpha': (),\n        'beta': (),\n        'gamma': (),\n        'delta': (),\n    },\n    states={\n        # The same for all state variables\n        'hares': (),\n        'lynx': (),\n    },\n    rhs_sympy=lotka_volterra,\n    derivative_params=[\n        # We need to specify with respect to which variables\n        # gradients should be computed.\n        ('alpha',),\n        ('beta',),\n    ],\n)\n\n# The solver generates uses numba and sympy to generate optimized C functions\n# for the right-hand-side and if necessary for the jacobian, adoint and\n# quadrature functions for gradients.\nfrom sunode.solver import Solver\nsolver = Solver(problem, sens_mode=None, solver='BDF')\n\n\nimport numpy as np\ntvals = np.linspace(0, 10)\n# We can use numpy structured arrays as input, so that we don't need\n# to think about how the different variables are stored in the array.\n# This does not introduce any runtime overhead during solving.\ny0 = np.zeros((), dtype=problem.state_dtype)\ny0['hares'] = 1\ny0['lynx'] = 0.1\n\n# We can also specify the parameters by name:\nsolver.set_params_dict({\n    'alpha': 0.1,\n    'beta': 0.2,\n    'gamma': 0.3,\n    'delta': 0.4,\n})\n\noutput = solver.make_output_buffers(tvals)\nsolver.solve(t0=0, tvals=tvals, y0=y0, y_out=output)\n\n# We can convert the solution to an xarray Dataset\nsolver.as_xarray(tvals, output).solution_hares.plot()\n\n# Or we can convert it to a numpy record array\nfrom matplotlib import pyplot as plt\nplt.plot(output.view(problem.state_dtype)['hares'])\n```\n\nFor this example the BDF solver (which isn't the best solver for a small non-stiff\nexample problem like this) takes ~200μs, while the `scipy.integrate.solve_ivp` solver\ntakes about 40ms at a tolerance of 1e-10, 1e-10. So we are faster by a factor of 200.\nThis advantage will get somewhat smaller for large problems however, when the\nPython overhead of the ODE solver has a smaller impact.\n\n## Usage in PyMC\n\nLet's use the same ODE, but fit the parameters using PyMC, and gradients\ncomputed using `sunode`.\n\nWe'll use some time artificial data:\n\n```python\nimport numpy as np\nimport sunode\nimport sunode.wrappers.as_pytensor\nimport pymc as pm\n\ntimes = np.arange(1900,1921,1)\nlynx_data = np.array([\n    4.0, 6.1, 9.8, 35.2, 59.4, 41.7, 19.0, 13.0, 8.3, 9.1, 7.4,\n    8.0, 12.3, 19.5, 45.7, 51.1, 29.7, 15.8, 9.7, 10.1, 8.6\n])\nhare_data = np.array([\n    30.0, 47.2, 70.2, 77.4, 36.3, 20.6, 18.1, 21.4, 22.0, 25.4,\n    27.1, 40.3, 57.0, 76.6, 52.3, 19.5, 11.2, 7.6, 14.6, 16.2, 24.7\n])\n```\n\nWe place priors on the steady-state ratio of hares and lynxes:\n\n```python\n\n\ndef lotka_volterra(t, y, p):\n    \"\"\"Right hand side of Lotka-Volterra equation.\n\n    All inputs are dataclasses of sympy variables, or in the case\n    of non-scalar variables numpy arrays of sympy variables.\n    \"\"\"\n    return {\n        'hares': p.alpha * y.hares - p.beta * y.lynx * y.hares,\n        'lynx': p.delta * y.hares * y.lynx - p.gamma * y.lynx,\n    }\n\n\nwith pm.Model() as model:\n    hares_start = pm.HalfNormal('hares_start', sigma=50)\n    lynx_start = pm.HalfNormal('lynx_start', sigma=50)\n    \n    ratio = pm.Beta('ratio', alpha=0.5, beta=0.5)\n        \n    fixed_hares = pm.HalfNormal('fixed_hares', sigma=50)\n    fixed_lynx = pm.Deterministic('fixed_lynx', ratio * fixed_hares)\n    \n    period = pm.Gamma('period', mu=10, sigma=1)\n    freq = pm.Deterministic('freq', 2 * np.pi / period)\n    \n    log_speed_ratio = pm.Normal('log_speed_ratio', mu=0, sigma=0.1)\n    speed_ratio = np.exp(log_speed_ratio)\n    \n    # Compute the parameters of the ode based on our prior parameters\n    alpha = pm.Deterministic('alpha', freq * speed_ratio * ratio)\n    beta = pm.Deterministic('beta', freq * speed_ratio / fixed_hares)\n    gamma = pm.Deterministic('gamma', freq / speed_ratio / ratio)\n    delta = pm.Deterministic('delta', freq / speed_ratio / fixed_hares / ratio)\n    \n    y_hat, _, problem, solver, _, _ = sunode.wrappers.as_pytensor.solve_ivp(\n        y0={\n        # The initial conditions of the ode. Each variable\n        # needs to specify a PyTensor or numpy variable and a shape.\n        # This dict can be nested.\n            'hares': (hares_start, ()),\n            'lynx': (lynx_start, ()),\n        },\n        params={\n        # Each parameter of the ode. sunode will only compute derivatives\n        # with respect to PyTensor variables. The shape needs to be specified\n        # as well. It it infered automatically for numpy variables.\n        # This dict can be nested.\n            'alpha': (alpha, ()),\n            'beta': (beta, ()),\n            'gamma': (gamma, ()),\n            'delta': (delta, ()),\n            'extra': np.zeros(1),\n        },\n        # A functions that computes the right-hand-side of the ode using\n        # sympy variables.\n        rhs=lotka_volterra,\n        # The time points where we want to access the solution\n        tvals=times,\n        t0=times[0],\n    )\n    \n    # We can access the individual variables of the solution using the\n    # variable names.\n    pm.Deterministic('hares_mu', y_hat['hares'])\n    pm.Deterministic('lynx_mu', y_hat['lynx'])\n    \n    sd = pm.HalfNormal('sd')\n    pm.LogNormal('hares', mu=y_hat['hares'], sigma=sd, observed=hare_data)\n    pm.LogNormal('lynx', mu=y_hat['lynx'], sigma=sd, observed=lynx_data)\n```\n\nWe can sample using PyMC (You need `cores=1` on Windows for the moment):\n\n```python\nwith model:\n    idata = pm.sample(tune=1000, draws=1000, chains=6, cores=6)\n```\n\nMany solver options can not be specified with a nice interface for now,\nwe can call the raw sundials functions however:\n\n```python\nlib = sunode._cvodes.lib\nlib.CVodeSStolerances(solver._ode, 1e-10, 1e-10)\nlib.CVodeSStolerancesB(solver._ode, solver._odeB, 1e-8, 1e-8)\nlib.CVodeQuadSStolerancesB(solver._ode, solver._odeB, 1e-8, 1e-8)\nlib.CVodeSetMaxNumSteps(solver._ode, 5000)\nlib.CVodeSetMaxNumStepsB(solver._ode, solver._odeB, 5000)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpymc-devs%2Fsunode","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpymc-devs%2Fsunode","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpymc-devs%2Fsunode/lists"}