From b4738edcba877e1c55dd5e5faba81a40601873f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 7 Dec 2020 11:20:04 +0100 Subject: [PATCH 001/102] add stub for FVC BDF2 --- examples/_const_step_size.ipynb | 77 ++++++++++++++++----------------- pyodesys/integrators.py | 43 ++++++++++++++++++ 2 files changed, 81 insertions(+), 39 deletions(-) diff --git a/examples/_const_step_size.ipynb b/examples/_const_step_size.ipynb index fcc68ba9..a0550377 100644 --- a/examples/_const_step_size.ipynb +++ b/examples/_const_step_size.ipynb @@ -3,9 +3,17 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, "outputs": [], "source": [ "from itertools import product, repeat\n", @@ -15,7 +23,7 @@ "from pyodesys import ODESys\n", "from pyodesys.integrators import (RK4_example_integrator, EulerForward_example_integrator,\n", " EulerBackward_example_integrator, Trapezoidal_example_integrator,\n", - " Midpoint_example_integrator)\n", + " Midpoint_example_integrator, BDF2FVC_example_integrator)\n", "from pyodesys.tests.bateman import bateman_full\n", "%matplotlib inline" ] @@ -23,13 +31,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "choice = RadioButtons(\n", - " options=['decay', 'sine'],\n", + " options=['sine', 'decay'],\n", " description='Case:',\n", " disabled=False\n", ")\n", @@ -39,9 +45,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "if choice.value == 'decay':\n", @@ -69,9 +73,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "xout1 = np.linspace(x0, xend, n0 + 1)\n", @@ -87,9 +89,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "def plot_order_of_convergence(Integrator, nruns=6):\n", @@ -117,9 +117,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "plot_order_of_convergence(EulerForward_example_integrator)" @@ -128,10 +126,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "plot_order_of_convergence(EulerBackward_example_integrator)" @@ -140,9 +135,16 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, + "outputs": [], + "source": [ + "plot_order_of_convergence(BDF2FVC_example_integrator)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, "outputs": [], "source": [ "plot_order_of_convergence(Midpoint_example_integrator)" @@ -151,9 +153,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "plot_order_of_convergence(Trapezoidal_example_integrator)" @@ -162,9 +162,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "plot_order_of_convergence(RK4_example_integrator)" @@ -187,16 +185,17 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.2" + "version": "3.8.6" }, + "name": "_const_step_size.ipynb", "widgets": { "state": { "7d4a5a6f8d584513bb39fffa7cdc282a": { - "views": [ - { - "cell_index": 1 - } - ] + "views": { + "cell_index": [ + 1 + ] + } } }, "version": "1.2.0" diff --git a/pyodesys/integrators.py b/pyodesys/integrators.py index 03cb2e04..ec58a9cc 100644 --- a/pyodesys/integrators.py +++ b/pyodesys/integrators.py @@ -170,3 +170,46 @@ def integrate_predefined(rhs, jac, y0, xout, **kwargs): yout.append((ynew + y + euler_fw_dy)/2) x_old = x return np.array(yout), {'nfev': (len(xout)-1)} + + +class BDF2FVC_example_integrator(EulerBackward_example_integrator): + + @staticmethod + def integrate_predefined(rhs, jac, y0, xout, tol_iter=1e-12, iter_max=20, **kwargs): + if kwargs: + warnings.warn("Ignoring keyword-argumtents: %s" % ', '.join(kwargs.keys())) + yout = [y0[:], Trapezoidal_example_integrator.integrate_predefined( + rhs, jac, y0, xout[:2], **kwargs + )[0][1,:]] + ny = len(y0) + assert len(yout) == 2 and yout[1].shape == (ny,) + x_old = xout[1] + h_old = xout[1] - xout[0] + f = np.empty(ny) + J = np.empty((ny, ny)) + I = np.eye(ny) + for i, x in enumerate(xout[2:], 2): + jac(x_old, yout[-1], J) + h = x - x_old + rho = h/h_old + # https://computing.llnl.gov/projects/parallel-time-integration-multigrid/2017_BDF_Paper_v1.pdf + # Page 9, Table 2, FVC: + beta0 = (rho+1)/(2*rho+1) + alpha1 = -(rho+1)**2/(2*rho+1) + alpha2 = rho**2/(2*rho+1) + gamma = beta0*h + lu_piv = lu_factor(gamma*J - I) + rhs(x, yout[-1], f) + ynew = yout[-1] + beta0*h*f - alpha1*yout[-1] - alpha2*yout[-2] + norm_delta_ynew = float('inf') + iiter = 0 + while norm_delta_ynew > tol_iter and iiter < iter_max: + rhs(x, ynew, f) + delta_ynew = lu_solve(lu_piv, ynew - alpha1*yout[-1] - alpha2*yout[-2] - beta0*h*f) + ynew += delta_ynew + norm_delta_ynew = np.sqrt(np.sum(np.square(delta_ynew))/ny) + iiter += 1 + yout.append(ynew) + x_old = x + h_old = h + return np.array(yout), {'nfev': (len(xout)-1)} From e8e0cf1bb311335fe7a9d9a308fdca58b4dd7e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 7 Dec 2020 21:56:28 +0100 Subject: [PATCH 002/102] small baby steps on getting bdf2 working --- examples/_const_step_size.ipynb | 62 +++++++++++----- examples/_extend_by_integration.ipynb | 32 +++------ examples/_lower_bounds.ipynb | 28 ++------ examples/_nonlinear_3body_system.ipynb | 32 +++------ examples/_params_by_name.ipynb | 32 +++------ examples/_radiolysis.ipynb | 12 ---- examples/_robertson.ipynb | 91 ++++++------------------ examples/_spring_error_convergence.ipynb | 84 ++++++---------------- pyodesys/integrators.py | 56 ++++++++++----- 9 files changed, 156 insertions(+), 273 deletions(-) diff --git a/examples/_const_step_size.ipynb b/examples/_const_step_size.ipynb index a0550377..1d65a76b 100644 --- a/examples/_const_step_size.ipynb +++ b/examples/_const_step_size.ipynb @@ -76,14 +76,38 @@ "metadata": {}, "outputs": [], "source": [ - "xout1 = np.linspace(x0, xend, n0 + 1)\n", - "res1 = odesys.integrate(xout1, y0, p, first_step=xend/n0, integrator=EulerForward_example_integrator)\n", - "yref1 = ref(res1.xout)\n", - "fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n", - "res1.plot(ls=('-',), ax=axes[0])\n", - "xplt = np.linspace(x0, xend)\n", - "res1.plot(x=xplt, y=ref(xplt), ls=(':',), ax=axes[0])\n", - "res1.plot(y=res1.yout - yref1, ax=axes[1])" + "def demo(integrator, slc=slice(None)):\n", + " xout1 = np.linspace(x0, xend, n0+1)[slc]\n", + " res1 = odesys.integrate(xout1, y0, p, first_step=xend/n0, integrator=integrator)\n", + "\n", + " fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n", + " fig.suptitle(integrator.__name__)\n", + " res1.plot(ls=('-',), ax=axes[0])\n", + " axes[0].set_title(\"values\")\n", + "\n", + " xplt = np.linspace(xout1[0], xout1[-1])\n", + " yref1 = ref(res1.xout)\n", + " res1.plot(x=xplt, y=ref(xplt), ls=(':',), ax=axes[0])\n", + " res1.plot(y=res1.yout - yref1, ax=axes[1])\n", + " axes[1].set_title(\"errors\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "demo(EulerForward_example_integrator, slc=slice(0,13))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "demo(BDF2FVC_example_integrator, slc=slice(0,13))" ] }, { @@ -93,7 +117,7 @@ "outputs": [], "source": [ "def plot_order_of_convergence(Integrator, nruns=6):\n", - " res, nruns = [], 7\n", + " res = []\n", " for i in range(nruns):\n", " n = n0 * 2**i\n", " x = np.linspace(0, 1, n)\n", @@ -103,15 +127,17 @@ " for ix, iy in product(range(n0), range(len(y0))):\n", " vals[ix, iy] = [res[i].yout[ix*(2**i), iy] for i in range(nruns)]\n", "\n", - " logn = np.log(n0 * 2**np.arange(nruns))\n", + " lgn = np.log10([len(r.xout) for r in res])\n", " fig, axes = plt.subplots(1, len(y0), figsize=(14, 4))\n", " for iy, ax in enumerate(axes):\n", " for ix in range(1, n0):\n", - " lnabserr = np.log(np.abs(np.array(vals[ix, iy]) - yref1[ix, iy]))\n", - " ax.plot(logn, lnabserr, '.-', c=(ix/n0, 0, 0))\n", - " popt = np.polyfit(logn, lnabserr, 1)\n", - " ax.plot(logn[[0,-1]], np.polyval(popt, logn[[0,-1]]), label='y=%.2f x %+.2f' % tuple(popt))\n", - " ax.legend()" + " lgabserr = np.log10(np.abs(np.array(vals[ix, iy]) - yref1[ix, iy]))\n", + " ax.plot(lgn, lgabserr, '.-', c=(ix/n0, 0, 0))\n", + " popt = np.polyfit(lgn, lgabserr, 1)\n", + " ax.plot(lgn[[0,-1]], np.polyval(popt, lgn[[0,-1]]), label='err_y%d(n)=%.2f x %+.2f' % ((iy,)+tuple(popt)))\n", + " ax.legend()\n", + " ax.set_xlabel(r'$\\lg{n_\\mathrm{t}}$')\n", + " ax.set_ylabel(r'$\\lg{|e|}$')" ] }, { @@ -170,6 +196,10 @@ } ], "metadata": { + "@webio": { + "lastCommId": null, + "lastKernelId": null + }, "kernelspec": { "display_name": "Python 3", "language": "python", @@ -185,7 +215,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.6" + "version": "3.8.1+" }, "name": "_const_step_size.ipynb", "widgets": { diff --git a/examples/_extend_by_integration.ipynb b/examples/_extend_by_integration.ipynb index 6ed72648..3fb9a656 100644 --- a/examples/_extend_by_integration.ipynb +++ b/examples/_extend_by_integration.ipynb @@ -3,9 +3,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", @@ -20,9 +18,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "linf, linj = get_ode_exprs()\n", @@ -34,9 +30,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "psysA = PartiallySolvedSystem.from_linear_invariants(linsys, preferred=[0], description='A ')\n", @@ -47,9 +41,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "tend, iv, pars = 1e18, [1, 0, 0], [0.04, 1e4, 3e7]\n", @@ -75,9 +67,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "linsys.autonomous_exprs, psysA.autonomous_exprs, psysC.autonomous_exprs" @@ -86,9 +76,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "linsys.autonomous_interface, psysA.autonomous_interface, psysC.autonomous_interface" @@ -97,9 +85,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "psysAr = PartiallySolvedSystem.from_linear_invariants(linsys, preferred=[0], roots=[1000*linsys.dep[0] - linsys.dep[2]])\n", @@ -109,9 +95,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "resAroot = psysAr.integrate(tend, iv, pars, return_on_root=True, **integrate_kw)\n", diff --git a/examples/_lower_bounds.ipynb b/examples/_lower_bounds.ipynb index 2a2e7555..f18e5a21 100644 --- a/examples/_lower_bounds.ipynb +++ b/examples/_lower_bounds.ipynb @@ -3,9 +3,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", @@ -20,9 +18,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "linf, linj = get_ode_exprs()\n", @@ -34,9 +30,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "psysA = PartiallySolvedSystem.from_linear_invariants(linsys, preferred=[0], description='A ')\n", @@ -47,9 +41,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "tend, iv, pars = 1e18, [1, 0, 0], [0.04, 1e4, 3e7]\n", @@ -76,9 +68,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "results = integrate_and_plot_systems(odes)" @@ -87,9 +77,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "_ = integrate_and_plot_systems([SymbolicSys.from_other(odesys, lower_bounds=[0]*odesys.ny) for odesys in odes])" @@ -98,9 +86,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [] } diff --git a/examples/_nonlinear_3body_system.ipynb b/examples/_nonlinear_3body_system.ipynb index 32d84758..0cdb6ccd 100644 --- a/examples/_nonlinear_3body_system.ipynb +++ b/examples/_nonlinear_3body_system.ipynb @@ -3,9 +3,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "from IPython.display import display\n", @@ -25,9 +23,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "def dydt(t, y, params=(), be=None):\n", @@ -58,9 +54,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "odesys = SymbolicSys.from_callback(dydt, 18, 3)" @@ -69,9 +63,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "y0 = [0, 0, 0, 0, 0, 1, 0, 1, 0] + [0]*9\n", @@ -82,9 +74,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "_ = res.plot(title_info=1)" @@ -93,9 +83,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "fig1 = plt.figure()\n", @@ -108,9 +96,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", @@ -122,9 +108,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "nlvls = 300\n", diff --git a/examples/_params_by_name.ipynb b/examples/_params_by_name.ipynb index 5eab7752..e1f8d9e1 100644 --- a/examples/_params_by_name.ipynb +++ b/examples/_params_by_name.ipynb @@ -3,9 +3,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", @@ -16,9 +14,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "def decay(t, y, p):\n", @@ -28,9 +24,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "odesys = SymbolicSys.from_callback(decay, 1, 1, par_by_name=True, param_names=['lmb'])" @@ -39,9 +33,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "res = odesys.integrate(3, [1], {'lmb': 7, 'irrelevant_key': 'irrelevant_value'}, integrator='odeint')\n", @@ -51,9 +43,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "results = odesys.integrate(3, [1], {'lmb': [1, 5, 25]}, integrator='odeint')\n", @@ -65,9 +55,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "def decay2(t, y, p):\n", @@ -77,9 +65,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "odesys2 = SymbolicSys.from_callback(decay2, 2, 2, par_by_name=True, param_names=['foo', 'bar'])\n", @@ -90,9 +76,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "results2 = odesys2.integrate(3, [2, 1], {'foo': 2, 'bar': [1, 2, 3]}, integrator='cvode')\n", diff --git a/examples/_radiolysis.ipynb b/examples/_radiolysis.ipynb index e65d0483..7233f00f 100644 --- a/examples/_radiolysis.ipynb +++ b/examples/_radiolysis.ipynb @@ -4,7 +4,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false, "deletable": true, "editable": true }, @@ -23,7 +22,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false, "deletable": true, "editable": true }, @@ -36,7 +34,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false, "deletable": true, "editable": true }, @@ -62,7 +59,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false, "deletable": true, "editable": true }, @@ -75,7 +71,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false, "deletable": true, "editable": true }, @@ -88,7 +83,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false, "deletable": true, "editable": true }, @@ -102,7 +96,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false, "deletable": true, "editable": true }, @@ -115,7 +108,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false, "deletable": true, "editable": true }, @@ -128,7 +120,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false, "deletable": true, "editable": true }, @@ -147,7 +138,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false, "deletable": true, "editable": true }, @@ -166,7 +156,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false, "deletable": true, "editable": true }, @@ -182,7 +171,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": true, "deletable": true, "editable": true }, diff --git a/examples/_robertson.ipynb b/examples/_robertson.ipynb index 68828b7f..b0b3a429 100644 --- a/examples/_robertson.ipynb +++ b/examples/_robertson.ipynb @@ -3,9 +3,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", @@ -20,9 +18,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "def integrate_and_plot(ax, title=None, info_vlines_kw=None, **kwargs):\n", @@ -46,9 +42,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "kw = dict(nsteps=18000, atol={'A': 1e-8, 'B': 1e-10, 'C': 1e-4}, rtol=1e-6,\n", @@ -61,9 +55,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "_ = integrate_and_plot(plt.subplot(1, 1, 1), wrapping_class=native_sys['cvode'], **kw)" @@ -72,9 +64,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "def integrate_and_plot_reduced(**kwargs):\n", @@ -95,9 +85,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "rlin = integrate_and_plot_reduced()" @@ -106,9 +94,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "kw_log = dict(atol=1e-7, logc=True, logt=True, dep_scaling=1)\n", @@ -118,9 +104,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "rloglin = integrate_and_plot_reduced(atol=1e-7, logc=True, logt=False, dep_scaling=1)" @@ -129,9 +113,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "rlinlog = integrate_and_plot_reduced(atol=1e-7, logc=False, logt=True, dep_scaling=1)" @@ -140,10 +122,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "rlog2 = integrate_and_plot_reduced(powsimp=True, **kw_log)" @@ -152,9 +131,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "import sympy as sp\n", @@ -164,9 +141,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "rlog1[0][-1].exprs" @@ -175,9 +150,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "rlog1[0][-1].get_jac()" @@ -186,10 +159,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "rlog2[0][-1].exprs" @@ -198,9 +168,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "rlog2[0][-1].get_jac()" @@ -209,9 +177,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "rlog2[0][-1]._native._written_files" @@ -220,9 +186,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "def rhs_code_in_odesys(odesys):\n", @@ -242,9 +206,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "print(''.join(rhs_code_in_odesys(rlog1[0][-1])))" @@ -253,10 +215,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "print(''.join(rhs_code_in_odesys(rlog2[0][-1])))" @@ -265,27 +224,21 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [] } diff --git a/examples/_spring_error_convergence.ipynb b/examples/_spring_error_convergence.ipynb index 339833c4..62cb5f73 100644 --- a/examples/_spring_error_convergence.ipynb +++ b/examples/_spring_error_convergence.ipynb @@ -3,9 +3,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "\n", @@ -20,9 +18,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "def dydt(t, y, params, backend):\n", @@ -32,9 +28,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "A0, spring_const = 3, 5" @@ -43,9 +37,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "analytic_xmin = spring_const**-.5 * np.pi\n", @@ -57,9 +49,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "integrate_kw = dict(integrator='cvode', method='adams', nsteps=2000)" @@ -68,9 +58,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "odesys = SymbolicSys.from_callback(dydt, 2, 1)\n", @@ -83,9 +71,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "%load_ext autoreload\n", @@ -95,9 +81,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "from pyodesys.convergence import integrate_tolerance_series, fit_factory" @@ -106,9 +90,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "def plot_tolerance_series(tols, relative=False, **kwargs):\n", @@ -136,10 +118,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "plot_tolerance_series(np.logspace(-6, -4, 7))" @@ -148,9 +127,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "plot_tolerance_series(np.logspace(-6, -4, 27))" @@ -159,9 +136,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "plot_tolerance_series(np.logspace(-10, -7, 50))" @@ -170,9 +145,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "plot_tolerance_series(np.logspace(-10, -8, 6))" @@ -181,10 +154,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "plot_tolerance_series(np.logspace(-10, -8, 12))" @@ -193,9 +163,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "plot_tolerance_series(np.logspace(-10, -8, 24))" @@ -204,10 +172,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false, - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ "plot_tolerance_series(np.logspace(-10, -8, 6), fit=fit_factory())" @@ -216,9 +181,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "plot_tolerance_series(np.logspace(-10, -8, 10), relative=True)" @@ -227,9 +190,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "from pyodesys.plotting import plot_result" @@ -238,10 +199,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "plot_tolerance_series(np.logspace(-2.5, -1.5, 12), fit=fit_factory(3))" @@ -250,9 +208,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [] } diff --git a/pyodesys/integrators.py b/pyodesys/integrators.py index ec58a9cc..d920831c 100644 --- a/pyodesys/integrators.py +++ b/pyodesys/integrators.py @@ -175,41 +175,59 @@ def integrate_predefined(rhs, jac, y0, xout, **kwargs): class BDF2FVC_example_integrator(EulerBackward_example_integrator): @staticmethod - def integrate_predefined(rhs, jac, y0, xout, tol_iter=1e-12, iter_max=20, **kwargs): + def integrate_predefined(rhs, jac, y0, tout, tol_iter=1e-12, iter_max=20, + predictor=EulerForward_example_integrator, + **kwargs): if kwargs: warnings.warn("Ignoring keyword-argumtents: %s" % ', '.join(kwargs.keys())) yout = [y0[:], Trapezoidal_example_integrator.integrate_predefined( - rhs, jac, y0, xout[:2], **kwargs + rhs, jac, y0, tout[:2], **kwargs )[0][1,:]] ny = len(y0) assert len(yout) == 2 and yout[1].shape == (ny,) - x_old = xout[1] - h_old = xout[1] - xout[0] + t_old = tout[1] + h_old = tout[1] - tout[0] f = np.empty(ny) J = np.empty((ny, ny)) I = np.eye(ny) - for i, x in enumerate(xout[2:], 2): - jac(x_old, yout[-1], J) - h = x - x_old + for i, t in enumerate(tout[2:], 2): + jac(t_old, yout[-1], J) + h = t - t_old rho = h/h_old # https://computing.llnl.gov/projects/parallel-time-integration-multigrid/2017_BDF_Paper_v1.pdf - # Page 9, Table 2, FVC: + # Page 9, Table 2, FVC (see "Experiments on Temporal Variable Step BDF 2 Algorithms, + # Anja Katrin Denner (MSc thesis)" for derivation): beta0 = (rho+1)/(2*rho+1) alpha1 = -(rho+1)**2/(2*rho+1) alpha2 = rho**2/(2*rho+1) gamma = beta0*h - lu_piv = lu_factor(gamma*J - I) - rhs(x, yout[-1], f) - ynew = yout[-1] + beta0*h*f - alpha1*yout[-1] - alpha2*yout[-2] + # α₀y₀ + α₁y₁ + α₂y₂ = hβ₀f(t,y₀) + # α₀ = 1, γ=hβ₀ => + # g = y₀ + α₁y₁ + α₂y₂ - γf(t, y₀) = 0 + # Find find a root of g(y₀) + # 1. y₀ ← y₁ + hf (predictor) + # 2. loop: y₀ ← y₀ - J⁻¹g (corrector) + + lu_piv = lu_factor(I - gamma*J) + print(i, t, yout[-1]) # ,f)##DO-NOT-MERGE!!! + pred = predictor.integrate_predefined(rhs, jac, yout[-1], tout[i-1:i+1], **kwargs)[0] + assert len(pred) == 2 and pred[1].shape == (ny,) + ynew = pred[1] # predictor + print(f" rho={rho}, a1={alpha1}, a2={alpha2}, gamma={gamma}, ynew={ynew}")##DO-NOT-MERGE!!! norm_delta_ynew = float('inf') - iiter = 0 - while norm_delta_ynew > tol_iter and iiter < iter_max: - rhs(x, ynew, f) - delta_ynew = lu_solve(lu_piv, ynew - alpha1*yout[-1] - alpha2*yout[-2] - beta0*h*f) - ynew += delta_ynew + for iiter in range(iter_max): + rhs(t, ynew, f) + delta_ynew = lu_solve(lu_piv, ynew + alpha1*yout[-1] + alpha2*yout[-2] + gamma*f) + ynew -= delta_ynew norm_delta_ynew = np.sqrt(np.sum(np.square(delta_ynew))/ny) - iiter += 1 + print(" %d %s %s" % (iiter, str(norm_delta_ynew), str(delta_ynew)))##DO-NOT-MERGE!!! + if norm_delta_ynew < tol_iter: + break + else: + print("FAILURE")##DO-NOT-MERGE!!! + return np.array(yout), dict(success=False) + yout.append(ynew) - x_old = x + t_old = t h_old = h - return np.array(yout), {'nfev': (len(xout)-1)} + return np.array(yout), {'nfev': (len(tout)-1), 'success': True} From 75ae187d29b17bb092c1a7e616067188a059e600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Thu, 10 Dec 2020 18:32:34 +0100 Subject: [PATCH 003/102] tweaks --- examples/_const_step_size.ipynb | 23 ++++++++++++++++++++--- pyodesys/integrators.py | 12 ++++++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/examples/_const_step_size.ipynb b/examples/_const_step_size.ipynb index 1d65a76b..3aee5e50 100644 --- a/examples/_const_step_size.ipynb +++ b/examples/_const_step_size.ipynb @@ -123,21 +123,24 @@ " x = np.linspace(0, 1, n)\n", " res.append(odesys.integrate(x, y0, p, integrator=Integrator))\n", "\n", - " vals = {}\n", + " vals, refs = {}, {}\n", " for ix, iy in product(range(n0), range(len(y0))):\n", " vals[ix, iy] = [res[i].yout[ix*(2**i), iy] for i in range(nruns)]\n", + " refs[ix, iy] = [ref(res[i].xout)[ix*(2**i), iy] for i in range(nruns)]\n", "\n", " lgn = np.log10([len(r.xout) for r in res])\n", " fig, axes = plt.subplots(1, len(y0), figsize=(14, 4))\n", + " fig.suptitle(Integrator.__name__)\n", " for iy, ax in enumerate(axes):\n", " for ix in range(1, n0):\n", - " lgabserr = np.log10(np.abs(np.array(vals[ix, iy]) - yref1[ix, iy]))\n", + " lgabserr = np.log10(np.abs(np.array(vals[ix, iy]) - refs[ix, iy]))\n", " ax.plot(lgn, lgabserr, '.-', c=(ix/n0, 0, 0))\n", " popt = np.polyfit(lgn, lgabserr, 1)\n", " ax.plot(lgn[[0,-1]], np.polyval(popt, lgn[[0,-1]]), label='err_y%d(n)=%.2f x %+.2f' % ((iy,)+tuple(popt)))\n", " ax.legend()\n", " ax.set_xlabel(r'$\\lg{n_\\mathrm{t}}$')\n", - " ax.set_ylabel(r'$\\lg{|e|}$')" + " ax.set_ylabel(r'$\\lg{|e|}$')\n", + " ax.set_ylim([-13, 2])" ] }, { @@ -193,6 +196,20 @@ "source": [ "plot_order_of_convergence(RK4_example_integrator)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/pyodesys/integrators.py b/pyodesys/integrators.py index d920831c..b73d71e1 100644 --- a/pyodesys/integrators.py +++ b/pyodesys/integrators.py @@ -209,24 +209,24 @@ def integrate_predefined(rhs, jac, y0, tout, tol_iter=1e-12, iter_max=20, # 2. loop: y₀ ← y₀ - J⁻¹g (corrector) lu_piv = lu_factor(I - gamma*J) - print(i, t, yout[-1]) # ,f)##DO-NOT-MERGE!!! + #print(i, t, yout[-1]) # ,f) pred = predictor.integrate_predefined(rhs, jac, yout[-1], tout[i-1:i+1], **kwargs)[0] assert len(pred) == 2 and pred[1].shape == (ny,) ynew = pred[1] # predictor - print(f" rho={rho}, a1={alpha1}, a2={alpha2}, gamma={gamma}, ynew={ynew}")##DO-NOT-MERGE!!! + #print(f" rho={rho}, a1={alpha1}, a2={alpha2}, gamma={gamma}, ynew={ynew}") norm_delta_ynew = float('inf') for iiter in range(iter_max): rhs(t, ynew, f) - delta_ynew = lu_solve(lu_piv, ynew + alpha1*yout[-1] + alpha2*yout[-2] + gamma*f) + g = ynew + alpha1*yout[-1] + alpha2*yout[-2] - gamma*f + delta_ynew = lu_solve(lu_piv, g) ynew -= delta_ynew norm_delta_ynew = np.sqrt(np.sum(np.square(delta_ynew))/ny) - print(" %d %s %s" % (iiter, str(norm_delta_ynew), str(delta_ynew)))##DO-NOT-MERGE!!! + #print(f" {iiter:4d} g={g} d={delta_ynew} |d|={norm_delta_ynew}") if norm_delta_ynew < tol_iter: break else: - print("FAILURE")##DO-NOT-MERGE!!! + print("FAILURE") return np.array(yout), dict(success=False) - yout.append(ynew) t_old = t h_old = h From 52ec471695d6da32d9c4a89cd21009dcce656642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 17 May 2021 23:14:44 +0200 Subject: [PATCH 004/102] colors --- pyodesys/integrators.py | 11 +++++++---- pyodesys/plotting.py | 9 +++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/pyodesys/integrators.py b/pyodesys/integrators.py index b73d71e1..1dac0a90 100644 --- a/pyodesys/integrators.py +++ b/pyodesys/integrators.py @@ -176,7 +176,7 @@ class BDF2FVC_example_integrator(EulerBackward_example_integrator): @staticmethod def integrate_predefined(rhs, jac, y0, tout, tol_iter=1e-12, iter_max=20, - predictor=EulerForward_example_integrator, + predictor=None, **kwargs): if kwargs: warnings.warn("Ignoring keyword-argumtents: %s" % ', '.join(kwargs.keys())) @@ -210,9 +210,12 @@ def integrate_predefined(rhs, jac, y0, tout, tol_iter=1e-12, iter_max=20, lu_piv = lu_factor(I - gamma*J) #print(i, t, yout[-1]) # ,f) - pred = predictor.integrate_predefined(rhs, jac, yout[-1], tout[i-1:i+1], **kwargs)[0] - assert len(pred) == 2 and pred[1].shape == (ny,) - ynew = pred[1] # predictor + if predictor is None: + ynew = yout[-1] + else: + pred = predictor.integrate_predefined(rhs, jac, yout[-1], tout[i-1:i+1], **kwargs)[0] + assert len(pred) == 2 and pred[1].shape == (ny,) + ynew = pred[1] # predictor #print(f" rho={rho}, a1={alpha1}, a2={alpha2}, gamma={gamma}, ynew={ynew}") norm_delta_ynew = float('inf') for iiter in range(iter_max): diff --git a/pyodesys/plotting.py b/pyodesys/plotting.py index 5934f398..384a34d0 100644 --- a/pyodesys/plotting.py +++ b/pyodesys/plotting.py @@ -6,6 +6,12 @@ import numpy as np +DEFAULT_C = tuple( + f'#{c}' for c in ( + '2ca02c', 'c49c94', 'f7b6d2', '7f7f7f', '9edae5', '98df8a', 'bcbd22', '9467bd', 'c5b0d5', 'd62728', + '8c564b', 'c7c7c7', '1f77b4', 'e377c2', 'dbdb8d', 'ff9896', '17becf', 'aec7e8', 'ffbb78', 'ff7f0e' + ) +) def _set_scale(cb, argstr): if argstr.count(';') == 0: @@ -23,8 +29,7 @@ def _latex_from_dimensionality(dim): def plot_result(x, y, indices=None, plot_kwargs_cb=None, ax=None, ls=('-', '--', ':', '-.'), - c=('tab:blue', 'tab:orange', 'tab:green', 'tab:red', 'tab:purple', - 'tab:brown', 'tab:pink', 'tab:gray', 'tab:olive', 'tab:cyan', 'black'), + c=DEFAULT_C, m=('o', 'v', '8', 's', 'p', 'x', '+', 'd', 's'), m_lim=-1, lines=None, interpolate=None, interp_from_deriv=None, names=None, latex_names=None, xlabel=None, ylabel=None, From 28098f4dab918524495ccc5e4798caf0009bf433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 17 May 2021 23:57:46 +0200 Subject: [PATCH 005/102] rhs method on native --- pyodesys/native/_base.py | 3 +++ pyodesys/native/sources/_cvode_wrapper.pyx | 20 +++++++++++++++++++ .../sources/odesys_anyode_iterative.pxd | 3 ++- pyodesys/native/tests/test_cvode.py | 3 +++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 93281f86..01cdfacb 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -305,6 +305,9 @@ def integrate(self, *args, **kwargs): return super(_NativeSysBase, self).integrate(*args, **kwargs) + def rhs(self, intern_t, intern_y, intern_p): + return self._native.mod.rhs(intern_t, intern_y, intern_p) + def _integrate_native(self, intern_x, intern_y0, intern_p, force_predefined=False, atol=1e-8, rtol=1e-8, nsteps=500, first_step=0.0, **kwargs): atol = np.atleast_1d(atol) diff --git a/pyodesys/native/sources/_cvode_wrapper.pyx b/pyodesys/native/sources/_cvode_wrapper.pyx index d0b29bcb..eda1ba56 100644 --- a/pyodesys/native/sources/_cvode_wrapper.pyx +++ b/pyodesys/native/sources/_cvode_wrapper.pyx @@ -75,6 +75,26 @@ def _as_dict(unordered_map[string, int] nfo, return dct +def rhs(floating t, floating [:] y, floating [:] p): + cdef: + cnp.ndarray[realtype, ndim=1, mode='c'] fout = np.zeros(y.size, dtype=dtype) + realtype rtol = 1e-9 + vector[realtype] atol_vec + vector[realtype] special_settings_vec + realtype get_dx_max_factor = 1.0 + bool error_outside_bounds = True + realtype max_invariant_violation = 1.0 + atol_vec.resize(y.size, 1.0) + cdef CvodesOdeSys * odesys = new CvodesOdeSys( + (NULL) if params.shape[0] == 0 else ¶ms_arr[0], + atol.data(), + rtol, + get_dx_max_factor, error_outside_bounds, + max_invariant_violation, special_settings_vec) + odesys.rhs(t, &y[0], &fout[0]) + del odesys + return fout + def integrate_adaptive(floating [:, ::1] y0, floating [::1] x0, floating [::1] xend, diff --git a/pyodesys/native/sources/odesys_anyode_iterative.pxd b/pyodesys/native/sources/odesys_anyode_iterative.pxd index c869dea3..43fe2a5b 100644 --- a/pyodesys/native/sources/odesys_anyode_iterative.pxd +++ b/pyodesys/native/sources/odesys_anyode_iterative.pxd @@ -12,9 +12,10 @@ cdef extern from "odesys_anyode_iterative.hpp" namespace "odesys_anyode": cdef cppclass OdeSys[Real_t, Index_t]: OdeSys(const Real_t * const, vector[Real_t], Real_t, Real_t, bool, Real_t, vector[Real_t]) nogil except + - Info current_info + Info current_info bool autonomous_exprs bool record_rhs_xvals bool record_jac_xvals bool record_order bool record_fpe + Status rhs(Real_t t, const Real_t * const y, Real_t * const f) except + diff --git a/pyodesys/native/tests/test_cvode.py b/pyodesys/native/tests/test_cvode.py index 71131ee8..123a04d9 100644 --- a/pyodesys/native/tests/test_cvode.py +++ b/pyodesys/native/tests/test_cvode.py @@ -148,6 +148,9 @@ def roots(t, y, p, backend): assert result.info['success'] == True # noqa assert np.min(np.abs(result.xout - 1)) < 1e-11 + f_out = odesys.rhs(0, np.array([2.0]), np.array([])) + assert f_out.size == 1 and np.all(f_out == 2.0) + @requires('sym', 'pycvodes') @pytest.mark.parametrize('idx', [0, 1, 2]) From 574f249430a31b00d23da75c8984e2021f447d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 18 May 2021 00:13:58 +0200 Subject: [PATCH 006/102] anyode --- pyodesys/native/sources/anyode.pxd | 17 ++++++++++++----- .../native/sources/odesys_anyode_iterative.pxd | 2 +- pyodesys/native/tests/test_cvode.py | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/pyodesys/native/sources/anyode.pxd b/pyodesys/native/sources/anyode.pxd index 12e3be69..b43ddfcc 100644 --- a/pyodesys/native/sources/anyode.pxd +++ b/pyodesys/native/sources/anyode.pxd @@ -6,13 +6,20 @@ from libcpp.vector cimport vector from libcpp cimport bool cdef extern from "anyode/anyode.hpp" namespace "AnyODE": - cdef cppclass OdeSysBase[Real_t, Index_t]: - int nfev, njev, njvev - bool use_get_dx_max + cdef cppclass OdeSysBase[Real_t, Index_t]: + int nfev, njev, njvev + bool use_get_dx_max -cdef extern from "anyode/anyode.hpp" namespace "AnyODE": - cdef cppclass Info: + cdef cppclass Info: unordered_map[string, int] nfo_int unordered_map[string, double] nfo_dbl unordered_map[string, vector[double]] nfo_vecdbl unordered_map[string, vector[int]] nfo_vecint + + cdef cppclass Status: + pass # Status is an enum class + +cdef extern from "anyode/anyode.hpp" namespace "AnyODE::Status": + cdef Status success + cdef Status recoverable_error + cdef Status unrecoverable_error diff --git a/pyodesys/native/sources/odesys_anyode_iterative.pxd b/pyodesys/native/sources/odesys_anyode_iterative.pxd index 43fe2a5b..c49ef7d5 100644 --- a/pyodesys/native/sources/odesys_anyode_iterative.pxd +++ b/pyodesys/native/sources/odesys_anyode_iterative.pxd @@ -6,7 +6,7 @@ from libcpp.string cimport string from libcpp.unordered_map cimport unordered_map from libcpp.vector cimport vector -from anyode cimport Info +from anyode cimport Info, Status cdef extern from "odesys_anyode_iterative.hpp" namespace "odesys_anyode": cdef cppclass OdeSys[Real_t, Index_t]: diff --git a/pyodesys/native/tests/test_cvode.py b/pyodesys/native/tests/test_cvode.py index 123a04d9..78a53469 100644 --- a/pyodesys/native/tests/test_cvode.py +++ b/pyodesys/native/tests/test_cvode.py @@ -148,7 +148,7 @@ def roots(t, y, p, backend): assert result.info['success'] == True # noqa assert np.min(np.abs(result.xout - 1)) < 1e-11 - f_out = odesys.rhs(0, np.array([2.0]), np.array([])) + f_out = oodesys.rhs(0, np.array([2.0]), np.array([])) assert f_out.size == 1 and np.all(f_out == 2.0) From 49a4730113222a49f025b2fe73651c76c4b52493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 18 May 2021 14:30:30 +0200 Subject: [PATCH 007/102] fix rhs --- pyodesys/native/sources/_cvode_wrapper.pyx | 13 +++++++++---- pyodesys/native/tests/test_cvode.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pyodesys/native/sources/_cvode_wrapper.pyx b/pyodesys/native/sources/_cvode_wrapper.pyx index eda1ba56..f671174c 100644 --- a/pyodesys/native/sources/_cvode_wrapper.pyx +++ b/pyodesys/native/sources/_cvode_wrapper.pyx @@ -75,9 +75,12 @@ def _as_dict(unordered_map[string, int] nfo, return dct -def rhs(floating t, floating [:] y, floating [:] p): +def rhs(double t, floating [:] y, floating [:] p): cdef: + #cnp.ndarray[realtype, ndim=1, mode='c'] t_arr = np.asarray(t, dtype=dtype) + cnp.ndarray[realtype, ndim=1, mode='c'] y_arr = np.asarray(y, dtype=dtype) cnp.ndarray[realtype, ndim=1, mode='c'] fout = np.zeros(y.size, dtype=dtype) + cnp.ndarray[realtype, ndim=1, mode='c'] params_arr = np.asarray(p, dtype=dtype) realtype rtol = 1e-9 vector[realtype] atol_vec vector[realtype] special_settings_vec @@ -85,13 +88,15 @@ def rhs(floating t, floating [:] y, floating [:] p): bool error_outside_bounds = True realtype max_invariant_violation = 1.0 atol_vec.resize(y.size, 1.0) + #assert t_arr.size == 1 cdef CvodesOdeSys * odesys = new CvodesOdeSys( - (NULL) if params.shape[0] == 0 else ¶ms_arr[0], - atol.data(), + #(NULL) if p.shape[0] == 0 else + params_arr.data, + atol_vec, rtol, get_dx_max_factor, error_outside_bounds, max_invariant_violation, special_settings_vec) - odesys.rhs(t, &y[0], &fout[0]) + odesys.rhs(t, y_arr.data, &fout[0]) del odesys return fout diff --git a/pyodesys/native/tests/test_cvode.py b/pyodesys/native/tests/test_cvode.py index 78a53469..07485a03 100644 --- a/pyodesys/native/tests/test_cvode.py +++ b/pyodesys/native/tests/test_cvode.py @@ -148,7 +148,7 @@ def roots(t, y, p, backend): assert result.info['success'] == True # noqa assert np.min(np.abs(result.xout - 1)) < 1e-11 - f_out = oodesys.rhs(0, np.array([2.0]), np.array([])) + f_out = odesys.rhs(np.array(0.0, dtype=np.float64), np.array([2.0]), np.array([], dtype=np.float64)) assert f_out.size == 1 and np.all(f_out == 2.0) From 41d71dc1a3fe749eb6b34b9ba4bb869ec11832ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 19 May 2021 13:33:37 +0200 Subject: [PATCH 008/102] branch master for anyode submodule --- .gitmodules | 1 + external/anyode | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index ce06ee9b..48899e90 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "external/anyode"] path = external/anyode url = git://github.com/bjodah/anyode + branch = master \ No newline at end of file diff --git a/external/anyode b/external/anyode index 2e7a0ea6..a63bd019 160000 --- a/external/anyode +++ b/external/anyode @@ -1 +1 @@ -Subproject commit 2e7a0ea669a88b744c61c492def4558fd6c7c664 +Subproject commit a63bd019436ae2c7822757c5033e715cf2e62913 From f622c457c567a799195526f7895773531157abbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Thu, 20 May 2021 23:54:49 +0200 Subject: [PATCH 009/102] only append in info in extend_by_integration when possible --- pyodesys/results.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyodesys/results.py b/pyodesys/results.py index 5567b7b9..41c1fd24 100644 --- a/pyodesys/results.py +++ b/pyodesys/results.py @@ -308,6 +308,9 @@ def extend_by_integration(self, xend, params=None, odesys=None, autonomous=None, new_info[k] = [new_info[k]] new_info[k].append(v) else: - new_info[k] += v + try: + new_info[k] += v + except TypeError: + new_info[k] = v self.info = new_info return self From b75fc92b3ea5802033f27af8bd0370718e191d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sat, 22 May 2021 09:44:42 +0200 Subject: [PATCH 010/102] expose jac --- pyodesys/native/_base.py | 3 +++ pyodesys/native/sources/_cvode_wrapper.pyx | 25 +++++++++++++++++++ pyodesys/native/sources/odesys_anyode.pxd | 2 +- .../sources/odesys_anyode_iterative.pxd | 5 ++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 01cdfacb..4a6978d2 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -308,6 +308,9 @@ def integrate(self, *args, **kwargs): def rhs(self, intern_t, intern_y, intern_p): return self._native.mod.rhs(intern_t, intern_y, intern_p) + def jac(self, intern_t, intern_y, intern_p): + return self._native.mod.dense_jac_cmaj(intern_t, intern_y, intern_p) + def _integrate_native(self, intern_x, intern_y0, intern_p, force_predefined=False, atol=1e-8, rtol=1e-8, nsteps=500, first_step=0.0, **kwargs): atol = np.atleast_1d(atol) diff --git a/pyodesys/native/sources/_cvode_wrapper.pyx b/pyodesys/native/sources/_cvode_wrapper.pyx index f671174c..8bc60655 100644 --- a/pyodesys/native/sources/_cvode_wrapper.pyx +++ b/pyodesys/native/sources/_cvode_wrapper.pyx @@ -100,6 +100,31 @@ def rhs(double t, floating [:] y, floating [:] p): del odesys return fout +def dense_jac_cmaj(double t, floating [:] y, floating [:] p): + cdef: + #cnp.ndarray[realtype, ndim=1, mode='c'] t_arr = np.asarray(t, dtype=dtype) + cnp.ndarray[realtype, ndim=1, mode='c'] y_arr = np.asarray(y, dtype=dtype) + cnp.ndarray[realtype, ndim=2, mode='fortran'] j_out = np.zeros((y.size, y.size), dtype=dtype, order='F') + cnp.ndarray[realtype, ndim=1, mode='c'] params_arr = np.asarray(p, dtype=dtype) + realtype rtol = 1e-9 + vector[realtype] atol_vec + vector[realtype] special_settings_vec + realtype get_dx_max_factor = 1.0 + bool error_outside_bounds = True + realtype max_invariant_violation = 1.0 + atol_vec.resize(y.size, 1.0) + #assert t_arr.size == 1 + cdef CvodesOdeSys * odesys = new CvodesOdeSys( + #(NULL) if p.shape[0] == 0 else + params_arr.data, + atol_vec, + rtol, + get_dx_max_factor, error_outside_bounds, + max_invariant_violation, special_settings_vec) + odesys.dense_jac_cmaj(t, y_arr.data, NULL, &j_out[0,0], y.size) + del odesys + return j_out + def integrate_adaptive(floating [:, ::1] y0, floating [::1] x0, floating [::1] xend, diff --git a/pyodesys/native/sources/odesys_anyode.pxd b/pyodesys/native/sources/odesys_anyode.pxd index cefe653d..09f9e27b 100644 --- a/pyodesys/native/sources/odesys_anyode.pxd +++ b/pyodesys/native/sources/odesys_anyode.pxd @@ -15,4 +15,4 @@ cdef extern from "odesys_anyode.hpp" namespace "odesys_anyode": Index_t get_ny() nogil Real_t get_dx0(Real_t, const Real_t * const) nogil unsigned nfev, njev, njvev - Info current_info + Info current_info diff --git a/pyodesys/native/sources/odesys_anyode_iterative.pxd b/pyodesys/native/sources/odesys_anyode_iterative.pxd index c49ef7d5..c334f2ae 100644 --- a/pyodesys/native/sources/odesys_anyode_iterative.pxd +++ b/pyodesys/native/sources/odesys_anyode_iterative.pxd @@ -19,3 +19,8 @@ cdef extern from "odesys_anyode_iterative.hpp" namespace "odesys_anyode": bool record_order bool record_fpe Status rhs(Real_t t, const Real_t * const y, Real_t * const f) except + + Status dense_jac_cmaj(Real_t t, + const Real_t * const y, + const Real_t * const fy, + Real_t * const jac, + long int ldim) except + From 1911a243a9f8138ddd6a7f75d2e8805f4bce6763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Fri, 28 May 2021 23:37:42 +0200 Subject: [PATCH 011/102] title_info, relax req. on description attr --- pyodesys/results.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyodesys/results.py b/pyodesys/results.py index 41c1fd24..cfe7c27c 100644 --- a/pyodesys/results.py +++ b/pyodesys/results.py @@ -233,7 +233,7 @@ def plot(self, info_vlines_kw=None, between=None, deriv=False, title_info=0, **k ax = self._plot(plot_result, **kwargs) if title_info: ax.set_title( - (self.odesys.description or '') + + (getattr(self.odesys, 'description', None) or '') + ', '.join( (['%d steps' % self.info['n_steps']] if self.info.get('n_steps', -1) >= 0 else []) + [ From ee5d79b63889d7dd96091d6f67467a27e2e4975e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 30 May 2021 21:32:57 +0200 Subject: [PATCH 012/102] make it easier to disable cse in native --- pyodesys/native/_base.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 4a6978d2..372058f6 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -79,13 +79,16 @@ class _NativeCodeBase(Cpp_Code): # `namespace_override` is set in init # `namespace_extend` is set in init - def __init__(self, odesys, *args, **kwargs): + def __init__(self, odesys, *args, use_cse=None, **kwargs): if Cpp_Code is object: raise ModuleNotFoundError("failed to import Cpp_Code from pycodeexport") if compile_sources is None: raise ModuleNotFoundError("failed to import compile_sources from pycompilation") if odesys.nroots > 0 and not self._support_roots: raise ValueError("%s does not support nroots > 0" % self.__class__.__name__) + if use_cse is None: + use_cse = os.getenv('PYODESYS_NATIVE_CSE', '1') == '1' + self.use_cse = use_cse self.namespace_override = kwargs.pop('namespace_override', {}) self.namespace_extend = kwargs.pop('namespace_extend', {}) self.tempdir_basename = '_pycodeexport_pyodesys_%s' % self.__class__.__name__ @@ -170,7 +173,7 @@ def common_cse_symbols(): def _ccode(expr): return self.odesys.be.ccode(expr.xreplace(subsd)) - if os.getenv('PYODESYS_NATIVE_CSE', '1') == '1': + if self.use_cse: cse_cb = self.odesys.be.cse else: logger.info("Not using common subexpression elimination (disabled by PYODESYS_NATIVE_CSE)") @@ -284,7 +287,7 @@ class _NativeSysBase(SymbolicSys): _NativeCode = None _native_name = None - def __init__(self, *args, **kwargs): + def __init__(self, *args, native_code_kw=None, **kwargs): namespace_override = kwargs.pop('namespace_override', {}) namespace_extend = kwargs.pop('namespace_extend', {}) save_temp = kwargs.pop('save_temp', False) @@ -294,7 +297,8 @@ def __init__(self, *args, **kwargs): super(_NativeSysBase, self).__init__(*args, **kwargs) self._native = self._NativeCode(self, save_temp=save_temp, namespace_override=namespace_override, - namespace_extend=namespace_extend) + namespace_extend=namespace_extend, + **(native_code_kw or {})) def integrate(self, *args, **kwargs): integrator = kwargs.pop('integrator', 'native') From ab3d501ec6fa4e9129b10a87f6a841ce4559840b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 31 May 2021 18:02:14 +0200 Subject: [PATCH 013/102] bump version --- pyodesys/_release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyodesys/_release.py b/pyodesys/_release.py index 0321c5d0..aeff9fdc 100644 --- a/pyodesys/_release.py +++ b/pyodesys/_release.py @@ -1 +1 @@ -__version__ = '0.14.0.dev0+git' +__version__ = '0.14.1.dev0+git' From 4d69f329dd81707bbc9327e2d8303310ce727f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 1 Jun 2021 10:31:36 +0200 Subject: [PATCH 014/102] skip notebook: _const_step_size.ipynb --- scripts/render_notebooks.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/render_notebooks.sh b/scripts/render_notebooks.sh index 8beae418..7c772179 100755 --- a/scripts/render_notebooks.sh +++ b/scripts/render_notebooks.sh @@ -29,6 +29,9 @@ for ipynb in *.ipynb; do if [[ $ipynb == "_bench_native_odesys_multi.ipynb" ]]; then continue fi + if [[ $ipynb == "_const_step_size.ipynb" ]]; then + continue + fi #quiet_unless_fail jupyter nbconvert --log-level=INFO --to=html --ExecutePreprocessor.enabled=True --ExecutePreprocessor.timeout=900 "${ipynb}" #if [ ${QUIET_EXIT_CODE} -ne 0 ]; then From 3dc9b9db0ec4eb0bf293abe5ebf2ee870257cd8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 1 Jun 2021 11:22:31 +0200 Subject: [PATCH 015/102] bump CI image --- .drone.yml | 4 ++-- scripts/render_notebooks.sh | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.drone.yml b/.drone.yml index 8fe1aff6..9821eed7 100644 --- a/.drone.yml +++ b/.drone.yml @@ -39,7 +39,7 @@ pipeline: test-sund-5.4.0-klu: group: testing - image: bjodah/bjodahimg20dev:2.1.0 + image: bjodah/bjodahimg20dev:2.1.1 environment: - CC=gcc-10 - CXX=g++-10 @@ -56,7 +56,7 @@ pipeline: test-sund-5.4.0-nolapack-noklu-extended: group: testing - image: bjodah/bjodahimg20dev:2.1.0 + image: bjodah/bjodahimg20dev:2.1.1 environment: - CC=gcc-10 - CXX=g++-10 diff --git a/scripts/render_notebooks.sh b/scripts/render_notebooks.sh index 7c772179..0851a370 100755 --- a/scripts/render_notebooks.sh +++ b/scripts/render_notebooks.sh @@ -33,7 +33,8 @@ for ipynb in *.ipynb; do continue fi #quiet_unless_fail - jupyter nbconvert --log-level=INFO --to=html --ExecutePreprocessor.enabled=True --ExecutePreprocessor.timeout=900 "${ipynb}" + jupyter nbconvert --log-level=INFO --to=html --ExecutePreprocessor.enabled=True --ExecutePreprocessor.timeout=900 "${ipynb}" \ + | grep -v -e "^\[NbConvertApp\] content: {'data':.*'image/png'" #if [ ${QUIET_EXIT_CODE} -ne 0 ]; then # exit ${QUIET_EXIT_CODE} #fi From 6231257bc3319c8d3083b67eabf0311fff70d965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 1 Jun 2021 13:38:31 +0200 Subject: [PATCH 016/102] avoid -e flag during quiet phase --- scripts/render_notebooks.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/render_notebooks.sh b/scripts/render_notebooks.sh index 0851a370..06f9f5e3 100755 --- a/scripts/render_notebooks.sh +++ b/scripts/render_notebooks.sh @@ -19,6 +19,7 @@ cd examples/ PREC=`python3 -c "from pycvodes import config; print(config.get('SUNDIALS_PRECISION', 'double'))"` set -x +set +e for ipynb in *.ipynb; do if [[ $ipynb == "_native_standalone.ipynb" ]]; then continue # issue with boost's program options @@ -32,11 +33,11 @@ for ipynb in *.ipynb; do if [[ $ipynb == "_const_step_size.ipynb" ]]; then continue fi - #quiet_unless_fail - jupyter nbconvert --log-level=INFO --to=html --ExecutePreprocessor.enabled=True --ExecutePreprocessor.timeout=900 "${ipynb}" \ + quiet_unless_fail jupyter nbconvert --log-level=INFO --to=html --ExecutePreprocessor.enabled=True --ExecutePreprocessor.timeout=900 "${ipynb}" \ | grep -v -e "^\[NbConvertApp\] content: {'data':.*'image/png'" #if [ ${QUIET_EXIT_CODE} -ne 0 ]; then # exit ${QUIET_EXIT_CODE} #fi done +set -e #../scripts/render_index.sh *.html From 892ae32e1432b8e09e21bbeae63d3dd12bea9134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Fri, 11 Jun 2021 22:59:21 +0200 Subject: [PATCH 017/102] unsusre about max_invariant_violation formula for negative values? --- pyodesys/native/sources/odesys_anyode_template.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 8ddcab5e..22117bfa 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -188,7 +188,9 @@ namespace odesys_anyode { const auto ${cse_token} = ${cse_expr}; %endfor %for idx, invar_expr in enumerate(p_invariants['exprs']): - if (std::abs(${invar_expr} - m_invar0[${idx}]) > ((m_max_invariant_violation > 0) ? m_max_invariant_violation : std::abs(m_max_invariant_violation*m_invar0[${idx}]) - m_max_invariant_violation)) { + if (std::abs(${invar_expr} - m_invar0[${idx}]) > ((m_max_invariant_violation > 0) + ? m_max_invariant_violation + : std::abs(m_max_invariant_violation*m_invar0[${idx}]) /*- m_max_invariant_violation*/)) { std::cerr << "Invariant (${idx}) violation at x=" << x << "\n"; return AnyODE::Status::recoverable_error; } From 8b18dc5898907784087b66a6fa371603009b475d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sat, 12 Jun 2021 09:27:47 +0200 Subject: [PATCH 018/102] Lambdify_kw (cse) --- .ci/run_ci.sh | 1 + pyodesys/symbolic.py | 15 +++++++++------ pyodesys/util.py | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.ci/run_ci.sh b/.ci/run_ci.sh index e893f730..17eaec01 100755 --- a/.ci/run_ci.sh +++ b/.ci/run_ci.sh @@ -13,6 +13,7 @@ fi mkdir -p $HOME/.config/pip/ echo -e "[global]\nno-cache-dir = false\ndownload-cache = $(pwd)/ci_cache/pip_cache" >$HOME/.config/pip/pip.conf python3 -m pip install symcxx pysym # unofficial backends, symengine is tested in the conda build +python3 -m pip install https://github.com/bjodah/sym/archive/Lambdify-cse.tar.gz # (cd ./tmp/pycvodes; SUND_CFLAGS="-isystem $SUNDBASE/include $CFLAGS" diff --git a/pyodesys/symbolic.py b/pyodesys/symbolic.py index 895ac615..17de9ecd 100644 --- a/pyodesys/symbolic.py +++ b/pyodesys/symbolic.py @@ -232,7 +232,7 @@ def __init__(self, dep_exprs, indep=None, params=None, jac=True, dfdx=True, jtimes=False, first_step_expr=None, roots=None, backend=None, lower_bounds=None, upper_bounds=None, linear_invariants=None, nonlinear_invariants=None, linear_invariant_names=None, nonlinear_invariant_names=None, steady_state_root=False, - init_indep=None, init_dep=None, sparse=False, **kwargs): + init_indep=None, init_dep=None, sparse=False, Lambdify_kw=None, **kwargs): self.dep, self.exprs = zip(*dep_exprs.items()) if isinstance(dep_exprs, dict) else zip(*dep_exprs) self.indep = indep if params is True or params is None: @@ -248,6 +248,7 @@ def __init__(self, dep_exprs, indep=None, params=None, jac=True, dfdx=True, self._dfdx = dfdx self.first_step_expr = first_step_expr self.be = Backend(backend) + self.Lambdify_kw = Lambdify_kw if steady_state_root: if steady_state_root is True: @@ -541,7 +542,7 @@ def from_other_new_params(cls, ori, par_subs, new_pars, new_par_names=None, drop_idxs = [ori.params.index(par) for par in par_subs] params = _skip(drop_idxs, ori.params, False) + list(new_pars) back_substitute = _Callback(ori.indep, ori.dep, params, list(par_subs.values()), - Lambdify=ori.be.Lambdify) + Lambdify=ori.be.Lambdify, Lambdify_kw=ori.Lambdify_kw) def recalc_params(t, y, p): rev = back_substitute(t, y, p) @@ -684,7 +685,8 @@ def get_dfdx(self): return self._dfdx def _callback_factory(self, exprs): - return _Callback(self.indep, self.dep, self.params, exprs, Lambdify=self.be.Lambdify) + return _Callback(self.indep, self.dep, self.params, exprs, + Lambdify=self.be.Lambdify, Lambdify_kw=self.Lambdify_kw) def get_f_ty_callback(self): """ Generates a callback for evaluating ``self.exprs``. """ @@ -738,8 +740,8 @@ def get_jtimes_callback(self): if jtimes is False: return None v, jtimes_exprs = jtimes - return _Callback(self.indep, tuple(self.dep) + tuple(v), self.params, - jtimes_exprs, Lambdify=self.be.Lambdify) + return _Callback(self.indep, tuple(self.dep) + tuple(v), self.params, jtimes_exprs, + Lambdify=self.be.Lambdify, Lambdify_kw=self.Lambdify_kw) def get_first_step_callback(self): if self.first_step_expr is None: @@ -1336,7 +1338,8 @@ def analytic_factory(x0, y0, p0, be): @staticmethod def _get_analytic_callback(ori_sys, analytic_exprs, new_dep, new_params): - return _Callback(ori_sys.indep, new_dep, new_params, analytic_exprs, Lambdify=ori_sys.be.Lambdify) + return _Callback(ori_sys.indep, new_dep, new_params, analytic_exprs, + Lambdify=ori_sys.be.Lambdify, Lambdify_kw=ori_sys.Lambdify_kw) def __getitem__(self, key): ori_dep = self.original_dep[self.names.index(key)] diff --git a/pyodesys/util.py b/pyodesys/util.py index 4bd531c3..f5e795a6 100644 --- a/pyodesys/util.py +++ b/pyodesys/util.py @@ -160,7 +160,7 @@ def _concat(*args): class _Callback(_Blessed): - def __init__(self, indep, dep, params, exprs, Lambdify=None): + def __init__(self, indep, dep, params, exprs, Lambdify=None, Lambdify_kw=None): self.indep, self.dep, self.params = indep, dep, params if indep is None: self.args = _concat(self.dep, self.params) @@ -168,7 +168,7 @@ def __init__(self, indep, dep, params, exprs, Lambdify=None): self.args = _concat(self.indep, self.dep, self.params) self.input_width = len(self.args) self.exprs = exprs - self.callback = Lambdify(self.args, self.exprs) + self.callback = Lambdify(self.args, self.exprs, **(Lambdify_kw or {})) self.ny = len(dep) self.take_params = len(params) From 9a91def6782c6b1790c1cdb3a38c3f3f18d1cc3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Thu, 17 Jun 2021 23:12:23 +0200 Subject: [PATCH 019/102] debug print of some cse exprs in native template --- pyodesys/native/sources/odesys_anyode_template.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 22117bfa..23fc539a 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -154,10 +154,14 @@ namespace odesys_anyode { %for cse_token, cse_expr in p_rhs['cses']: const auto ${cse_token} = ${cse_expr}; %endfor - + <% import os %> %for i, expr in enumerate(p_rhs['exprs']): f[${i}] = ${expr}; %endfor + %if os.environ.get("HOST_LOGNAME", "") == "bjorn" and os.environ.get("IN_DOCKER", "0") == "2" and p_rhs['cses']: + <%doc>DO-NOT-MERGE! + std::clog << nfev << ", t=" << x << ", cse11=" << cse11 << ", cse12=" << cse12 << ", cse17=" << cse17 << ", f[1, Fe+3]=" << f[1] << '\n'; + %endif this->nfev++; %if p_support_recoverable_error: if (m_error_outside_bounds){ From fe845b9a8ee7769c9a15039f61b45127775ae6de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 20 Jun 2021 13:22:03 +0200 Subject: [PATCH 020/102] more specific debug --- pyodesys/native/sources/odesys_anyode_template.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 23fc539a..9bcaa26f 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -158,8 +158,8 @@ namespace odesys_anyode { %for i, expr in enumerate(p_rhs['exprs']): f[${i}] = ${expr}; %endfor - %if os.environ.get("HOST_LOGNAME", "") == "bjorn" and os.environ.get("IN_DOCKER", "0") == "2" and p_rhs['cses']: - <%doc>DO-NOT-MERGE! + %if os.environ.get("MY_DEBUG_EMITTING_TOKEN", "") == "kinetg2021fricke" and os.environ.get("IN_DOCKER", "0") == "2" and p_rhs['cses']: + <%doc>DO-NOT-MERGE! std::clog << nfev << ", t=" << x << ", cse11=" << cse11 << ", cse12=" << cse12 << ", cse17=" << cse17 << ", f[1, Fe+3]=" << f[1] << '\n'; %endif this->nfev++; From cb2892ecd3005184097c1e95e939e624f25202f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 23 Jun 2021 14:58:35 +0200 Subject: [PATCH 021/102] bump version number --- pyodesys/_release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyodesys/_release.py b/pyodesys/_release.py index aeff9fdc..2ad6f263 100644 --- a/pyodesys/_release.py +++ b/pyodesys/_release.py @@ -1 +1 @@ -__version__ = '0.14.1.dev0+git' +__version__ = '0.14.2.dev0+git' From 8fbb6695003160347e88b267f59b8d057b144d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 23 Jun 2021 17:00:40 +0200 Subject: [PATCH 022/102] compile kwargs etc. --- pyodesys/native/_base.py | 7 +++++++ pyodesys/native/cvode.py | 4 ++-- pyodesys/native/sources/_cvode_wrapper.pyx | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 372058f6..55d6ed9f 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -45,6 +45,13 @@ 'cplus': True, } +def get_compile_kwargs(): + kw = _compile_kwargs.copy() + options = os.environ.get("PYODESYS_OPTIONS") + if options: + kw['options'] = options.split(',') + return kw + _ext_suffix = '.so' # sysconfig.get_config_var('EXT_SUFFIX') _obj_suffix = '.o' # os.path.splitext(_ext_suffix)[0] + '.o' # '.obj' diff --git a/pyodesys/native/cvode.py b/pyodesys/native/cvode.py index 8eae2110..b40a192b 100644 --- a/pyodesys/native/cvode.py +++ b/pyodesys/native/cvode.py @@ -6,7 +6,7 @@ import sys from ..util import import_ -from ._base import _NativeCodeBase, _NativeSysBase, _compile_kwargs +from ._base import _NativeCodeBase, _NativeSysBase, get_compile_kwargs get_include, config, _libs = import_("pycvodes", "get_include", "config", "_libs") @@ -36,7 +36,7 @@ class NativeCvodeCode(_NativeCodeBase): _support_roots = True def __init__(self, *args, **kwargs): - self.compile_kwargs = copy.deepcopy(_compile_kwargs) + self.compile_kwargs = get_compile_kwargs() self.compile_kwargs['define'] = ['PYCVODES_NO_KLU={}'.format("0" if config.get('KLU', True) else "1"), 'PYCVODES_NO_LAPACK={}'.format("0" if config.get('LAPACK', True) else "1"), 'ANYODE_NO_LAPACK={}'.format("0" if config.get('LAPACK', True) else "1")] diff --git a/pyodesys/native/sources/_cvode_wrapper.pyx b/pyodesys/native/sources/_cvode_wrapper.pyx index 8bc60655..8242e23d 100644 --- a/pyodesys/native/sources/_cvode_wrapper.pyx +++ b/pyodesys/native/sources/_cvode_wrapper.pyx @@ -125,6 +125,7 @@ def dense_jac_cmaj(double t, floating [:] y, floating [:] p): del odesys return j_out + def integrate_adaptive(floating [:, ::1] y0, floating [::1] x0, floating [::1] xend, From 35dcc862a81fb5a204af613891f0cc8ae53e4655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 13 Jul 2021 20:49:39 +0200 Subject: [PATCH 023/102] bump CI image --- .drone.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.drone.yml b/.drone.yml index 9821eed7..48a9621e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -17,38 +17,38 @@ pipeline: ttl: 90 # liftetime in days # get-pre-reqs: - # image: bjodah/bjodahimg20dev:2.1.0 + # image: bjodah/bjodahimg20dev:21.7.b # commands: # - mkdir ./tmp # - git clone --recurse-submodules https://github.com/bjodah/pycvodes.git ./tmp/pycvodes # test-py2: - # image: bjodah/bjodahimg20dev:2.1.0 + # image: bjodah/bjodahimg20dev:21.7.b # environment: # - CC=gcc-10 # - CXX=g++-10 # - OMP_NUM_THREADS=1 # - ANYODE_NUM_THREADS=2 - # - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost_1_74_p/include + # - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.76.0/include # - LIBRARY_PATH=/usr/lib/x86_64-linux-gnu # - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - # - SUNDBASE=/opt/sundials-5.4.0-klu-lapack + # - SUNDBASE=/opt/sundials-5.7.0-release # commands: # - apt-get update && apt-get --assume-yes install python2-dev # - bash -c "ulimit -v 2048000; .ci/test_py2.sh pyodesys $SUNDBASE" test-sund-5.4.0-klu: group: testing - image: bjodah/bjodahimg20dev:2.1.1 + image: bjodah/bjodahimg20dot:21.7.b environment: - CC=gcc-10 - CXX=g++-10 - OMP_NUM_THREADS=1 - ANYODE_NUM_THREADS=2 - - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost_1_74_p/include + - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.76.0/include - LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - - SUNDBASE=/opt/sundials-5.4.0-klu-lapack + - SUNDBASE=/opt/sundials-5.7.0-release commands: - bash -c "ulimit -v 2048000; .ci/run_ci.sh pyodesys $SUNDBASE" - ./scripts/prepare_deploy.sh @@ -56,13 +56,13 @@ pipeline: test-sund-5.4.0-nolapack-noklu-extended: group: testing - image: bjodah/bjodahimg20dev:2.1.1 + image: bjodah/bjodahimg20dot:21.7.b environment: - CC=gcc-10 - CXX=g++-10 - OMP_NUM_THREADS=1 - ANYODE_NUM_THREADS=2 - - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost_1_74_p/include + - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.76.0/include - LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - SUNDBASE=/opt/sundials-5.4.0-noklu-nolapack-extended-int64 From 41d32030a595ddcd70294e5d52a5110181487f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 14 Jul 2021 17:17:35 +0200 Subject: [PATCH 024/102] update CI config --- .drone.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.drone.yml b/.drone.yml index 48a9621e..950499d3 100644 --- a/.drone.yml +++ b/.drone.yml @@ -17,16 +17,16 @@ pipeline: ttl: 90 # liftetime in days # get-pre-reqs: - # image: bjodah/bjodahimg20dev:21.7.b + # image: bjodah/bjodahimg20dev:21.7.c # commands: # - mkdir ./tmp # - git clone --recurse-submodules https://github.com/bjodah/pycvodes.git ./tmp/pycvodes # test-py2: - # image: bjodah/bjodahimg20dev:21.7.b + # image: bjodah/bjodahimg20dev:21.7.c # environment: - # - CC=gcc-10 - # - CXX=g++-10 + # - CC=gcc-11 + # - CXX=g++-11 # - OMP_NUM_THREADS=1 # - ANYODE_NUM_THREADS=2 # - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.76.0/include @@ -37,12 +37,12 @@ pipeline: # - apt-get update && apt-get --assume-yes install python2-dev # - bash -c "ulimit -v 2048000; .ci/test_py2.sh pyodesys $SUNDBASE" - test-sund-5.4.0-klu: + test-sund-5.7.0-rel: group: testing - image: bjodah/bjodahimg20dot:21.7.b + image: bjodah/bjodahimg20dot:21.7.c environment: - - CC=gcc-10 - - CXX=g++-10 + - CC=gcc-11 + - CXX=g++-11 - OMP_NUM_THREADS=1 - ANYODE_NUM_THREADS=2 - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.76.0/include @@ -54,18 +54,18 @@ pipeline: - ./scripts/prepare_deploy.sh - bash -c '[[ $(python3 setup.py --version) =~ ^[0-9]+.* ]]' - test-sund-5.4.0-nolapack-noklu-extended: + test-sund-5.7.0-extended: group: testing - image: bjodah/bjodahimg20dot:21.7.b + image: bjodah/bjodahimg20dot:21.7.c environment: - - CC=gcc-10 - - CXX=g++-10 + - CC=gcc-11 + - CXX=g++-11 - OMP_NUM_THREADS=1 - ANYODE_NUM_THREADS=2 - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.76.0/include - LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - - SUNDBASE=/opt/sundials-5.4.0-noklu-nolapack-extended-int64 + - SUNDBASE=/opt/sundials-5.7.0-extended - PYCVODES_NO_LAPACK=1 - PYCVODES_NO_KLU=1 commands: From a678dcb4dd6f436b851e7f8ee36b4e7ea0cd0934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 14 Jul 2021 18:02:22 +0200 Subject: [PATCH 025/102] point to newer sym branch --- .ci/run_ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/run_ci.sh b/.ci/run_ci.sh index 17eaec01..9b191aa9 100755 --- a/.ci/run_ci.sh +++ b/.ci/run_ci.sh @@ -13,7 +13,7 @@ fi mkdir -p $HOME/.config/pip/ echo -e "[global]\nno-cache-dir = false\ndownload-cache = $(pwd)/ci_cache/pip_cache" >$HOME/.config/pip/pip.conf python3 -m pip install symcxx pysym # unofficial backends, symengine is tested in the conda build -python3 -m pip install https://github.com/bjodah/sym/archive/Lambdify-cse.tar.gz +python3 -m pip install https://github.com/bjodah/sym/archive/jun21.tar.gz # (cd ./tmp/pycvodes; SUND_CFLAGS="-isystem $SUNDBASE/include $CFLAGS" From 827d03dd5d9f239423202f920312c098b2848dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 14 Jul 2021 18:22:08 +0200 Subject: [PATCH 026/102] latest pycompilation? --- .ci/run_ci.sh | 2 +- .drone.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.ci/run_ci.sh b/.ci/run_ci.sh index 9b191aa9..7c4224a0 100755 --- a/.ci/run_ci.sh +++ b/.ci/run_ci.sh @@ -18,7 +18,7 @@ python3 -m pip install https://github.com/bjodah/sym/archive/jun21.tar.gz # (cd ./tmp/pycvodes; SUND_CFLAGS="-isystem $SUNDBASE/include $CFLAGS" SUND_LDFLAGS="-Wl,--disable-new-dtags -Wl,-rpath,$SUNDBASE/lib -L$SUNDBASE/lib $LDFLAGS" -CFLAGS=$SUND_CFLAGS LDFLAGS=$SUND_LDFLAGS python3 -m pip install pycvodes +CFLAGS="$SUND_CFLAGS" LDFLAGS="$SUND_LDFLAGS" python3 -m pip install pycvodes git clean -xfd # -e tmp/ # export CPATH=$SUNDBASE/include diff --git a/.drone.yml b/.drone.yml index 950499d3..8007ca72 100644 --- a/.drone.yml +++ b/.drone.yml @@ -50,6 +50,7 @@ pipeline: - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - SUNDBASE=/opt/sundials-5.7.0-release commands: + - python3 -m pip install https://github.com/bjodah/pycompilation/archive/master.tar.gz - bash -c "ulimit -v 2048000; .ci/run_ci.sh pyodesys $SUNDBASE" - ./scripts/prepare_deploy.sh - bash -c '[[ $(python3 setup.py --version) =~ ^[0-9]+.* ]]' From 05c483746287f4fca1998b168fe20d6df8788cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 14 Jul 2021 18:38:58 +0200 Subject: [PATCH 027/102] deepcopy perhaps? --- pyodesys/native/_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 55d6ed9f..53c557a0 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -5,6 +5,7 @@ from functools import reduce import logging from operator import add +import copy import os import shutil import sys @@ -46,7 +47,7 @@ } def get_compile_kwargs(): - kw = _compile_kwargs.copy() + kw = copy.deepcopy(_compile_kwargs) options = os.environ.get("PYODESYS_OPTIONS") if options: kw['options'] = options.split(',') From 6a0278927098eb6516e9aa1c75eb68d8198768aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 14 Jul 2021 23:46:35 +0200 Subject: [PATCH 028/102] Drop opt-in debug statement printing specific cse's in native rhs --- pyodesys/native/sources/odesys_anyode_template.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 9bcaa26f..70d66545 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -158,10 +158,6 @@ namespace odesys_anyode { %for i, expr in enumerate(p_rhs['exprs']): f[${i}] = ${expr}; %endfor - %if os.environ.get("MY_DEBUG_EMITTING_TOKEN", "") == "kinetg2021fricke" and os.environ.get("IN_DOCKER", "0") == "2" and p_rhs['cses']: - <%doc>DO-NOT-MERGE! - std::clog << nfev << ", t=" << x << ", cse11=" << cse11 << ", cse12=" << cse12 << ", cse17=" << cse17 << ", f[1, Fe+3]=" << f[1] << '\n'; - %endif this->nfev++; %if p_support_recoverable_error: if (m_error_outside_bounds){ From 73666d064e1ff19e9d20106653ed1f8c4891a369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Thu, 29 Jul 2021 12:49:51 +0200 Subject: [PATCH 029/102] bump anyode --- external/anyode | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/anyode b/external/anyode index a63bd019..46e72656 160000 --- a/external/anyode +++ b/external/anyode @@ -1 +1 @@ -Subproject commit a63bd019436ae2c7822757c5033e715cf2e62913 +Subproject commit 46e72656d1dca3e02ca0f759d474dac8918ebed2 From 69ae5201db4c8b5f847e5f7db5f816154c200b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 3 Aug 2021 11:48:26 +0200 Subject: [PATCH 030/102] bump anyode commit --- external/anyode | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/anyode b/external/anyode index 46e72656..c06c0dd1 160000 --- a/external/anyode +++ b/external/anyode @@ -1 +1 @@ -Subproject commit 46e72656d1dca3e02ca0f759d474dac8918ebed2 +Subproject commit c06c0dd1f50b6e7bca8db4d0e9d017d026b05936 From 96d31c6eb0e41b0159affc5703f711b734f19ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sat, 7 Aug 2021 17:07:12 +0200 Subject: [PATCH 031/102] stub for neumaier summation in generated code (currently CSEs will interfere) --- pyodesys/native/_base.py | 66 ++++++++++++++------ pyodesys/native/sources/add_neumaier_rvt.hpp | 36 +++++++++++ 2 files changed, 82 insertions(+), 20 deletions(-) create mode 100644 pyodesys/native/sources/add_neumaier_rvt.hpp diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 53c557a0..51335fb7 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -11,6 +11,7 @@ import sys import tempfile +from sympy.printing.cxx import CXX17CodePrinter import numpy as np import pkg_resources @@ -46,17 +47,36 @@ 'cplus': True, } + +class CXXNeumaier(CXX17CodePrinter): + func_template_name = "add_neumaier" + + def func_template_code(self): + source_snippet_path = pkg_resources.resource_filename( + __name__, 'sources/add_neumaier_rvt.hpp' % self.wrapper_name) + with open(source_snippet_path) as ifh: + source = ifh.read() + return source.replace("add_neumaier_rvt", self.func_template_name) + + def _print_Add(self, expr, *args, **kwargs): + if len(expr.args) > 2: + return f"{self.func_template_name}({', '.join(map(self._print, expr.args))})" + else: + return super()._print_Add(expr, *args, **kwargs) + + def get_compile_kwargs(): kw = copy.deepcopy(_compile_kwargs) - options = os.environ.get("PYODESYS_OPTIONS") - if options: + if options := os.environ.get("PYODESYS_OPTIONS"): kw['options'] = options.split(',') return kw + _ext_suffix = '.so' # sysconfig.get_config_var('EXT_SUFFIX') _obj_suffix = '.o' # os.path.splitext(_ext_suffix)[0] + '.o' # '.obj' + class _NativeCodeBase(Cpp_Code): """ Base class for generated code. @@ -132,6 +152,13 @@ def __init__(self, odesys, *args, use_cse=None, **kwargs): raise OSError("Failed to place prebuilt file at: %s" % _dest) super(_NativeCodeBase, self).__init__(*args, logger=logger, **kwargs) + def _ccode(self, expr, subsd): + expr_x = expr.xreplace(subsd) + if self.compensated_summation: + return CXXNeumaier().doprint(expr_x) + else: + return self.odesys.be.ccode(expr_x) + def variables(self): ny = self.odesys.ny if self.odesys.band is not None: @@ -178,9 +205,6 @@ def common_cse_symbols(): yield self.odesys.be.Symbol('m_p_cse[%d]' % idx) idx += 1 - def _ccode(expr): - return self.odesys.be.ccode(expr.xreplace(subsd)) - if self.use_cse: cse_cb = self.odesys.be.cse else: @@ -234,6 +258,8 @@ def _ccode(expr): self.odesys.roots, symbols=self.odesys.be.numbered_symbols('cse')) + ccode = lambda x: self._ccode(x, subsd) + ns = dict( _message_for_rendered=[ "-*- mode: read-only -*-", @@ -242,40 +268,40 @@ def _ccode(expr): ], p_odesys=self.odesys, p_common={ - 'cses': [(symb.name, _ccode(expr)) for symb, expr in common_cses], + 'cses': [(symb.name, ccode(expr)) for symb, expr in common_cses], 'nsubs': len(common_cse_subs) }, p_rhs={ - 'cses': [(symb.name, _ccode(expr)) for symb, expr in rhs_cses], - 'exprs': list(map(_ccode, rhs_exprs)) + 'cses': [(symb.name, ccode(expr)) for symb, expr in rhs_cses], + 'exprs': list(map(ccode, rhs_exprs)) }, p_jtimes=None if jtimes is False else{ - 'cses': [(symb.name, _ccode(expr)) for symb, expr in jtimes_cses], - 'exprs': list(map(_ccode, jtimes_exprs)) + 'cses': [(symb.name, ccode(expr)) for symb, expr in jtimes_cses], + 'exprs': list(map(ccode, jtimes_exprs)) }, p_jac_dense=None if jac is False or nnz >= 0 else { - 'cses': [(symb.name, _ccode(expr)) for symb, expr in jac_cses], - 'exprs': {(idx//ny, idx % ny): _ccode(expr) + 'cses': [(symb.name, ccode(expr)) for symb, expr in jac_cses], + 'exprs': {(idx//ny, idx % ny): ccode(expr) for idx, expr in enumerate(jac_exprs[:ny*ny])}, - 'dfdt_exprs': list(map(_ccode, jac_exprs[ny*ny:])) + 'dfdt_exprs': list(map(ccode, jac_exprs[ny*ny:])) }, p_jac_sparse=None if jac is False or nnz < 0 else { - 'cses': [(symb.name, _ccode(expr)) for symb, expr in jac_cses], - 'exprs': list(map(_ccode, jac_exprs[:nj])), + 'cses': [(symb.name, ccode(expr)) for symb, expr in jac_cses], + 'exprs': list(map(ccode, jac_exprs[:nj])), 'colptrs': self.odesys._colptrs, 'rowvals': self.odesys._rowvals }, p_first_step=None if first_step is None else { 'cses': first_step_cses, - 'expr': _ccode(first_step_exprs[0]), + 'expr': ccode(first_step_exprs[0]), }, p_roots=None if self.odesys.roots is None else { - 'cses': [(symb.name, _ccode(expr)) for symb, expr in roots_cses], - 'exprs': list(map(_ccode, roots_exprs)) + 'cses': [(symb.name, ccode(expr)) for symb, expr in roots_cses], + 'exprs': list(map(ccode, roots_exprs)) }, p_invariants=None if all_invar == () else { - 'cses': [(symb.name, _ccode(expr)) for symb, expr in invar_cses], - 'exprs': list(map(_ccode, invar_exprs)) + 'cses': [(symb.name, ccode(expr)) for symb, expr in invar_cses], + 'exprs': list(map(ccode, invar_exprs)) }, p_nroots=self.odesys.nroots, p_constructor=[], diff --git a/pyodesys/native/sources/add_neumaier_rvt.hpp b/pyodesys/native/sources/add_neumaier_rvt.hpp new file mode 100644 index 00000000..6b74830c --- /dev/null +++ b/pyodesys/native/sources/add_neumaier_rvt.hpp @@ -0,0 +1,36 @@ +// -*- eval: (read-only-mode); -*- +#pragma once +#include + +namespace { +template +void add_neumaier_rvt_(std::array &work, T arg, Ts... args) { + do { + T tmp = (work[0]) + (arg); + if ((((tmp) < 0) ? -(tmp) : (tmp)) > (((arg) < 0) ? -(arg) : (arg))) { + work[1] += T{(work[0]) - (tmp)} + (arg); + } else { + work[1] += T{(arg) - (tmp)} + (work[0]); + } + work[0] = (tmp); + } while (0); + if constexpr (sizeof...(args) > 0) { + add_neumaier_rvt_(work, args...); + } +} +} // namespace + +template T add_neumaier_rvt(T arg, Ts... args) { + std::array work{}; + do { + T tmp = (work[0]) + (arg); + if ((((tmp) < 0) ? -(tmp) : (tmp)) > (((arg) < 0) ? -(arg) : (arg))) { + work[1] += T{(work[0]) - (tmp)} + (arg); + } else { + work[1] += T{(arg) - (tmp)} + (work[0]); + } + work[0] = (tmp); + } while (0); + add_neumaier_rvt_(work, args...); + return ((work[0]) + (work[1])); +} From 4fbc2b7fa6cdb36d68dc47e07e60bd0c3bd44854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sat, 7 Aug 2021 20:35:33 +0200 Subject: [PATCH 032/102] compensated summation --- pyodesys/native/_base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 51335fb7..2669449d 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -150,6 +150,7 @@ def __init__(self, odesys, *args, use_cse=None, **kwargs): shutil.rmtree(tmpdir) if not os.path.exists(_dest): raise OSError("Failed to place prebuilt file at: %s" % _dest) + self.compensated_summation = os.envron.get("PYODESYS_COMPENSATED_SUMMATION", "0") == "1" super(_NativeCodeBase, self).__init__(*args, logger=logger, **kwargs) def _ccode(self, expr, subsd): From 0067845d5a4e53498ad4e2b2e1325f4ec0c378ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sat, 7 Aug 2021 22:07:00 +0200 Subject: [PATCH 033/102] typo --- pyodesys/native/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 2669449d..310cb854 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -150,7 +150,7 @@ def __init__(self, odesys, *args, use_cse=None, **kwargs): shutil.rmtree(tmpdir) if not os.path.exists(_dest): raise OSError("Failed to place prebuilt file at: %s" % _dest) - self.compensated_summation = os.envron.get("PYODESYS_COMPENSATED_SUMMATION", "0") == "1" + self.compensated_summation = os.environ.get("PYODESYS_COMPENSATED_SUMMATION", "0") == "1" super(_NativeCodeBase, self).__init__(*args, logger=logger, **kwargs) def _ccode(self, expr, subsd): From 112d25e9928a14b812b824119b9b8d24c421a0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 16 Aug 2021 22:45:33 +0200 Subject: [PATCH 034/102] other method for Neumaier compensation --- pyodesys/native/_base.py | 44 +- pyodesys/native/sources/add_neumaier_rvt.hpp | 36 -- .../native/sources/odesys_anyode_template.cpp | 40 +- pyodesys/native/sympy_interface.py | 443 ++++++++++++++++++ 4 files changed, 480 insertions(+), 83 deletions(-) delete mode 100644 pyodesys/native/sources/add_neumaier_rvt.hpp create mode 100644 pyodesys/native/sympy_interface.py diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 310cb854..0d65b7be 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -48,23 +48,6 @@ } -class CXXNeumaier(CXX17CodePrinter): - func_template_name = "add_neumaier" - - def func_template_code(self): - source_snippet_path = pkg_resources.resource_filename( - __name__, 'sources/add_neumaier_rvt.hpp' % self.wrapper_name) - with open(source_snippet_path) as ifh: - source = ifh.read() - return source.replace("add_neumaier_rvt", self.func_template_name) - - def _print_Add(self, expr, *args, **kwargs): - if len(expr.args) > 2: - return f"{self.func_template_name}({', '.join(map(self._print, expr.args))})" - else: - return super()._print_Add(expr, *args, **kwargs) - - def get_compile_kwargs(): kw = copy.deepcopy(_compile_kwargs) if options := os.environ.get("PYODESYS_OPTIONS"): @@ -155,10 +138,7 @@ def __init__(self, odesys, *args, use_cse=None, **kwargs): def _ccode(self, expr, subsd): expr_x = expr.xreplace(subsd) - if self.compensated_summation: - return CXXNeumaier().doprint(expr_x) - else: - return self.odesys.be.ccode(expr_x) + return self.odesys.be.ccode(expr_x) def variables(self): ny = self.odesys.ny @@ -207,7 +187,18 @@ def common_cse_symbols(): idx += 1 if self.use_cse: - cse_cb = self.odesys.be.cse + if self.compensated_summation: + def cse_cb(exprs, **kwargs): + from .sympy_interface import _NeumaierTransformer + from sympy import cse + nm = _NeumaierTransformer(*cse(exprs)) + return nm.statements, nm.final_exprs + else: + from sympy.codegen.ast import Assigment + from sympy import cse + def cse_cb(exprs, **kwargs): + repl, new_exprs = cse(exprs, **kwargs) + return [Assigment(lhs, rhs) for lhs, rhs in repl], new_exprs else: logger.info("Not using common subexpression elimination (disabled by PYODESYS_NATIVE_CSE)") cse_cb = lambda exprs, **kwargs: ([], exprs) @@ -219,13 +210,12 @@ def common_cse_symbols(): common_cse_subs = {} comm_cse_symbs = common_cse_symbols() - for symb, subexpr in common_cses: + for st in common_cses: for expr in common_exprs: - if symb in expr.free_symbols: - common_cse_subs[symb] = next(comm_cse_symbs) + if st.lhs in expr.free_symbols: + common_cse_subs[st.lhs] = next(comm_cse_symbs) break - common_cses = [(x.xreplace(common_cse_subs), expr.xreplace(common_cse_subs)) - for x, expr in common_cses] + common_cses = [st.xreplace(common_cse_subs) for st in common_cses] common_exprs = [expr.xreplace(common_cse_subs) for expr in common_exprs] rhs_cses, rhs_exprs = cse_cb( diff --git a/pyodesys/native/sources/add_neumaier_rvt.hpp b/pyodesys/native/sources/add_neumaier_rvt.hpp deleted file mode 100644 index 6b74830c..00000000 --- a/pyodesys/native/sources/add_neumaier_rvt.hpp +++ /dev/null @@ -1,36 +0,0 @@ -// -*- eval: (read-only-mode); -*- -#pragma once -#include - -namespace { -template -void add_neumaier_rvt_(std::array &work, T arg, Ts... args) { - do { - T tmp = (work[0]) + (arg); - if ((((tmp) < 0) ? -(tmp) : (tmp)) > (((arg) < 0) ? -(arg) : (arg))) { - work[1] += T{(work[0]) - (tmp)} + (arg); - } else { - work[1] += T{(arg) - (tmp)} + (work[0]); - } - work[0] = (tmp); - } while (0); - if constexpr (sizeof...(args) > 0) { - add_neumaier_rvt_(work, args...); - } -} -} // namespace - -template T add_neumaier_rvt(T arg, Ts... args) { - std::array work{}; - do { - T tmp = (work[0]) + (arg); - if ((((tmp) < 0) ? -(tmp) : (tmp)) > (((arg) < 0) ? -(arg) : (arg))) { - work[1] += T{(work[0]) - (tmp)} + (arg); - } else { - work[1] += T{(arg) - (tmp)} + (work[0]); - } - work[0] = (tmp); - } while (0); - add_neumaier_rvt_(work, args...); - return ((work[0]) + (work[1])); -} diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 70d66545..bec1506d 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -101,11 +101,11 @@ namespace odesys_anyode { m_special_settings(special_settings) { m_p.assign(params, params + ${len(p_odesys.params) + p_odesys.ny if p_odesys.append_iv else 0}); <% idx = 0 %> - %for cse_token, cse_expr in p_common['cses']: - %if cse_token.startswith('m_p_cse'): - ${cse_token} = ${cse_expr}; <% assert cse_token == 'm_p_cse[{0}]'.format(idx); idx += 1 %> + %for cse_assign in p_common['cses']: + %if cse_assign.lhs.startswith('m_p_cse'): + ${cse_assign} <% assert cse_token == 'm_p_cse[{0}]'.format(idx); idx += 1 %> %else: - const auto ${cse_token} = ${cse_expr}; + const auto ${cse_assign} %endif %endfor use_get_dx_max = (m_get_dx_max_factor > 0.0) ? ${'true' if p_get_dx_max else 'false'} : false; @@ -113,8 +113,8 @@ namespace odesys_anyode { if (m_max_invariant_violation != 0.0){ ${'' if p_odesys.append_iv else 'throw std::runtime_error("append_iv not set to True")'} const realtype * const y = params + ${len(p_odesys.params)}; - %for cse_token, cse_expr in p_invariants['cses']: - const auto ${cse_token} = ${cse_expr}; + %for cse_assign in p_invariants['cses']: + const auto ${cse_assign} %endfor %for expr in p_invariants['exprs']: m_invar0.push_back(${expr}); @@ -151,8 +151,8 @@ namespace odesys_anyode { ${p_rhs} %else: ${'AnyODE::ignore(x);' if p_odesys.autonomous_exprs else ''} - %for cse_token, cse_expr in p_rhs['cses']: - const auto ${cse_token} = ${cse_expr}; + %for cse_assign in p_rhs['cses']: + const auto ${cse_assign} %endfor <% import os %> %for i, expr in enumerate(p_rhs['exprs']): @@ -184,8 +184,8 @@ namespace odesys_anyode { } %if p_invariants is not None: if (m_max_invariant_violation != 0.0){ - %for cse_token, cse_expr in p_invariants['cses']: - const auto ${cse_token} = ${cse_expr}; + %for cse_assign in p_invariants['cses']: + const auto ${cse_assign} %endfor %for idx, invar_expr in enumerate(p_invariants['exprs']): if (std::abs(${invar_expr} - m_invar0[${idx}]) > ((m_max_invariant_violation > 0) @@ -226,8 +226,8 @@ namespace odesys_anyode { AnyODE::ignore(fy); // Currently we are not using fy (could be done through extensive pattern matching) ${'AnyODE::ignore(x);' if p_odesys.autonomous_exprs else ''} - %for cse_token, cse_expr in p_jtimes['cses']: - const auto ${cse_token} = ${cse_expr}; + %for cse_assign in p_jtimes['cses']: + const auto ${cse_assign}; %endfor %for i in range(p_odesys.ny): @@ -262,8 +262,8 @@ namespace odesys_anyode { ${'AnyODE::ignore(y);' if (not any([yi in p_odesys.get_jac().free_symbols for yi in p_odesys.dep]) and not any([yi in p_odesys.get_dfdx().free_symbols for yi in p_odesys.dep])) else ''} - %for cse_token, cse_expr in p_jac_dense['cses']: - const auto ${cse_token} = ${cse_expr}; + %for cse_assign in p_jac_dense['cses']: + const auto ${cse_assign} %endfor %for i_major in range(p_odesys.ny): @@ -301,8 +301,8 @@ namespace odesys_anyode { %elif isinstance(p_first_step, str): ${p_first_step} %else: - %for cse_token, cse_expr in p_first_step['cses']: - const realtype ${cse_token} = ${cse_expr}; + %for cse_assign in p_first_step['cses']: + const realtype ${cse_assign} %endfor ${'' if p_odesys.indep in p_odesys.first_step_expr.free_symbols else 'AnyODE::ignore(x);'} ${'' if any([yi in p_odesys.first_step_expr.free_symbols for yi in p_odesys.dep]) else 'AnyODE::ignore(y);'} @@ -321,8 +321,8 @@ namespace odesys_anyode { ${'AnyODE::ignore(x);' if p_odesys.autonomous_exprs else ''} ${'AnyODE::ignore(y);' if (not any([yi in p_odesys.get_jac().free_symbols for yi in p_odesys.dep]) and not any([yi in p_odesys.get_dfdx().free_symbols for yi in p_odesys.dep])) else ''} - %for cse_token, cse_expr in p_jac_sparse['cses']: - const auto ${cse_token} = ${cse_expr}; + %for cse_assign in p_jac_sparse['cses']: + const auto ${cse_assign} %endfor %for i in range(p_odesys.nnz): @@ -385,8 +385,8 @@ namespace odesys_anyode { %else: ${'' if any(p_odesys.indep in expr.free_symbols for expr in p_odesys.roots) else 'AnyODE::ignore(x);'} - %for cse_token, cse_expr in p_roots['cses']: - const auto ${cse_token} = ${cse_expr}; + %for cse_assign in p_roots['cses']: + const auto ${cse_assign} %endfor %for i, expr in enumerate(p_roots['exprs']): diff --git a/pyodesys/native/sympy_interface.py b/pyodesys/native/sympy_interface.py new file mode 100644 index 00000000..acfb14b4 --- /dev/null +++ b/pyodesys/native/sympy_interface.py @@ -0,0 +1,443 @@ +#!/usr/bin/env python3 +"""Use this utility for symbolic treatment. + +Example usage: + +$ python3 sympy_interface.py demo1 | clang-format --style=Google | batcat -pl C + +""" +import pprint +from collections import defaultdict +from functools import reduce +from operator import add +from sympy import ( + Abs, Add, Basic, ccode, cse, exp, Expr, numbered_symbols, Piecewise, pi, + postorder_traversal, preorder_traversal, pycode, Symbol, Tuple +) +from sympy.codegen import Assignment, aug_assign, CodeBlock +from sympy.codegen.ast import Token, Variable, float64 +from sympy.printing.precedence import PRECEDENCE, precedence + + +class OrderedAdd(Expr): + """Printed with parenthesis, useful for floating point math.""" + + nargs = -1 + precedence = PRECEDENCE["Add"] - 1 + + def _ccode(self, printer): + return " + ".join(printer.parenthesize(arg, precedence(self)) for arg in self.args) + + _pythoncode = _ccode + + +class _NeumaierAdd(Token, Expr): + """Represents KBN compensated summation.""" + + __slots__ = ('terms', 'accum', 'carry', 'temp') + _construct_terms = staticmethod(lambda args: Tuple(*args)) + + def _ccode(self, printer): + terms = ", ".join(map(printer._print, self.terms)) + return f"NA({terms} /*{str(self.accum)[:-1]}*/)" + + def to_statements(self, existing, expanded): + """Transform into statements.""" + neum, ordinary = [], [] + for term in self.terms: + # print(f"/*{term, term in existing}*/") + if term in existing: #.values(): + # neum.append(term) + neum.append(existing[term]) + else: + ordinary.append(term) + # print(neum, ordinary, existing) + st = [] + if neum: + st.append(Assignment(self.accum, sum(na.accum for na in neum))) + st.append(Assignment(self.carry, sum(na.carry for na in neum))) + for na in neum: + expanded.add(na) + else: + st.append(Assignment(self.accum, ordinary.pop(0))) + st.append(Assignment(self.carry, 0)) + + for elem in ordinary: + st.extend(_NeumaierAdd._impl_add(self.accum, self.carry, elem, self.temp)) + expanded.add(self) + return st + + def finalize(self): + """Close the summation.""" + return self._impl_finalize(self.accum, self.carry) + + @staticmethod + def _impl_add(accum, carry, elem, temp): + """Perform Kahan-Babuska-Neumaier addition.""" + big_temp = OrderedAdd(OrderedAdd(accum, -temp), elem) + big_elem = OrderedAdd(OrderedAdd(elem, -temp), accum) + pw = Piecewise((big_temp, Abs(temp) > Abs(elem)), (big_elem, True)) + return [ + Assignment(temp, accum + elem), + aug_assign(carry, '+', pw), + Assignment(accum, temp) + ] + + @staticmethod + def _impl_finalize(accum, carry): + return Add(accum, carry) + + +class _NeumaierTransformer: + """Transform Add instances in CSEs to use compensated sum. + + Parameters + ---------- + up_to: int, [0-100] + Code is guaranteed to compile at levels 0 (no passes, no compensation) + and 100 (all passes). + """ + + def __init__(self, repl, red, *, tmp_pfx="t", neu_pfx="n", up_to=100, limit=3): + pprint.pprint(repl) + pprint.pprint(red) + print("") + self.repl = repl + self.red = red + self.limit = limit + + self.created = {} + self._all_accum = set() + self._all_carry = set() + self.expanded = set() + self._analysis = defaultdict(int) + self._tmp_var = numbered_symbols(tmp_pfx) + self._neu_var = numbered_symbols(neu_pfx) + self.passes = [] + for p in filter(lambda n: n.startswith('_pass_'), dir(self)): + null, rest = p.split("_pass_") + assert null == "" + num, *_ = rest.split("_") + if len(_) == 0: + continue + if int(num) <= up_to: + self.passes.append(getattr(self, p)) + + self.statements, self.final_exprs = self._pipeline() + # print("\ncreated: ", self.created) + # print("") + # print(self._analysis) + # print(self.statements) + + def statements_and_expressions(self): + return self._declare(self.statements), self.final_exprs + + def _mk_Neu(self, terms, lhs): + pfx = str(next(self._tmp_var)) if lhs is None else str(lhs) + accum = Symbol(pfx+'a') + carry = Symbol(pfx+'c') + tempv = Symbol(pfx+'t') + self._all_accum.add(accum) + self._all_carry.add(carry) + return _NeumaierAdd(terms, accum, carry, tempv) + + @staticmethod + def _is_Neu(x): + return isinstance(x, _NeumaierAdd) + + def _declare(self, stmts, *, type_=float64): + seen = set() + result = [] + for st in stmts: + if isinstance(st, Assignment) and st.lhs not in seen: + seen.add(st.lhs) + st = Variable(st.lhs, type=type_).as_Declaration(value=st.rhs) + result.append(st) + return result + + def _pipeline(self): + statements = [Assignment(*lr) for lr in self.repl] + final_exprs = self.red + for pass_ in self.passes: + new_stmts, new_exprs = [], [] + for st in statements: + new_rhs = pass_(st.lhs, st.rhs, statements=new_stmts) + if st.lhs not in self.created: # not Neumaier, (aug)assign: + new_stmts.append(st.__class__(st.lhs, new_rhs)) + for expr in final_exprs: + new_exprs.append(pass_(None, expr, statements=new_stmts)) + statements = new_stmts + final_exprs = new_exprs + # print(f"/*{self._analysis}*/") + return statements, final_exprs + + def _pass_05_analysis(self, lhs, rhs, *, statements): + if rhs.is_Add: + if lhs is not None: + self._analysis[lhs] += len(rhs.args) + for term in rhs.args: + if term in self._analysis: + self._analysis[term] += len(rhs.args) - 1 + return rhs + + def _pass_10_create_nodes(self, lhs, rhs, *, statements, debug=False): + #new_rhs = rhs.xreplace(self.created) + new_rhs = rhs + while True: + for _add in filter(lambda x: x.is_Add, postorder_traversal(new_rhs)): + score = self._analysis.get(lhs, 0) + reduce(add, [self._analysis.get(k, 1) for k in _add.args]) + if score >= self.limit or any(self._is_Neu(arg) for arg in _add.args): + na = self._mk_Neu(_add.args, lhs) + if _add is rhs and lhs is not None: + key = lhs + + else: + key = next(self._neu_var) + #statements.append(Assignment(key, na)) + self.created[key] = na + # print(new_rhs, _add) + new_rhs = new_rhs.xreplace({_add: key}) + break + else: + return new_rhs + + assert False + # if debug and lhs is None: + # print("new_rhs: ", new_rhs) + #return + # if debug and lhs is None: + # print("") + # print("_add: ", _add) + # print("") + # print("na: ", na) + # print("") + # print("new_rhs: ", new_rhs) + + #return new_rhs + + # def _pass_11_(self, lhs, rhs, *, statements): + # return self._pass_10_create_nodes(lhs, rhs, statements=statements, debug=True) + + def _pass_50_to_stmnts(self, lhs, rhs, *, statements): + for arg in postorder_traversal(rhs): + # print(arg, self._is_Neu(arg)) + if arg in self.created: + for t in list(self.created[arg].terms)+[arg]: + if t in self.created: + if arg not in self.expanded: + statements.extend(self.created[t].to_statements(self.created, self.expanded)) + # elif self._is_Neu(arg): + # print(".") + # statements.extend(arg.to_statements(self.created, self.expanded)) + return rhs + + def _pass_60_xrepl(self, lhs, rhs, *, statements): + return rhs.xreplace(self.created) + + def _group(self, x): + # print(self.created) + # print(x)### + all_accum, all_carry = [], [] + for term in x.args: + if term in self.created: + all_accum.append(self.created[term].accum) + all_carry.append(self.created[term].carry) + elif term in self._all_accum: + all_accum.append(term) + elif term in self._all_carry: + all_carry.append(term) + else: + all_accum.append(term) + # print('//', all_accum, all_carry) + if all_accum and all_carry: + return OrderedAdd(OrderedAdd(*all_accum), OrderedAdd(*all_carry)) + else: + return OrderedAdd(*(all_accum+all_carry)) + + def _pass_95_group(self, lhs, rhs, *, statements): + # Not sure if this pass is still needed + return rhs.replace(lambda s: s.is_Add and any( + t in self.created or t in self._all_accum or t in self._all_carry for t in s.args + ), self._group) + + def _pass_90_fin(self, lhs, rhs, *, statements): + return rhs.replace(lambda x: self._is_Neu(x), lambda x: x.finalize()) + + +def _compensated_code(case, **kwargs): + repl, red = cse(case.exprs) + nm = _NeumaierTransformer(repl, red, **kwargs) + statements, new_exprs = nm.statements_and_expressions() + for i, new_expr in enumerate(new_exprs): + statements.append(Assignment(Symbol("out[%d]" % i), new_expr)) + return CodeBlock(*statements) + + +class Case: + """test case.""" + + vals_ref = None + + @classmethod + def check(cls): + """Perform self-check.""" + return _NeumaierTransformer(*cse(cls.exprs)) + + +class _Case0(Case): + x = list(map(Symbol, ["y[%d]" % i for i in range(3)])) + a = reduce(add, x[:3]) + exprs = [ + a, + a+1, + ] + + +class _Case1(Case): + x = list(map(Symbol, ["y[%d]" % i for i in range(6)])) + a = reduce(add, x[:3]) + b = reduce(add, x[3:6]) + exprs = [ + a, + b, + a + b, + a + b + 1, + ] + + +class _Case2(Case): + x = list(map(Symbol, ["y[%d]" % i for i in range(6)])) + a = reduce(add, x[:3]) + b = reduce(add, x[3:6]) + exprs = [ + a, + b, + a + b + 1, + a + b + 1 + pi, + ] + + +class _Case3(Case): + x = list(map(Symbol, ["y[%d]" % i for i in range(10)])) + a = reduce(add, x[:3]) + b = reduce(add, x[3:6]) + c = reduce(add, x[6:9]) + exprs = [ + a + 1, + b + 2, + c + 3, + a + b + x[9], + a + b + c + x[8] + ] + + +class _Case4(Case): + x = list(map(Symbol, ["y[%d]" % i for i in range(3)])) + a = 1*x[0] + b = 2*x[1] + c = 3*x[2] + exprs = [ + a + b, + a + b + c, + a + b + c + 1 + ] + + +def real(name): + return Symbol(name, real=True) + + +class _Case5(Case): + x = list(map(real, ["y[%d]" % i for i in range(5)])) + a = x[0]*x[1]**2 + x[2]**3 + x[3]**4 + b = 3*x[3] + 4*x[4] + c = exp(1*x[1] + 2*x[2] + b) + d = (1*x[1] + 2*x[2] + 3*x[3])/b + exprs = [a, a/42, b+c, c*d, b+exp(x[0])] + + +class _Case6(Case): + x = list(map(Symbol, ["y[%d]" % i for i in range(2)])) + a = reduce(add, x[:]) + exprs = [ + a, + a+1, + ] + + +class _Case7(Case): # no Neumaier + x = list(map(Symbol, ["y[%d]" % i for i in range(2)])) + a = reduce(add, x[:]) + exprs = [ + a, + a/3, + ] + + @classmethod + def check(cls): + tr = super().check() + assert(len(tr.statements) <= 1) + + +class _Case8(Case): + x = list(map(Symbol, ["y[%d]" % i for i in range(6)])) + a = reduce(add, x[:3]) + b = reduce(add, x[3:]) + exprs = [ + a, + b, + a+b + ] + +class _Case9(Case): + x = list(map(Symbol, ["y[%d]" % i for i in range(6)])) + a = reduce(add, x[:3]) + b = reduce(add, x[3:]) + exprs = [ + exp(a) + exp(b), + a+b + ] + +class _Case10(Case): + x = list(map(Symbol, ["y[%d]" % i for i in range(6)])) + a = reduce(add, x[:3]) + b = reduce(add, x[3:]) + exprs = [ + a/b, + 2*a + 3*b + ] + +class _Case11(Case): + x = list(map(Symbol, ["y[%d]" % i for i in range(4)])) + exprs = [sum(x)] + vals_ref = ([1e14, 1.0, -1e14, 1.0], [2.0]) + + +cases = [v for k, v in globals().items() + if k.startswith("_Case") and Case in v.__mro__] + +template_c = ( + "#include \n" + "void f(double * const out, const double * const y) {%s}" +) +template_py = "def f(out, y):\n %s" + +if __name__ == '__main__': + + def demo_c(index=0, up_to=100, limit=3): + """Demo C-code.""" + case = cases[index] + print('/* ' + str(case.exprs)) + code_block = _compensated_code(case, up_to=up_to, limit=limit) + print('*/') + # print(code_block) + src = ccode(code_block) + print(template_c % src) + + def demo_py(index=0, up_to=100, limit=3): + """Demo Python-code.""" + src = pycode(_compensated_code(cases[index], up_to=up_to, limit=limit)) + print(template_py % src.replace('\n', '\n ')) + + import argh + argh.dispatch_commands([demo_c, demo_py]) From d5784448e58746e1e21638c7a91b7f2f697cf080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Fri, 20 Aug 2021 22:47:49 +0200 Subject: [PATCH 035/102] in the middle of refactoring cse logic in native --- pyodesys/native/_base.py | 149 +++++------ pyodesys/native/compensated.py | 236 ++++++++++++++++++ pyodesys/native/core.py | 45 ++++ pyodesys/native/groupwise.py | 143 +++++++++++ .../native/sources/odesys_anyode_template.cpp | 127 +++++----- pyodesys/native/sympy_interface.py | 6 +- 6 files changed, 539 insertions(+), 167 deletions(-) create mode 100644 pyodesys/native/compensated.py create mode 100644 pyodesys/native/core.py create mode 100644 pyodesys/native/groupwise.py diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 0d65b7be..f1981b16 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -18,6 +18,8 @@ from ..symbolic import SymbolicSys from .. import __version__ +from .groupwise import GroupwiseCSE + try: import appdirs except ImportError: @@ -133,8 +135,8 @@ def __init__(self, odesys, *args, use_cse=None, **kwargs): shutil.rmtree(tmpdir) if not os.path.exists(_dest): raise OSError("Failed to place prebuilt file at: %s" % _dest) - self.compensated_summation = os.environ.get("PYODESYS_COMPENSATED_SUMMATION", "0") == "1" - super(_NativeCodeBase, self).__init__(*args, logger=logger, **kwargs) + self.compensated_summation = kwargs.pop("compensated_summation", os.environ.get("PYODESYS_COMPENSATED_SUMMATION", "0") == "1") + super().__init__(*args, logger=logger, **kwargs) def _ccode(self, expr, subsd): expr_x = expr.xreplace(subsd) @@ -149,14 +151,17 @@ def variables(self): ninvar = len(all_invar) jac = self.odesys.get_jac() nnz = self.odesys.nnz - all_exprs = self.odesys.exprs + all_invar + all_exprs = dict( + rhs=self.odesys.exprs, + invar=all_invar + ) if jac is not False and nnz < 0: jac_dfdx = list(reduce(add, jac.tolist() + self.odesys.get_dfdx().tolist())) - all_exprs += tuple(jac_dfdx) + all_exprs["jac_dfdx"] = jac_dfdx nj = len(jac_dfdx) elif jac is not False and nnz >= 0: jac_dfdx = list(reduce(add, jac.tolist())) - all_exprs += tuple(jac_dfdx) + all_exprs["jac_dfdx"] = jac_dfdx nj = len(jac_dfdx) else: nj = 0 @@ -164,79 +169,14 @@ def variables(self): jtimes = self.odesys.get_jtimes() if jtimes is not False: v, jtimes_exprs = jtimes - all_exprs += tuple(jtimes_exprs) - njtimes = len(jtimes_exprs) + all_exprs["jtimes"] = jtimes_exprs else: v = () jtimes_exprs = () - njtimes = 0 - subsd = {k: self.odesys.be.Symbol('y[%d]' % idx) for - idx, k in enumerate(self.odesys.dep)} - subsd[self.odesys.indep] = self.odesys.be.Symbol('x') if jtimes is not False: subsd.update({k: self.odesys.be.Symbol('v[%d]' % idx) for idx, k in enumerate(v)}) - subsd.update({k: self.odesys.be.Symbol('m_p[%d]' % idx) for - idx, k in enumerate(self.odesys.params)}) - - def common_cse_symbols(): - idx = 0 - while True: - yield self.odesys.be.Symbol('m_p_cse[%d]' % idx) - idx += 1 - - if self.use_cse: - if self.compensated_summation: - def cse_cb(exprs, **kwargs): - from .sympy_interface import _NeumaierTransformer - from sympy import cse - nm = _NeumaierTransformer(*cse(exprs)) - return nm.statements, nm.final_exprs - else: - from sympy.codegen.ast import Assigment - from sympy import cse - def cse_cb(exprs, **kwargs): - repl, new_exprs = cse(exprs, **kwargs) - return [Assigment(lhs, rhs) for lhs, rhs in repl], new_exprs - else: - logger.info("Not using common subexpression elimination (disabled by PYODESYS_NATIVE_CSE)") - cse_cb = lambda exprs, **kwargs: ([], exprs) - - common_cses, common_exprs = cse_cb( - all_exprs, symbols=self.odesys.be.numbered_symbols('cse_temporary'), - ignore=(self.odesys.indep,) + self.odesys.dep + v) - - common_cse_subs = {} - comm_cse_symbs = common_cse_symbols() - - for st in common_cses: - for expr in common_exprs: - if st.lhs in expr.free_symbols: - common_cse_subs[st.lhs] = next(comm_cse_symbs) - break - common_cses = [st.xreplace(common_cse_subs) for st in common_cses] - common_exprs = [expr.xreplace(common_cse_subs) for expr in common_exprs] - - rhs_cses, rhs_exprs = cse_cb( - common_exprs[:ny], - symbols=self.odesys.be.numbered_symbols('cse')) - - if all_invar: - invar_cses, invar_exprs = cse_cb( - common_exprs[ny:(ny + ninvar)], - symbols=self.odesys.be.numbered_symbols('cse') - ) - - if jac is not False: - jac_cses, jac_exprs = cse_cb( - common_exprs[(ny + ninvar):(ny + ninvar + nj)], - symbols=self.odesys.be.numbered_symbols('cse')) - - if jtimes is not False: - jtimes_cses, jtimes_exprs = cse_cb( - common_exprs[(ny + ninvar + nj):(ny + ninvar + nj + njtimes)], - symbols=self.odesys.be.numbered_symbols('cse')) first_step = self.odesys.first_step_expr if first_step is not None: @@ -249,7 +189,40 @@ def cse_cb(exprs, **kwargs): self.odesys.roots, symbols=self.odesys.be.numbered_symbols('cse')) - ccode = lambda x: self._ccode(x, subsd) + + subsd = {k: self.odesys.be.Symbol('y[%d]' % idx) for + idx, k in enumerate(self.odesys.dep)} + subsd[self.odesys.indep] = self.odesys.be.Symbol('x') + + subsd.update({k: self.odesys.be.Symbol('m_p[%d]' % idx) for + idx, k in enumerate(self.odesys.params)}) + + if self.use_cse: + if self.compensated_summation: + from .compensated import _NeumaierTransformer as Transformer + else: + from .core import NullTransformer as Transformer + gw = GroupwiseCSE( + all_exprs, + common_cse_template="m_cse[{}]", + common_ignore=(self.odesys.indep,) + self.odesys.dep + v, + to_code=lambda x: ccode(x, math_macros={}), + subsd=subsd, + Transformer=Transformer + ) + def _block(k, assign_to=lambda i: _r("out[%d]" % i)): + return CodeBlock(*(gw.assignment(k, declare=lambda s: '[' not in s.name) + [ + Assignment(assign_to(i), e) for i, e in enumerate(gw.exprs(k)) + ])) + + src = {k: gw.render(_block(k)) for k in gw.keys} + else: + logger.info("Not using common subexpression elimination (disabled by PYODESYS_NATIVE_CSE)") + def _block(exprs, assign_to=lambda i: _r("out[%d]" % i)): + return CodeBlock(*[ + Assignment(assign_to(i), e) for i, e in enumerate(exprs) + ]).xreplace(subsd) + src = {k: self.odesys.be.ccode(_block(exprs)) for k, exprs in all_exprs.items()} ns = dict( _message_for_rendered=[ @@ -257,42 +230,34 @@ def cse_cb(exprs, **kwargs): "This file was generated using pyodesys-%s at %s" % ( __version__, dt.now().isoformat()) ], - p_odesys=self.odesys, p_common={ - 'cses': [(symb.name, ccode(expr)) for symb, expr in common_cses], - 'nsubs': len(common_cse_subs) - }, + 'assign': src["common"] + } + p_odesys=self.odesys, + p_src=src, p_rhs={ - 'cses': [(symb.name, ccode(expr)) for symb, expr in rhs_cses], - 'exprs': list(map(ccode, rhs_exprs)) + 'assign': src["rhs"] }, p_jtimes=None if jtimes is False else{ - 'cses': [(symb.name, ccode(expr)) for symb, expr in jtimes_cses], - 'exprs': list(map(ccode, jtimes_exprs)) + 'assign': src["jtimes"] }, p_jac_dense=None if jac is False or nnz >= 0 else { - 'cses': [(symb.name, ccode(expr)) for symb, expr in jac_cses], - 'exprs': {(idx//ny, idx % ny): ccode(expr) - for idx, expr in enumerate(jac_exprs[:ny*ny])}, - 'dfdt_exprs': list(map(ccode, jac_exprs[ny*ny:])) + 'assign': src["jac_dfdt"] }, p_jac_sparse=None if jac is False or nnz < 0 else { - 'cses': [(symb.name, ccode(expr)) for symb, expr in jac_cses], - 'exprs': list(map(ccode, jac_exprs[:nj])), + 'assign': src["jac_dfdt"] 'colptrs': self.odesys._colptrs, 'rowvals': self.odesys._rowvals }, p_first_step=None if first_step is None else { - 'cses': first_step_cses, - 'expr': ccode(first_step_exprs[0]), + 'assign': src["first_step"] }, p_roots=None if self.odesys.roots is None else { - 'cses': [(symb.name, ccode(expr)) for symb, expr in roots_cses], - 'exprs': list(map(ccode, roots_exprs)) + 'assign': src["roots"] }, p_invariants=None if all_invar == () else { - 'cses': [(symb.name, ccode(expr)) for symb, expr in invar_cses], - 'exprs': list(map(ccode, invar_exprs)) + 'assign': src["invariants"] + 'n_invar': len(all_invar) }, p_nroots=self.odesys.nroots, p_constructor=[], diff --git a/pyodesys/native/compensated.py b/pyodesys/native/compensated.py new file mode 100644 index 00000000..d3533214 --- /dev/null +++ b/pyodesys/native/compensated.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Use this utility for symbolic treatment. + +Example usage: + +$ python3 compensated_cse.py demo1 | clang-format --style=Google | batcat -pl C + +""" +import pprint +from collections import defaultdict +from functools import reduce +from operator import add +from sympy import ( + Abs, Add, Basic, cse, exp, Expr, numbered_symbols, Piecewise, pi, + postorder_traversal, preorder_traversal, pycode, Symbol, Tuple +) +from sympy.codegen import Assignment, aug_assign, CodeBlock +from sympy.codegen.ast import Token, Variable, float64, value_const + +from .core import NullTransformer +from .util import OrderedAdd + +class _NeumaierAdd(Token, Expr): + """Represents KBN compensated summation.""" + + __slots__ = ('terms', 'accum', 'carry', 'temp') + _construct_terms = staticmethod(lambda args: Tuple(*args)) + + def _ccode(self, printer): + terms = ", ".join(map(printer._print, self.terms)) + return f"NA({terms} /*{str(self.accum)[:-1]}*/)" + + def to_statements(self, existing, expanded): + """Transform into statements.""" + neum, ordinary = [], [] + for term in self.terms: + if term in existing: + neum.append(existing[term]) + else: + ordinary.append(term) + st = [] + if neum: + st.append(Assignment(self.accum, sum(na.accum for na in neum))) + st.append(Assignment(self.carry, sum(na.carry for na in neum))) + for na in neum: + expanded.add(na) + else: + st.append(Assignment(self.accum, ordinary.pop(0))) + st.append(Assignment(self.carry, 0)) + + for elem in ordinary: + st.extend(_NeumaierAdd._impl_add(self.accum, self.carry, elem, self.temp)) + expanded.add(self) + return st + + def finalize(self): + """Close the summation.""" + return self._impl_finalize(self.accum, self.carry) + + @staticmethod + def _impl_add(accum, carry, elem, temp): + """Perform Kahan-Babuska-Neumaier addition.""" + big_temp = OrderedAdd(OrderedAdd(accum, -temp), elem) + big_elem = OrderedAdd(OrderedAdd(elem, -temp), accum) + pw = Piecewise((big_temp, Abs(temp) > Abs(elem)), (big_elem, True)) + return [ + Assignment(temp, accum + elem), + aug_assign(carry, '+', pw), + Assignment(accum, temp) + ] + + @staticmethod + def _impl_finalize(accum, carry): + return Add(accum, carry) + + +class _NeumaierTransformer(NullTransformer): + """Transform Add instances in CSEs to use compensated sum. + + Parameters + ---------- + up_to: int, [0-100] + Code is guaranteed to compile at levels 0 (no passes, no compensation) + and 100 (all passes). + """ + + def __init__(self, repl, red, *, tmp_pfx="t", neu_pfx="n", up_to=100, limit=3, parent=None, ignore=None): + self.repl = repl + self.red = red + self.limit = limit + self.parent = parent + self.ignore = ignore + + self.created = {} + self._all_accum = {} + self._all_carry = {} + self._all_tempv = {} + self.expanded = set() + self._analysis = defaultdict(int) + self._tmp_var = numbered_symbols(tmp_pfx) + self._neu_var = numbered_symbols(neu_pfx) + self.passes = [] + for p in filter(lambda n: n.startswith('_pass_'), dir(self)): + null, rest = p.split("_pass_") + assert null == "" + num, *_ = rest.split("_") + if len(_) == 0: + continue + if int(num) <= up_to: + self.passes.append(getattr(self, p)) + + self.statements, self.final_exprs = self._pipeline() + + def remapping_for_arrayification(self, template="m_glob[{0}]"): + remapping = {} + i = 0 + for st in self.statements: + if st.lhs in remapping: + continue + if st.lhs in self._all_accum or st.lhs in self._all_carry: + remapping[st.lhs] = Symbol(template.format(i), real=True) + elif st.lhs in self._all_tempv: + pass + else: + remapping[st.lhs] = Symbol(template.format(i), real=True) + i = i + 1 + return remapping + + def _mk_Neu(self, terms, lhs): + pfx = str(next(self._tmp_var)) if lhs is None else str(lhs) + accum = Symbol(pfx+'a') + carry = Symbol(pfx+'c') + tempv = Symbol(pfx+'t') + na = _NeumaierAdd(terms, accum, carry, tempv) + self._all_accum[accum] = na + self._all_carry[carry] = na + self._all_tempv[tempv] = na + return na + + @staticmethod + def _is_Neu(x): + return isinstance(x, _NeumaierAdd) + + def _pipeline(self): + statements = [Assignment(*lr) for lr in self.repl] + final_exprs = self.red + for pass_ in self.passes: + new_stmts, new_exprs = [], [] + for st in statements: + new_rhs = pass_(st.lhs, st.rhs, statements=new_stmts) + if st.lhs not in self.created: # not Neumaier, (aug)assign: + new_stmts.append(st.__class__(st.lhs, new_rhs)) + for expr in final_exprs: + new_exprs.append(pass_(None, expr, statements=new_stmts)) + statements = new_stmts + final_exprs = new_exprs + return statements, final_exprs + + def _pass_05_analysis(self, lhs, rhs, *, statements): + if rhs.is_Add: + if lhs is not None: + self._analysis[lhs] += len(rhs.args) + for term in rhs.args: + if term in self._analysis: + self._analysis[term] += len(rhs.args) - 1 + return rhs + + def _pass_10_create_nodes(self, lhs, rhs, *, statements, debug=False): + new_rhs = rhs + if self.ignore: + if lhs in self.ignore or any(rhs.has(ig) for ig in self.ignore): + return new_rhs + + while True: + for _add in filter(lambda x: x.is_Add, postorder_traversal(new_rhs)): + score = self._analysis.get(lhs, 0) + reduce(add, [self._analysis.get(k, 1) for k in _add.args]) + if score >= self.limit or any(self._is_Neu(arg) for arg in _add.args): + na = self._mk_Neu(_add.args, lhs) + if _add is rhs and lhs is not None: + key = lhs + + else: + key = next(self._neu_var) + self.created[key] = na + new_rhs = new_rhs.xreplace({_add: key}) + break + else: + return new_rhs + + assert False + + def _pass_50_to_stmnts(self, lhs, rhs, *, statements): + for arg in postorder_traversal(rhs): + if arg in self.created: + for t in list(self.created[arg].terms)+[arg]: + if t in self.created: + if arg not in self.expanded: + statements.extend(self.created[t].to_statements(self.created, self.expanded)) + return rhs + + def _pass_60_xrepl(self, lhs, rhs, *, statements): + return rhs.xreplace(self.created) + + def _group(self, x): + all_accum, all_carry = [], [] + for term in x.args: + if term in self.created: + all_accum.append(self.created[term].accum) + all_carry.append(self.created[term].carry) + elif term in self._all_accum: + all_accum.append(term) + elif term in self._all_carry: + all_carry.append(term) + else: + all_accum.append(term) + if all_accum and all_carry: + return OrderedAdd(OrderedAdd(*all_accum), OrderedAdd(*all_carry)) + else: + return OrderedAdd(*(all_accum+all_carry)) + + def _pass_95_group(self, lhs, rhs, *, statements): + return rhs.replace(lambda s: s.is_Add and any( + t in self.created or t in self._all_accum or t in self._all_carry for t in s.args + ), self._group) + + def _pass_90_fin(self, lhs, rhs, *, statements): + return rhs.replace(lambda x: self._is_Neu(x), lambda x: x.finalize()) + + +def _compensated_code(case, **kwargs): + repl, red = cse(case.exprs) + nm = _NeumaierTransformer(repl, red, **kwargs) + statements = nm.statements_with_declarations() + for i, new_expr in enumerate(nm.final_exprs): + statements.append(Assignment(Symbol("out[%d]" % i), new_expr)) + return CodeBlock(*statements) diff --git a/pyodesys/native/core.py b/pyodesys/native/core.py new file mode 100644 index 00000000..50130163 --- /dev/null +++ b/pyodesys/native/core.py @@ -0,0 +1,45 @@ +from collections import defaultdict +from sympy import Symbol +from sympy.codegen.ast import Assignment, float64, value_const, Variable + + +class NullTransformer: + """Perform no transformation.""" + + def __init__(self, repl, red, parent=None, ignore=None): + self.statements = [Assignment(lhs, rhs) for lhs, rhs in repl] + self.final_exprs = red + + def statements_with_declarations(self, pred=lambda s: '[' not in s.name, + type_=float64): + return self._declare(self.statements, pred=pred, type_=type_) + + def _declare(self, stmts, *, pred=lambda x: True, type_=float64): + seen = set() + result = [] + + is_const = defaultdict(int) + for st in stmts: + is_const[st.lhs] += 1 if type(st) is Assignment else 2 + for st in stmts: + if isinstance(st, Assignment) and st.lhs not in seen and pred(st.lhs): + seen.add(st.lhs) + st = Variable(st.lhs, type=type_, attrs=[value_const] if + is_const[st.lhs] == 1 else []).as_Declaration(value=st.rhs) + result.append(st) + return result + + + def apply_remapping(self, remapping): + self.statements = [st.xreplace(remapping) for st in self.statements] + self.final_exprs = [e.xreplace(remapping) for e in self.final_exprs] + + def remapping_for_arrayification(self, template="m_glob[{0}]"): + remapping = {} + i = 0 + for st in self.statements: + if st.lhs in remapping: + continue + remapping[st.lhs] = Symbol(template.format(i), real=True) + i = i + 1 + return remapping diff --git a/pyodesys/native/groupwise.py b/pyodesys/native/groupwise.py new file mode 100644 index 00000000..25ced97f --- /dev/null +++ b/pyodesys/native/groupwise.py @@ -0,0 +1,143 @@ +"""Handle common cses among groups of code.""" + +from functools import reduce +from operator import add +import numpy as np + +from sympy import Symbol, cse, numbered_symbols +from sympy.codegen.ast import Assignment, Variable, float64 +from sympy.codegen.rewriting import create_expand_pow_optimization +from sympy.printing.c import ccode + + +from .core import NullTransformer + + +expand_up_to_3 = create_expand_pow_optimization(3) + + +def pre_process(expr): + """Simplify, expand & factor.""" + return expr.simplify(rational=True).expand().factor() + + +def post_process(expr): + """Expand low integer powers.""" + return expand_up_to_3(expr) + + +class GroupwiseCSE: + """Eliminate common sub-expressions from groups of expressions.""" + + def __init__(self, groups, *, + common_cse_template, + common_ignore=(), + to_code=lambda arg: ccode(arg, math_macros={}), + subsd=None, + Transformer=NullTransformer, + type_=float64, + pre_process=pre_process, + post_process=post_process, + ): + """ + Parameters + ---------- + groups : list like + \\*\\*kwargs : see code for now. + + """ + # self.groups = groups + self._to_code = to_code + self._subsd = subsd or {} + self._type = type_ + self._keys, _values = zip(*groups.items()) + self._spans = np.cumsum([0]+list(map(len, _values))) + self._common_cse_template = common_cse_template + self._grp, self._common = self._get_all_cses( + map(pre_process, reduce(add, _values)), + common_ignore=common_ignore, + post_process=post_process, + Transformer=Transformer + ) + + @property + def keys(self): + """Retrieve the keys of the groups.""" + return self._keys + + def render(self, x): + """Generate a code string.""" + return self._to_code(x.xreplace(self._subsd)) + + def _common_cse(self, all_exprs, **kwargs): + repls, reds = cse(all_exprs, **kwargs) + def comm_symbols(): + idx = 0 + while True: + yield Symbol(self._common_cse_template.format(idx), real=True) + idx += 1 + + cse_symbols = comm_symbols() + comm_subs = {} + for lhs, rhs in repls: + for expr in reds: + if lhs in expr.free_symbols: + comm_subs[lhs] = next(cse_symbols) + break + return ( + [(lhs.xreplace(comm_subs), rhs.xreplace(comm_subs)) for lhs, rhs in repls], + [r.xreplace(comm_subs) for r in reds] + ) + + def _get_all_cses(self, all_exprs, *, common_ignore, post_process, + Transformer): + repls, reds = self._common_cse( + all_exprs, ignore=common_ignore, + symbols=numbered_symbols('cse_temporary')) + comm_tformer = Transformer(repls, reds, ignore=common_ignore) + + assert(len(comm_tformer.final_exprs) == len(reds)) + del reds + + remap = comm_tformer.remapping_for_arrayification(template=self._common_cse_template) + comm_tformer.apply_remapping(remap) + + grp = {} + for i, k in enumerate(self._keys): + g_repls, g_exprs = cse( + comm_tformer.final_exprs[slice(*self._spans[i:i+2])], + symbols=numbered_symbols("cse") + ) + g_tformer = Transformer(g_repls, g_exprs, parent=comm_tformer) + grp[k] = g_tformer + + return (grp, comm_tformer) + + @staticmethod + def _declare(stmts, *, pred, type_=float64): + seen = set() + result = [] + for st in stmts: + if isinstance(st, Assignment) and st.lhs not in seen and pred(st.lhs): + seen.add(st.lhs) + st = Variable(st.lhs, type=type_).as_Declaration(value=st.rhs) + result.append(st) + return result + + def common_statements(self, declare=False): + """Initialize the common sub-expressions among the groups.""" + if declare: + return self._common.statements_with_declarations(pred=declare, type_=self._type) + else: + return self._common.statements + + def assignments(self, key, declare=False): + """Initialize the group specific sub-expressions.""" + if declare: + return self._grp[key].statements_with_declarations(pred=declare, type_=self._type) + else: + return self._grp[key].statements + + def exprs(self, key): + """Retrieve the resulting expressions of the group named ``key``.""" + return self._grp[key].final_exprs diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index bec1506d..45cfdf64 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -17,7 +17,7 @@ This is file is a mako template for a C++ source file defining the ODE system. %endfor namespace { // anonymous namespace for user-defined helper functions - std::vector p_odesys_names ${'' if p_odesys.names is None else '= {"%s"}' % '", "'.join(p_odesys.names)}; + std::vector p_odesys_names ${"" if p_odesys.names is None else '= {"%s"}' % '", "'.join(p_odesys.names)}; %if p_anon is not None: ${p_anon} %endif @@ -32,10 +32,11 @@ namespace odesys_anyode { template <> struct OdeSys: public AnyODE::${p_baseclass} { std::vector m_p; - std::vector m_p_cse; + std::vector m_cse; std::vector m_atol; std::vector m_upper_bounds; std::vector m_lower_bounds; + std::vector m_invar; std::vector m_invar0; realtype m_rtol; realtype m_get_dx_max_factor; @@ -96,32 +97,27 @@ namespace odesys_anyode { bool error_outside_bounds, realtype max_invariant_violation, std::vector special_settings) : - m_p_cse(${p_common['nsubs']}), m_atol(atol), m_rtol(rtol), m_get_dx_max_factor(get_dx_max_factor), - m_error_outside_bounds(error_outside_bounds), m_max_invariant_violation(max_invariant_violation), - m_special_settings(special_settings) { + m_cse(${p_common["nsubs"]}), m_atol(atol), m_rtol(rtol), m_get_dx_max_factor(get_dx_max_factor), + m_error_outside_bounds(error_outside_bounds), + m_max_invariant_violation(max_invariant_violation), + m_special_settings(special_settings), + m_invar(${0 if p_invariants is None else p_invariants["n_invar"]}), + m_invar0(${0 if p_invariants is None else p_invariants["n_invar"]}) + { m_p.assign(params, params + ${len(p_odesys.params) + p_odesys.ny if p_odesys.append_iv else 0}); - <% idx = 0 %> - %for cse_assign in p_common['cses']: - %if cse_assign.lhs.startswith('m_p_cse'): - ${cse_assign} <% assert cse_token == 'm_p_cse[{0}]'.format(idx); idx += 1 %> - %else: - const auto ${cse_assign} - %endif - %endfor - use_get_dx_max = (m_get_dx_max_factor > 0.0) ? ${'true' if p_get_dx_max else 'false'} : false; + ${src["common"]} + use_get_dx_max = (m_get_dx_max_factor > 0.0) ? ${"true" if p_get_dx_max else "false"} : false; %if p_invariants is not None and p_support_recoverable_error: if (m_max_invariant_violation != 0.0){ - ${'' if p_odesys.append_iv else 'throw std::runtime_error("append_iv not set to True")'} + ${"" if p_odesys.append_iv else 'throw std::runtime_error("append_iv not set to True")'} const realtype * const y = params + ${len(p_odesys.params)}; - %for cse_assign in p_invariants['cses']: - const auto ${cse_assign} - %endfor - %for expr in p_invariants['exprs']: - m_invar0.push_back(${expr}); - %endfor - } + m_invar0.resize(${p_invariants["n_invar"]}); + m_invar0.resize(${p_invariants["n_invar"]}); + realtype * const out = m_invar0.data(); + ${p_invariants["assign"]} + } %endif - ${'\n '.join(p_constructor)} + ${"\n ".join(p_constructor)} } indextype OdeSys::get_ny() const { @@ -146,18 +142,12 @@ namespace odesys_anyode { AnyODE::Status OdeSys::rhs(realtype x, const realtype * const ANYODE_RESTRICT y, - realtype * const ANYODE_RESTRICT f) { + realtype * const ANYODE_RESTRICT out) { %if isinstance(p_rhs, str): ${p_rhs} %else: - ${'AnyODE::ignore(x);' if p_odesys.autonomous_exprs else ''} - %for cse_assign in p_rhs['cses']: - const auto ${cse_assign} - %endfor - <% import os %> - %for i, expr in enumerate(p_rhs['exprs']): - f[${i}] = ${expr}; - %endfor + ${"AnyODE::ignore(x);" if p_odesys.autonomous_exprs else ""} + ${p_rhs["assign"] this->nfev++; %if p_support_recoverable_error: if (m_error_outside_bounds){ @@ -184,10 +174,11 @@ namespace odesys_anyode { } %if p_invariants is not None: if (m_max_invariant_violation != 0.0){ - %for cse_assign in p_invariants['cses']: - const auto ${cse_assign} + ${p_invariants["assign"]} + %for cse_assign in p_invariants["cses"]: + ${cse_assign}; %endfor - %for idx, invar_expr in enumerate(p_invariants['exprs']): + %for idx, invar_expr in enumerate(p_invariants["exprs"]): if (std::abs(${invar_expr} - m_invar0[${idx}]) > ((m_max_invariant_violation > 0) ? m_max_invariant_violation : std::abs(m_max_invariant_violation*m_invar0[${idx}]) /*- m_max_invariant_violation*/)) { @@ -198,7 +189,7 @@ namespace odesys_anyode { } %endif %endif - %if getattr(p_odesys, '_nonnegative', False) and p_support_recoverable_error: + %if getattr(p_odesys, "_nonnegative", False) and p_support_recoverable_error: for (int i=0; i<${p_odesys.ny}; ++i) if (y[i] < 0) return AnyODE::Status::recoverable_error; %endif return AnyODE::Status::success; @@ -224,14 +215,14 @@ namespace odesys_anyode { ${p_jtimes} %else: AnyODE::ignore(fy); // Currently we are not using fy (could be done through extensive pattern matching) - ${'AnyODE::ignore(x);' if p_odesys.autonomous_exprs else ''} + ${"AnyODE::ignore(x);" if p_odesys.autonomous_exprs else ""} - %for cse_assign in p_jtimes['cses']: - const auto ${cse_assign}; + %for cse_assign in p_jtimes["cses"]: + ${cse_assign}; %endfor %for i in range(p_odesys.ny): - <% curr_expr = p_jtimes['exprs'][i] %> + <% curr_expr = p_jtimes["exprs"][i] %> Jv[${i}] = ${curr_expr}; %endfor %endif @@ -245,7 +236,7 @@ namespace odesys_anyode { } - %for order in ('cmaj', 'rmaj'): + %for order in ("cmaj", "rmaj"): AnyODE::Status OdeSys::dense_jac_${order}(realtype x, const realtype * const ANYODE_RESTRICT y, const realtype * const ANYODE_RESTRICT fy, @@ -258,26 +249,26 @@ namespace odesys_anyode { %else: // The AnyODE::ignore(...) calls below are used to generate code free from false compiler warnings. AnyODE::ignore(fy); // Currently we are not using fy (could be done through extensive pattern matching) - ${'AnyODE::ignore(x);' if p_odesys.autonomous_exprs else ''} - ${'AnyODE::ignore(y);' if (not any([yi in p_odesys.get_jac().free_symbols for yi in p_odesys.dep]) and - not any([yi in p_odesys.get_dfdx().free_symbols for yi in p_odesys.dep])) else ''} + ${"AnyODE::ignore(x);" if p_odesys.autonomous_exprs else ""} + ${"AnyODE::ignore(y);" if (not any([yi in p_odesys.get_jac().free_symbols for yi in p_odesys.dep]) and + not any([yi in p_odesys.get_dfdx().free_symbols for yi in p_odesys.dep])) else ""} - %for cse_assign in p_jac_dense['cses']: - const auto ${cse_assign} + %for cse_assign in p_jac_dense["cses"]: + ${cse_assign}; %endfor %for i_major in range(p_odesys.ny): %for i_minor in range(p_odesys.ny): <% - curr_expr = p_jac_dense['exprs'][i_minor, i_major] if order == 'cmaj' else p_jac_dense['exprs'][i_major, i_minor] - if curr_expr == '0' and p_jacobian_set_to_zero_by_solver: + curr_expr = p_jac_dense["exprs"][i_minor, i_major] if order == "cmaj" else p_jac_dense["exprs"][i_major, i_minor] + if curr_expr == "0" and p_jacobian_set_to_zero_by_solver: continue %> jac[ldim*${i_major} + ${i_minor}] = ${curr_expr}; %endfor %endfor if (dfdt){ - %for idx, expr in enumerate(p_jac_dense['dfdt_exprs']): + %for idx, expr in enumerate(p_jac_dense["dfdt_exprs"]): dfdt[${idx}] = ${expr}; %endfor } @@ -301,40 +292,34 @@ namespace odesys_anyode { %elif isinstance(p_first_step, str): ${p_first_step} %else: - %for cse_assign in p_first_step['cses']: - const realtype ${cse_assign} + %for cse_assign in p_first_step["cses"]: + const realtype ${cse_assign}; %endfor - ${'' if p_odesys.indep in p_odesys.first_step_expr.free_symbols else 'AnyODE::ignore(x);'} - ${'' if any([yi in p_odesys.first_step_expr.free_symbols for yi in p_odesys.dep]) else 'AnyODE::ignore(y);'} - return ${p_first_step['expr']}; + ${"" if p_odesys.indep in p_odesys.first_step_expr.free_symbols else "AnyODE::ignore(x);"} + ${"" if any([yi in p_odesys.first_step_expr.free_symbols for yi in p_odesys.dep]) else "AnyODE::ignore(y);"} + return ${p_first_step["expr"]}; %endif } AnyODE::Status OdeSys::sparse_jac_csc(realtype x, const realtype * const ANYODE_RESTRICT y, const realtype * const ANYODE_RESTRICT fy, - realtype * const ANYODE_RESTRICT data, + realtype * const ANYODE_RESTRICT out, indextype * const ANYODE_RESTRICT colptrs, indextype * const ANYODE_RESTRICT rowvals) { %if p_jac_sparse is not None: AnyODE::ignore(fy); // Currently we are not using fy (could be done through extensive pattern matching) - ${'AnyODE::ignore(x);' if p_odesys.autonomous_exprs else ''} - ${'AnyODE::ignore(y);' if (not any([yi in p_odesys.get_jac().free_symbols for yi in p_odesys.dep]) and - not any([yi in p_odesys.get_dfdx().free_symbols for yi in p_odesys.dep])) else ''} - %for cse_assign in p_jac_sparse['cses']: - const auto ${cse_assign} - %endfor - - %for i in range(p_odesys.nnz): - data[${i}] = ${p_jac_sparse['exprs'][i]}; - %endfor + ${"AnyODE::ignore(x);" if p_odesys.autonomous_exprs else ""} + ${"AnyODE::ignore(y);" if (not any([yi in p_odesys.get_jac().free_symbols for yi in p_odesys.dep]) and + not any([yi in p_odesys.get_dfdx().free_symbols for yi in p_odesys.dep])) else ""} + ${p_jac_sparse["assign"]} %for i in range(p_odesys.nnz): - rowvals[${i}] = ${p_jac_sparse['rowvals'][i]}; + rowvals[${i}] = ${p_jac_sparse["rowvals"][i]}; %endfor %for i in range(p_odesys.ny + 1): - colptrs[${i}] = ${p_jac_sparse['colptrs'][i]}; + colptrs[${i}] = ${p_jac_sparse["colptrs"][i]}; %endfor this->njev++; return AnyODE::Status::success; @@ -383,13 +368,13 @@ namespace odesys_anyode { %elif isinstance(p_roots, str): ${p_roots} %else: - ${'' if any(p_odesys.indep in expr.free_symbols for expr in p_odesys.roots) else 'AnyODE::ignore(x);'} + ${"" if any(p_odesys.indep in expr.free_symbols for expr in p_odesys.roots) else "AnyODE::ignore(x);"} - %for cse_assign in p_roots['cses']: - const auto ${cse_assign} + %for cse_assign in p_roots["cses"]: + ${cse_assign}; %endfor - %for i, expr in enumerate(p_roots['exprs']): + %for i, expr in enumerate(p_roots["exprs"]): out[${i}] = ${expr}; %endfor this->nrev++; diff --git a/pyodesys/native/sympy_interface.py b/pyodesys/native/sympy_interface.py index acfb14b4..a548d32b 100644 --- a/pyodesys/native/sympy_interface.py +++ b/pyodesys/native/sympy_interface.py @@ -99,9 +99,6 @@ class _NeumaierTransformer: """ def __init__(self, repl, red, *, tmp_pfx="t", neu_pfx="n", up_to=100, limit=3): - pprint.pprint(repl) - pprint.pprint(red) - print("") self.repl = repl self.red = red self.limit = limit @@ -145,7 +142,8 @@ def _mk_Neu(self, terms, lhs): def _is_Neu(x): return isinstance(x, _NeumaierAdd) - def _declare(self, stmts, *, type_=float64): + @staticmethod + def _declare(stmts, *, type_=float64): seen = set() result = [] for st in stmts: From 604b42e2ee410b46550f73ee3e605c1354424501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 23 Aug 2021 08:16:54 +0200 Subject: [PATCH 036/102] small steps --- pyodesys/native/_base.py | 24 ++++++--- .../native/sources/odesys_anyode_template.cpp | 53 ++++++++----------- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index f1981b16..21755283 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -210,18 +210,28 @@ def variables(self): subsd=subsd, Transformer=Transformer ) - def _block(k, assign_to=lambda i: _r("out[%d]" % i)): - return CodeBlock(*(gw.assignment(k, declare=lambda s: '[' not in s.name) + [ - Assignment(assign_to(i), e) for i, e in enumerate(gw.exprs(k)) - ])) - src = {k: gw.render(_block(k)) for k in gw.keys} + def _cses(k, assign_to=lambda i: _r("out[%d]" % i)): + return CodeBlock(*gw.assignment(k, declare=lambda s: '[' not in s.name)) + cses = {k: gw.render(_cses(k)) for k in gw.keys} + class Assigner: + def __init__(self, k): + self.k = k + def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): + return self.gw.render(Assignment(assign_to(i), gw.exprs(self.k)[i])) + ";" + assignments = {k: Assigner(k) for k in gw.keys} else: logger.info("Not using common subexpression elimination (disabled by PYODESYS_NATIVE_CSE)") - def _block(exprs, assign_to=lambda i: _r("out[%d]" % i)): + def _block(exprs): return CodeBlock(*[ - Assignment(assign_to(i), e) for i, e in enumerate(exprs) + ]).xreplace(subsd) + class Assigner: + def __init__(self, k): + self.k = k + def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): + return self.odesys.be.ccode(Assignment(assign_to(i), all_exprs[self.k][i]) for i, e in enumerate(exprs)) + ';' + de src = {k: self.odesys.be.ccode(_block(exprs)) for k, exprs in all_exprs.items()} ns = dict( diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 45cfdf64..3d4e5184 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -96,15 +96,15 @@ namespace odesys_anyode { realtype get_dx_max_factor, bool error_outside_bounds, realtype max_invariant_violation, - std::vector special_settings) : - m_cse(${p_common["nsubs"]}), m_atol(atol), m_rtol(rtol), m_get_dx_max_factor(get_dx_max_factor), - m_error_outside_bounds(error_outside_bounds), - m_max_invariant_violation(max_invariant_violation), - m_special_settings(special_settings), - m_invar(${0 if p_invariants is None else p_invariants["n_invar"]}), - m_invar0(${0 if p_invariants is None else p_invariants["n_invar"]}) + std::vector special_settings) + : m_cse(${p_common["nsubs"]}), m_atol(atol), m_rtol(rtol), m_get_dx_max_factor(get_dx_max_factor) + , m_p(params, params + ${len(p_odesys.params) + p_odesys.ny if p_odesys.append_iv else 0}) + , m_error_outside_bounds(error_outside_bounds) + , m_max_invariant_violation(max_invariant_violation) + , m_special_settings(special_settings) + , m_invar(${0 if p_invariants is None else p_invariants["n_invar"]}) + , m_invar0(${0 if p_invariants is None else p_invariants["n_invar"]}) { - m_p.assign(params, params + ${len(p_odesys.params) + p_odesys.ny if p_odesys.append_iv else 0}); ${src["common"]} use_get_dx_max = (m_get_dx_max_factor > 0.0) ? ${"true" if p_get_dx_max else "false"} : false; %if p_invariants is not None and p_support_recoverable_error: @@ -175,17 +175,14 @@ namespace odesys_anyode { %if p_invariants is not None: if (m_max_invariant_violation != 0.0){ ${p_invariants["assign"]} - %for cse_assign in p_invariants["cses"]: - ${cse_assign}; - %endfor - %for idx, invar_expr in enumerate(p_invariants["exprs"]): - if (std::abs(${invar_expr} - m_invar0[${idx}]) > ((m_max_invariant_violation > 0) + for (int idx=0; idx<${p_invariants["n_invar"]}; ++idx) { + if (std::abs(m_invar[idx] - m_invar0[idx]) > ((m_max_invariant_violation > 0) ? m_max_invariant_violation - : std::abs(m_max_invariant_violation*m_invar0[${idx}]) /*- m_max_invariant_violation*/)) { - std::cerr << "Invariant (${idx}) violation at x=" << x << "\n"; - return AnyODE::Status::recoverable_error; + : std::abs(m_max_invariant_violation*m_invar0[idx]) /*- m_max_invariant_violation*/)) { + std::cerr << "Invariant (" << idx << ") violation at x=" << x << "\n"; + return AnyODE::Status::recoverable_error; + } } - %endfor } %endif %endif @@ -205,26 +202,19 @@ namespace odesys_anyode { } AnyODE::Status OdeSys::jtimes( - const realtype * const ANYODE_RESTRICT v, - realtype * const ANYODE_RESTRICT Jv, - realtype x, - const realtype * const ANYODE_RESTRICT y, - const realtype * const ANYODE_RESTRICT fy) { + const realtype * const ANYODE_RESTRICT v, + realtype * const ANYODE_RESTRICT out, + realtype x, + const realtype * const ANYODE_RESTRICT y, + const realtype * const ANYODE_RESTRICT fy) + { %if p_jtimes is not None: %if isinstance(p_jtimes, str): ${p_jtimes} %else: AnyODE::ignore(fy); // Currently we are not using fy (could be done through extensive pattern matching) ${"AnyODE::ignore(x);" if p_odesys.autonomous_exprs else ""} - - %for cse_assign in p_jtimes["cses"]: - ${cse_assign}; - %endfor - - %for i in range(p_odesys.ny): - <% curr_expr = p_jtimes["exprs"][i] %> - Jv[${i}] = ${curr_expr}; - %endfor + ${p_jtimes["assign"]} %endif this->njvev++; return AnyODE::Status::success; @@ -253,6 +243,7 @@ namespace odesys_anyode { ${"AnyODE::ignore(y);" if (not any([yi in p_odesys.get_jac().free_symbols for yi in p_odesys.dep]) and not any([yi in p_odesys.get_dfdx().free_symbols for yi in p_odesys.dep])) else ""} + ${p_jac_dense["assign"]} %for cse_assign in p_jac_dense["cses"]: ${cse_assign}; %endfor From e799f830b3f5d1f7cd9e159aaafc76e3f9197b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Thu, 26 Aug 2021 15:16:23 +0200 Subject: [PATCH 037/102] continued refactoring --- pyodesys/native/_base.py | 105 ++++++++++++----- pyodesys/native/compensated.py | 109 +++++++++++------- pyodesys/native/core.py | 57 ++++++--- pyodesys/native/groupwise.py | 45 ++++---- .../native/sources/odesys_anyode_template.cpp | 67 ++++++----- 5 files changed, 240 insertions(+), 143 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 21755283..f125681f 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -11,7 +11,8 @@ import sys import tempfile -from sympy.printing.cxx import CXX17CodePrinter +import sympy +from sympy.codegen.ast import CodeBlock, Assignment import numpy as np import pkg_resources @@ -61,6 +62,45 @@ def get_compile_kwargs(): _obj_suffix = '.o' # os.path.splitext(_ext_suffix)[0] + '.o' # '.obj' +class _AssignerBase: + def all(self, **kwargs): + return "\n".join(self(i, **kwargs) for i in range(self.n)) + + +def _r(s): + if isinstance(s, sympy.Symbol): + return s + else: + return sympy.Symbol(s, real=True) + + +class _AssignerGW(_AssignerBase): + def __init__(self, k, gw): + self.k = k + self.gw = gw + self.n = len(gw.exprs(k)) + + def expr_is_zero(self, i): + return self.gw.exprs(self.k)[i] == 0 + + def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): + return self.gw.render(Assignment(_r(assign_to(i)), self.gw.exprs(self.k)[i])) + ";" + + +class _AssignerPlain(_AssignerBase): + def __init__(self, k, all_exprs): + self.k = k + self.all_exrs = all_exprs + self.n = len(all_exprs[k]) + + def expr_is_zero(self, i): + return self.all_exprs[i] == 0 + + def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): + return self.odesys.be.ccode( + Assignment(_r(assign_to(i)), self.all_exprs[self.k][i]) + ) + ';' + class _NativeCodeBase(Cpp_Code): """ Base class for generated code. @@ -157,11 +197,11 @@ def variables(self): ) if jac is not False and nnz < 0: jac_dfdx = list(reduce(add, jac.tolist() + self.odesys.get_dfdx().tolist())) - all_exprs["jac_dfdx"] = jac_dfdx + all_exprs["jac_dfdt"] = jac_dfdx nj = len(jac_dfdx) elif jac is not False and nnz >= 0: jac_dfdx = list(reduce(add, jac.tolist())) - all_exprs["jac_dfdx"] = jac_dfdx + all_exprs["jac_dfdt"] = jac_dfdx nj = len(jac_dfdx) else: nj = 0 @@ -206,33 +246,27 @@ def variables(self): all_exprs, common_cse_template="m_cse[{}]", common_ignore=(self.odesys.indep,) + self.odesys.dep + v, - to_code=lambda x: ccode(x, math_macros={}), + to_code=lambda x: self.odesys.be.ccode(x, math_macros={}), subsd=subsd, Transformer=Transformer ) + def not_arr(s): + return '[' not in s.name + def _cses(k, assign_to=lambda i: _r("out[%d]" % i)): - return CodeBlock(*gw.assignment(k, declare=lambda s: '[' not in s.name)) + return CodeBlock(*gw.statements(k, declare=not_arr)) cses = {k: gw.render(_cses(k)) for k in gw.keys} - class Assigner: - def __init__(self, k): - self.k = k - def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): - return self.gw.render(Assignment(assign_to(i), gw.exprs(self.k)[i])) + ";" - assignments = {k: Assigner(k) for k in gw.keys} + n_common_cses = gw.n_remapped + common_cses = gw.render(CodeBlock(*gw.common_statements(declare=not_arr))) + + assigners = {k: _AssignerGW(k, gw) for k in gw.keys} else: logger.info("Not using common subexpression elimination (disabled by PYODESYS_NATIVE_CSE)") - def _block(exprs): - return CodeBlock(*[ - - ]).xreplace(subsd) - class Assigner: - def __init__(self, k): - self.k = k - def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): - return self.odesys.be.ccode(Assignment(assign_to(i), all_exprs[self.k][i]) for i, e in enumerate(exprs)) + ';' - de - src = {k: self.odesys.be.ccode(_block(exprs)) for k, exprs in all_exprs.items()} + n_common_cses=0 + common_cses="" + + assigners = {k: _AssignerPlain(k, all_exprs) for k in all_exprsself} ns = dict( _message_for_rendered=[ @@ -241,32 +275,39 @@ def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): __version__, dt.now().isoformat()) ], p_common={ - 'assign': src["common"] - } + 'cses': common_cses, + 'n_cses': n_common_cses + }, p_odesys=self.odesys, - p_src=src, p_rhs={ - 'assign': src["rhs"] + 'cses': cses["rhs"], + 'assign': assigners["rhs"] }, p_jtimes=None if jtimes is False else{ - 'assign': src["jtimes"] + 'cses': cses["jtimes"], + 'assign': assigners["jtimes"] }, p_jac_dense=None if jac is False or nnz >= 0 else { - 'assign': src["jac_dfdt"] + 'cses': cses["jac_dfdt"], + 'assign': assigners["jac_dfdt"] }, p_jac_sparse=None if jac is False or nnz < 0 else { - 'assign': src["jac_dfdt"] + 'cses': cses["jac_dfdt"], + 'assign': assigners["jac_dfdt"], 'colptrs': self.odesys._colptrs, 'rowvals': self.odesys._rowvals }, p_first_step=None if first_step is None else { - 'assign': src["first_step"] + 'cses': cses["first_step"], + 'assign': assigners["first_step"] }, p_roots=None if self.odesys.roots is None else { - 'assign': src["roots"] + 'cses': cses["roots"], + 'assign': assigners["roots"] }, p_invariants=None if all_invar == () else { - 'assign': src["invariants"] + 'cses': cses["invariants"], + 'assign': assigners["invariants"], 'n_invar': len(all_invar) }, p_nroots=self.odesys.nroots, diff --git a/pyodesys/native/compensated.py b/pyodesys/native/compensated.py index d3533214..b04b6d27 100644 --- a/pyodesys/native/compensated.py +++ b/pyodesys/native/compensated.py @@ -6,20 +6,26 @@ $ python3 compensated_cse.py demo1 | clang-format --style=Google | batcat -pl C """ -import pprint from collections import defaultdict from functools import reduce from operator import add from sympy import ( - Abs, Add, Basic, cse, exp, Expr, numbered_symbols, Piecewise, pi, - postorder_traversal, preorder_traversal, pycode, Symbol, Tuple + Abs, Add, And, Eq, Expr, Lt, Ne, numbered_symbols, Piecewise, + postorder_traversal, Symbol, Tuple ) from sympy.codegen import Assignment, aug_assign, CodeBlock -from sympy.codegen.ast import Token, Variable, float64, value_const +from sympy.codegen.ast import AssignmentBase, Token, While, break_ from .core import NullTransformer from .util import OrderedAdd + +def If(cond, body): + return While(cond, CodeBlock( + *body, + break_ + )) + class _NeumaierAdd(Token, Expr): """Represents KBN compensated summation.""" @@ -30,7 +36,7 @@ def _ccode(self, printer): terms = ", ".join(map(printer._print, self.terms)) return f"NA({terms} /*{str(self.accum)[:-1]}*/)" - def to_statements(self, existing, expanded): + def to_statements(self, existing, expanded, do_swap=False): """Transform into statements.""" neum, ordinary = [], [] for term in self.terms: @@ -42,14 +48,12 @@ def to_statements(self, existing, expanded): if neum: st.append(Assignment(self.accum, sum(na.accum for na in neum))) st.append(Assignment(self.carry, sum(na.carry for na in neum))) - for na in neum: - expanded.add(na) else: st.append(Assignment(self.accum, ordinary.pop(0))) st.append(Assignment(self.carry, 0)) for elem in ordinary: - st.extend(_NeumaierAdd._impl_add(self.accum, self.carry, elem, self.temp)) + st.extend(_NeumaierAdd._impl_add(self.accum, self.carry, elem, self.temp, do_swap)) expanded.add(self) return st @@ -58,16 +62,27 @@ def finalize(self): return self._impl_finalize(self.accum, self.carry) @staticmethod - def _impl_add(accum, carry, elem, temp): + def _impl_add(accum, carry, elem, temp, do_swap=False): """Perform Kahan-Babuska-Neumaier addition.""" big_temp = OrderedAdd(OrderedAdd(accum, -temp), elem) big_elem = OrderedAdd(OrderedAdd(elem, -temp), accum) - pw = Piecewise((big_temp, Abs(temp) > Abs(elem)), (big_elem, True)) - return [ + abs_elem = Abs(elem) + pw = Piecewise((big_temp, Abs(temp) > abs_elem), (big_elem, True)) + statements = [ Assignment(temp, accum + elem), aug_assign(carry, '+', pw), Assignment(accum, temp) ] + if do_swap: + return [ + If(And(Eq(carry, 0), Ne(accum, 0), Lt(Abs(accum), abs_elem)), [ + Assignment(temp, accum), + Assignment(accum, carry), + Assignment(carry, temp) + ]) + ] + statements + else: + return statements @staticmethod def _impl_finalize(accum, carry): @@ -79,12 +94,13 @@ class _NeumaierTransformer(NullTransformer): Parameters ---------- - up_to: int, [0-100] + up_to_debug: int, [0-100] Code is guaranteed to compile at levels 0 (no passes, no compensation) and 100 (all passes). """ - def __init__(self, repl, red, *, tmp_pfx="t", neu_pfx="n", up_to=100, limit=3, parent=None, ignore=None): + def __init__(self, repl, red, *, tmp_pfx="t", neu_pfx="n", up_to_debug=100, + limit=3, parent=None, ignore=None, do_swap=False): self.repl = repl self.red = red self.limit = limit @@ -106,23 +122,20 @@ def __init__(self, repl, red, *, tmp_pfx="t", neu_pfx="n", up_to=100, limit=3, p num, *_ = rest.split("_") if len(_) == 0: continue - if int(num) <= up_to: + if int(num) <= up_to_debug: self.passes.append(getattr(self, p)) + self.do_swap = do_swap self.statements, self.final_exprs = self._pipeline() def remapping_for_arrayification(self, template="m_glob[{0}]"): remapping = {} i = 0 for st in self.statements: - if st.lhs in remapping: + if st.lhs in remapping or st.lhs in self._all_tempv: continue - if st.lhs in self._all_accum or st.lhs in self._all_carry: - remapping[st.lhs] = Symbol(template.format(i), real=True) - elif st.lhs in self._all_tempv: - pass - else: - remapping[st.lhs] = Symbol(template.format(i), real=True) + #if st.lhs in self._all_accum or st.lhs in self._all_carry: + remapping[st.lhs] = Symbol(template.format(i), real=True) i = i + 1 return remapping @@ -141,18 +154,31 @@ def _mk_Neu(self, terms, lhs): def _is_Neu(x): return isinstance(x, _NeumaierAdd) + def _single_pass(self, statements, pass_): + new_stmts = [] + for st in statements: + if isinstance(st, AssignmentBase): + new_rhs = pass_(st.lhs, st.rhs, statements=new_stmts) + if st.lhs not in self.created: + new_stmts.append(st.__class__(st.lhs, new_rhs)) + elif hasattr(st, "body"): + assert isinstance(st.body, CodeBlock) + new_body = CodeBlock(*self._single_pass(st.body.args, pass_)) + new_args = (new_body if attr == 'body' else getattr(st, attr) + for attr in st.__slots__) + new_stmts.append(st.__class__(*new_args)) + else: + new_stmts.append(st) # no-op (e.g. BreakToken instance) + return new_stmts + def _pipeline(self): statements = [Assignment(*lr) for lr in self.repl] final_exprs = self.red for pass_ in self.passes: - new_stmts, new_exprs = [], [] - for st in statements: - new_rhs = pass_(st.lhs, st.rhs, statements=new_stmts) - if st.lhs not in self.created: # not Neumaier, (aug)assign: - new_stmts.append(st.__class__(st.lhs, new_rhs)) + statements = self._single_pass(statements, pass_) + new_exprs = [] for expr in final_exprs: - new_exprs.append(pass_(None, expr, statements=new_stmts)) - statements = new_stmts + new_exprs.append(pass_(None, expr, statements=statements)) final_exprs = new_exprs return statements, final_exprs @@ -190,12 +216,18 @@ def _pass_10_create_nodes(self, lhs, rhs, *, statements, debug=False): assert False def _pass_50_to_stmnts(self, lhs, rhs, *, statements): - for arg in postorder_traversal(rhs): - if arg in self.created: - for t in list(self.created[arg].terms)+[arg]: - if t in self.created: - if arg not in self.expanded: - statements.extend(self.created[t].to_statements(self.created, self.expanded)) + for neu in map(self.created.get, postorder_traversal(rhs)): + if neu is None: + continue + for t_neu in map(self.created.get, neu.terms): + if t_neu is None: + continue + if t_neu not in self.expanded: + statements.extend(t_neu.to_statements( + self.created, self.expanded, self.do_swap)) + if neu not in self.expanded: + statements.extend(neu.to_statements( + self.created, self.expanded, self.do_swap)) return rhs def _pass_60_xrepl(self, lhs, rhs, *, statements): @@ -225,12 +257,3 @@ def _pass_95_group(self, lhs, rhs, *, statements): def _pass_90_fin(self, lhs, rhs, *, statements): return rhs.replace(lambda x: self._is_Neu(x), lambda x: x.finalize()) - - -def _compensated_code(case, **kwargs): - repl, red = cse(case.exprs) - nm = _NeumaierTransformer(repl, red, **kwargs) - statements = nm.statements_with_declarations() - for i, new_expr in enumerate(nm.final_exprs): - statements.append(Assignment(Symbol("out[%d]" % i), new_expr)) - return CodeBlock(*statements) diff --git a/pyodesys/native/core.py b/pyodesys/native/core.py index 50130163..53cf32d0 100644 --- a/pyodesys/native/core.py +++ b/pyodesys/native/core.py @@ -1,40 +1,71 @@ from collections import defaultdict from sympy import Symbol -from sympy.codegen.ast import Assignment, float64, value_const, Variable +from sympy.codegen.ast import Assignment, AssignmentBase, float64, value_const, Variable class NullTransformer: """Perform no transformation.""" def __init__(self, repl, red, parent=None, ignore=None): + """From CSE result.""" self.statements = [Assignment(lhs, rhs) for lhs, rhs in repl] self.final_exprs = red def statements_with_declarations(self, pred=lambda s: '[' not in s.name, type_=float64): + """Conditionally add declarations.""" return self._declare(self.statements, pred=pred, type_=type_) + @staticmethod + def _is_const(ctx_is_const, statements): + for st in statements: # filter(lambda s: isinstance(s, AssignmentBase), stmts): + if isinstance(st, AssignmentBase): + ctx_is_const[st.lhs] += 1 if type(st) is Assignment else 2 + elif hasattr(st, "body"): + NullTransformer._is_const(ctx_is_const, st.body.args) + else: + pass # no-op + + @staticmethod + def _as_decl(ctx_seen, statements, is_const, *, pred, type_, lift=False): + new_stmnts, lifted = [], [] + for st in statements: + if isinstance(st, Assignment) and st.lhs not in ctx_seen and pred(st.lhs): + ctx_seen.add(st.lhs) + var = Variable(st.lhs, type=type_, attrs=[value_const] if + is_const[st.lhs] == 1 else []) + if lift: + lifted.append(var.as_Declaration()) + else: + st = var.as_Declaration(value=st.rhs) + elif hasattr(st, "body"): + body_stmnts, body_lifted = NullTransformer._as_decl( + ctx_seen, st.body.args, is_const, + pred=pred, type_=type_, lift=True) + if lift: + lifted.extend(body_lifted) + else: + new_stmnts.extend(body_lifted) + else: + pass # no-op, e.g. BreakToken() + new_stmnts.append(st) + return new_stmnts, lifted + def _declare(self, stmts, *, pred=lambda x: True, type_=float64): seen = set() - result = [] - is_const = defaultdict(int) - for st in stmts: - is_const[st.lhs] += 1 if type(st) is Assignment else 2 - for st in stmts: - if isinstance(st, Assignment) and st.lhs not in seen and pred(st.lhs): - seen.add(st.lhs) - st = Variable(st.lhs, type=type_, attrs=[value_const] if - is_const[st.lhs] == 1 else []).as_Declaration(value=st.rhs) - result.append(st) - return result - + self._is_const(is_const, stmts) + new_stmnts, lifted = self._as_decl(seen, stmts, is_const, pred=pred, lift=False, type_=type_) + assert(len(lifted) == 0) + return new_stmnts def apply_remapping(self, remapping): + """Replace in statements & final_exprs.""" self.statements = [st.xreplace(remapping) for st in self.statements] self.final_exprs = [e.xreplace(remapping) for e in self.final_exprs] def remapping_for_arrayification(self, template="m_glob[{0}]"): + """Create a replacement dictionary.""" remapping = {} i = 0 for st in self.statements: diff --git a/pyodesys/native/groupwise.py b/pyodesys/native/groupwise.py index 25ced97f..3dbbf5e4 100644 --- a/pyodesys/native/groupwise.py +++ b/pyodesys/native/groupwise.py @@ -30,7 +30,7 @@ class GroupwiseCSE: """Eliminate common sub-expressions from groups of expressions.""" def __init__(self, groups, *, - common_cse_template, + common_cse_template="common_cse{0}", common_ignore=(), to_code=lambda arg: ccode(arg, math_macros={}), subsd=None, @@ -38,6 +38,7 @@ def __init__(self, groups, *, type_=float64, pre_process=pre_process, post_process=post_process, + kw_cse=None ): """ Parameters @@ -53,11 +54,12 @@ def __init__(self, groups, *, self._keys, _values = zip(*groups.items()) self._spans = np.cumsum([0]+list(map(len, _values))) self._common_cse_template = common_cse_template - self._grp, self._common = self._get_all_cses( - map(pre_process, reduce(add, _values)), + self._per_g_tformrs, self._comm_tformr = self._get_all_cses( + map(pre_process, reduce(add, map(list, _values))), common_ignore=common_ignore, post_process=post_process, - Transformer=Transformer + Transformer=Transformer, + kw_cse=kw_cse or {}, ) @property @@ -71,13 +73,13 @@ def render(self, x): def _common_cse(self, all_exprs, **kwargs): repls, reds = cse(all_exprs, **kwargs) - def comm_symbols(): - idx = 0 - while True: - yield Symbol(self._common_cse_template.format(idx), real=True) - idx += 1 - - cse_symbols = comm_symbols() + # def comm_symbols(): + # idx = 0 + # while True: + # yield Symbol(self._common_cse_template.format(idx), real=True) + # idx += 1 + #cse_symbols = comm_symbols() + cse_symbols = numbered_symbols("cse_t") # local temporaries comm_subs = {} for lhs, rhs in repls: for expr in reds: @@ -90,10 +92,10 @@ def comm_symbols(): ) def _get_all_cses(self, all_exprs, *, common_ignore, post_process, - Transformer): + Transformer, kw_cse): repls, reds = self._common_cse( all_exprs, ignore=common_ignore, - symbols=numbered_symbols('cse_temporary')) + symbols=numbered_symbols('cse_temporary'), **kw_cse) comm_tformer = Transformer(repls, reds, ignore=common_ignore) assert(len(comm_tformer.final_exprs) == len(reds)) @@ -101,12 +103,13 @@ def _get_all_cses(self, all_exprs, *, common_ignore, post_process, remap = comm_tformer.remapping_for_arrayification(template=self._common_cse_template) comm_tformer.apply_remapping(remap) + self.n_remapped = len(remap) grp = {} for i, k in enumerate(self._keys): g_repls, g_exprs = cse( comm_tformer.final_exprs[slice(*self._spans[i:i+2])], - symbols=numbered_symbols("cse") + symbols=numbered_symbols("cse"), **kw_cse ) g_tformer = Transformer(g_repls, g_exprs, parent=comm_tformer) grp[k] = g_tformer @@ -127,17 +130,17 @@ def _declare(stmts, *, pred, type_=float64): def common_statements(self, declare=False): """Initialize the common sub-expressions among the groups.""" if declare: - return self._common.statements_with_declarations(pred=declare, type_=self._type) + return self._comm_tformr.statements_with_declarations(pred=declare, type_=self._type) else: - return self._common.statements + return self._comm_tformr.statements - def assignments(self, key, declare=False): + def statements(self, gk, declare=False): """Initialize the group specific sub-expressions.""" if declare: - return self._grp[key].statements_with_declarations(pred=declare, type_=self._type) + return self._per_g_tformrs[gk].statements_with_declarations(pred=declare, type_=self._type) else: - return self._grp[key].statements + return self._per_g_tformrs[gk].statements - def exprs(self, key): + def exprs(self, gk): """Retrieve the resulting expressions of the group named ``key``.""" - return self._grp[key].final_exprs + return self._per_g_tformrs[gk].final_exprs diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 3d4e5184..6253bb56 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -97,15 +97,18 @@ namespace odesys_anyode { bool error_outside_bounds, realtype max_invariant_violation, std::vector special_settings) - : m_cse(${p_common["nsubs"]}), m_atol(atol), m_rtol(rtol), m_get_dx_max_factor(get_dx_max_factor) - , m_p(params, params + ${len(p_odesys.params) + p_odesys.ny if p_odesys.append_iv else 0}) + : m_p(params, params + ${len(p_odesys.params) + p_odesys.ny if p_odesys.append_iv else 0}) + , m_cse(${p_common["n_cses"]}) + , m_atol(atol) + , m_invar(${0 if p_invariants is None else p_invariants["n_invar"]}) + , m_invar0(${0 if p_invariants is None else p_invariants["n_invar"]}) + , m_rtol(rtol) + , m_get_dx_max_factor(get_dx_max_factor) , m_error_outside_bounds(error_outside_bounds) , m_max_invariant_violation(max_invariant_violation) , m_special_settings(special_settings) - , m_invar(${0 if p_invariants is None else p_invariants["n_invar"]}) - , m_invar0(${0 if p_invariants is None else p_invariants["n_invar"]}) { - ${src["common"]} + ${p_common["cses"]} use_get_dx_max = (m_get_dx_max_factor > 0.0) ? ${"true" if p_get_dx_max else "false"} : false; %if p_invariants is not None and p_support_recoverable_error: if (m_max_invariant_violation != 0.0){ @@ -113,8 +116,8 @@ namespace odesys_anyode { const realtype * const y = params + ${len(p_odesys.params)}; m_invar0.resize(${p_invariants["n_invar"]}); m_invar0.resize(${p_invariants["n_invar"]}); - realtype * const out = m_invar0.data(); - ${p_invariants["assign"]} + //realtype * const out = m_invar0.data(); + ${p_invariants["assign"].all(assign_to=lambda i: "m_invar0[%d]" % i)} } %endif ${"\n ".join(p_constructor)} @@ -147,7 +150,7 @@ namespace odesys_anyode { ${p_rhs} %else: ${"AnyODE::ignore(x);" if p_odesys.autonomous_exprs else ""} - ${p_rhs["assign"] + ${p_rhs["assign"].all()} this->nfev++; %if p_support_recoverable_error: if (m_error_outside_bounds){ @@ -172,9 +175,9 @@ namespace odesys_anyode { } } } - %if p_invariants is not None: + %if p_invariants is not None: if (m_max_invariant_violation != 0.0){ - ${p_invariants["assign"]} + ${p_invariants["assign"].all(assign_to=lambda i: "m_invar[%d]" % i)}} for (int idx=0; idx<${p_invariants["n_invar"]}; ++idx) { if (std::abs(m_invar[idx] - m_invar0[idx]) > ((m_max_invariant_violation > 0) ? m_max_invariant_violation @@ -184,7 +187,7 @@ namespace odesys_anyode { } } } - %endif + %endif %endif %if getattr(p_odesys, "_nonnegative", False) and p_support_recoverable_error: for (int i=0; i<${p_odesys.ny}; ++i) if (y[i] < 0) return AnyODE::Status::recoverable_error; @@ -214,12 +217,12 @@ namespace odesys_anyode { %else: AnyODE::ignore(fy); // Currently we are not using fy (could be done through extensive pattern matching) ${"AnyODE::ignore(x);" if p_odesys.autonomous_exprs else ""} - ${p_jtimes["assign"]} + ${p_jtimes["assign"].all()} %endif this->njvev++; return AnyODE::Status::success; %else: - AnyODE::ignore(v); AnyODE::ignore(Jv); AnyODE::ignore(x); + AnyODE::ignore(v); AnyODE::ignore(out); AnyODE::ignore(x); AnyODE::ignore(y); AnyODE::ignore(fy); return AnyODE::Status::unrecoverable_error; %endif @@ -243,24 +246,26 @@ namespace odesys_anyode { ${"AnyODE::ignore(y);" if (not any([yi in p_odesys.get_jac().free_symbols for yi in p_odesys.dep]) and not any([yi in p_odesys.get_dfdx().free_symbols for yi in p_odesys.dep])) else ""} - ${p_jac_dense["assign"]} - %for cse_assign in p_jac_dense["cses"]: - ${cse_assign}; - %endfor + ${p_jac_dense["cses"]} %for i_major in range(p_odesys.ny): %for i_minor in range(p_odesys.ny): + + <% - curr_expr = p_jac_dense["exprs"][i_minor, i_major] if order == "cmaj" else p_jac_dense["exprs"][i_major, i_minor] - if curr_expr == "0" and p_jacobian_set_to_zero_by_solver: - continue - %> jac[ldim*${i_major} + ${i_minor}] = ${curr_expr}; + if order == "cmaj": + i = i_minor*p_odesys.ny + i_major + else: + i = i_major*p_odesys.ny + i_minor + if p_jac_dense["assign"].expr_is_zero(i) and p_jacobian_set_to_zero_by_solver: + continue + %> + ${p_jac_dense["assign"](i, assign_to=lambda _: "jac[ldim*%d + %d]" % (i_major, i_minor))} %endfor - %endfor if (dfdt){ - %for idx, expr in enumerate(p_jac_dense["dfdt_exprs"]): - dfdt[${idx}] = ${expr}; + %for idx in range(p_odesys.ny**2, p_jac_dense["assign"].n): + ${p_jac_dense["assign"](i, assign_to=lambda _: "dfdt[%d]" % (idx - p_odesys.ny**2))} %endfor } this->njev++; @@ -303,7 +308,7 @@ namespace odesys_anyode { ${"AnyODE::ignore(x);" if p_odesys.autonomous_exprs else ""} ${"AnyODE::ignore(y);" if (not any([yi in p_odesys.get_jac().free_symbols for yi in p_odesys.dep]) and not any([yi in p_odesys.get_dfdx().free_symbols for yi in p_odesys.dep])) else ""} - ${p_jac_sparse["assign"]} + ${p_jac_sparse["assign"].all()} %for i in range(p_odesys.nnz): rowvals[${i}] = ${p_jac_sparse["rowvals"][i]}; @@ -316,7 +321,7 @@ namespace odesys_anyode { return AnyODE::Status::success; %else: AnyODE::ignore(x); AnyODE::ignore(y); AnyODE::ignore(fy); - AnyODE::ignore(data); AnyODE::ignore(colptrs); AnyODE::ignore(rowvals); + AnyODE::ignore(out); AnyODE::ignore(colptrs); AnyODE::ignore(rowvals); return AnyODE::Status::unrecoverable_error; %endif } @@ -360,14 +365,8 @@ namespace odesys_anyode { ${p_roots} %else: ${"" if any(p_odesys.indep in expr.free_symbols for expr in p_odesys.roots) else "AnyODE::ignore(x);"} - - %for cse_assign in p_roots["cses"]: - ${cse_assign}; - %endfor - - %for i, expr in enumerate(p_roots["exprs"]): - out[${i}] = ${expr}; - %endfor + ${p_roots["cses"]} + ${p_roots["assign"].all()} this->nrev++; return AnyODE::Status::success; %endif From 7acff638a63c62049e86c602ecd19967effbc7b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 30 Aug 2021 14:18:57 +0200 Subject: [PATCH 038/102] add subset of symcse --- pyodesys/native/_base.py | 6 +- .../native/sources/odesys_anyode_template.cpp | 8 +- pyodesys/native/symcse/__init__.py | 1 + pyodesys/native/{ => symcse}/compensated.py | 42 +-- pyodesys/native/{ => symcse}/core.py | 0 pyodesys/native/{ => symcse}/groupwise.py | 87 ++++--- pyodesys/native/symcse/util.py | 241 ++++++++++++++++++ 7 files changed, 321 insertions(+), 64 deletions(-) create mode 100644 pyodesys/native/symcse/__init__.py rename pyodesys/native/{ => symcse}/compensated.py (90%) rename pyodesys/native/{ => symcse}/core.py (100%) rename pyodesys/native/{ => symcse}/groupwise.py (61%) create mode 100644 pyodesys/native/symcse/util.py diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index f125681f..865d20a7 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -19,7 +19,7 @@ from ..symbolic import SymbolicSys from .. import __version__ -from .groupwise import GroupwiseCSE +from .symcse.groupwise import GroupwiseCSE try: import appdirs @@ -241,12 +241,12 @@ def variables(self): if self.compensated_summation: from .compensated import _NeumaierTransformer as Transformer else: - from .core import NullTransformer as Transformer + from .symcse.core import NullTransformer as Transformer gw = GroupwiseCSE( all_exprs, common_cse_template="m_cse[{}]", common_ignore=(self.odesys.indep,) + self.odesys.dep + v, - to_code=lambda x: self.odesys.be.ccode(x, math_macros={}), +# to_code=lambda x: self.odesys.be.ccode(x, math_macros={}), subsd=subsd, Transformer=Transformer ) diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 6253bb56..1f7800fa 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -150,6 +150,7 @@ namespace odesys_anyode { ${p_rhs} %else: ${"AnyODE::ignore(x);" if p_odesys.autonomous_exprs else ""} + ${p_rhs["cses"]} ${p_rhs["assign"].all()} this->nfev++; %if p_support_recoverable_error: @@ -177,6 +178,7 @@ namespace odesys_anyode { } %if p_invariants is not None: if (m_max_invariant_violation != 0.0){ + ${p_invariants["cses"]} ${p_invariants["assign"].all(assign_to=lambda i: "m_invar[%d]" % i)}} for (int idx=0; idx<${p_invariants["n_invar"]}; ++idx) { if (std::abs(m_invar[idx] - m_invar0[idx]) > ((m_max_invariant_violation > 0) @@ -217,6 +219,7 @@ namespace odesys_anyode { %else: AnyODE::ignore(fy); // Currently we are not using fy (could be done through extensive pattern matching) ${"AnyODE::ignore(x);" if p_odesys.autonomous_exprs else ""} + ${p_jtimes["cses"]} ${p_jtimes["assign"].all()} %endif this->njvev++; @@ -288,9 +291,7 @@ namespace odesys_anyode { %elif isinstance(p_first_step, str): ${p_first_step} %else: - %for cse_assign in p_first_step["cses"]: - const realtype ${cse_assign}; - %endfor + ${p_first_step["cses"]} ${"" if p_odesys.indep in p_odesys.first_step_expr.free_symbols else "AnyODE::ignore(x);"} ${"" if any([yi in p_odesys.first_step_expr.free_symbols for yi in p_odesys.dep]) else "AnyODE::ignore(y);"} return ${p_first_step["expr"]}; @@ -308,6 +309,7 @@ namespace odesys_anyode { ${"AnyODE::ignore(x);" if p_odesys.autonomous_exprs else ""} ${"AnyODE::ignore(y);" if (not any([yi in p_odesys.get_jac().free_symbols for yi in p_odesys.dep]) and not any([yi in p_odesys.get_dfdx().free_symbols for yi in p_odesys.dep])) else ""} + ${p_jac_sparse["cses"]} ${p_jac_sparse["assign"].all()} %for i in range(p_odesys.nnz): diff --git a/pyodesys/native/symcse/__init__.py b/pyodesys/native/symcse/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/pyodesys/native/symcse/__init__.py @@ -0,0 +1 @@ + diff --git a/pyodesys/native/compensated.py b/pyodesys/native/symcse/compensated.py similarity index 90% rename from pyodesys/native/compensated.py rename to pyodesys/native/symcse/compensated.py index b04b6d27..dc48cb0a 100644 --- a/pyodesys/native/compensated.py +++ b/pyodesys/native/symcse/compensated.py @@ -17,7 +17,7 @@ from sympy.codegen.ast import AssignmentBase, Token, While, break_ from .core import NullTransformer -from .util import OrderedAdd +from .ordered_add import ordered_add def If(cond, body): @@ -64,8 +64,8 @@ def finalize(self): @staticmethod def _impl_add(accum, carry, elem, temp, do_swap=False): """Perform Kahan-Babuska-Neumaier addition.""" - big_temp = OrderedAdd(OrderedAdd(accum, -temp), elem) - big_elem = OrderedAdd(OrderedAdd(elem, -temp), accum) + big_temp = ordered_add(ordered_add(accum, -temp), elem) + big_elem = ordered_add(ordered_add(elem, -temp), accum) abs_elem = Abs(elem) pw = Piecewise((big_temp, Abs(temp) > abs_elem), (big_elem, True)) statements = [ @@ -176,10 +176,7 @@ def _pipeline(self): final_exprs = self.red for pass_ in self.passes: statements = self._single_pass(statements, pass_) - new_exprs = [] - for expr in final_exprs: - new_exprs.append(pass_(None, expr, statements=statements)) - final_exprs = new_exprs + final_exprs = [pass_(None, e, statements=statements) for e in final_exprs] return statements, final_exprs def _pass_05_analysis(self, lhs, rhs, *, statements): @@ -199,7 +196,8 @@ def _pass_10_create_nodes(self, lhs, rhs, *, statements, debug=False): while True: for _add in filter(lambda x: x.is_Add, postorder_traversal(new_rhs)): - score = self._analysis.get(lhs, 0) + reduce(add, [self._analysis.get(k, 1) for k in _add.args]) + score = self._analysis.get(lhs, 0) + reduce(add, [ + self._analysis.get(k, 1) for k in _add.args]) if score >= self.limit or any(self._is_Neu(arg) for arg in _add.args): na = self._mk_Neu(_add.args, lhs) if _add is rhs and lhs is not None: @@ -234,7 +232,7 @@ def _pass_60_xrepl(self, lhs, rhs, *, statements): return rhs.xreplace(self.created) def _group(self, x): - all_accum, all_carry = [], [] + all_accum, all_carry, rest = [], [], [] for term in x.args: if term in self.created: all_accum.append(self.created[term].accum) @@ -244,16 +242,26 @@ def _group(self, x): elif term in self._all_carry: all_carry.append(term) else: - all_accum.append(term) - if all_accum and all_carry: - return OrderedAdd(OrderedAdd(*all_accum), OrderedAdd(*all_carry)) - else: - return OrderedAdd(*(all_accum+all_carry)) + rest.append(term) + result = [] + if all_accum: + result.append(ordered_add(*all_accum)) + if all_carry: + result.append(ordered_add(*all_carry)) + if rest: + result += rest + result = reduce(add, result) + return result + + def _has(self, term): + return term in self.created or term in self._all_accum or term in self._all_carry def _pass_95_group(self, lhs, rhs, *, statements): - return rhs.replace(lambda s: s.is_Add and any( - t in self.created or t in self._all_accum or t in self._all_carry for t in s.args - ), self._group) + new_rhs = rhs.replace( + lambda s: s.is_Add and any(self._has(t) for t in s.args), + self._group + ) + return new_rhs def _pass_90_fin(self, lhs, rhs, *, statements): return rhs.replace(lambda x: self._is_Neu(x), lambda x: x.finalize()) diff --git a/pyodesys/native/core.py b/pyodesys/native/symcse/core.py similarity index 100% rename from pyodesys/native/core.py rename to pyodesys/native/symcse/core.py diff --git a/pyodesys/native/groupwise.py b/pyodesys/native/symcse/groupwise.py similarity index 61% rename from pyodesys/native/groupwise.py rename to pyodesys/native/symcse/groupwise.py index 3dbbf5e4..1f916db7 100644 --- a/pyodesys/native/groupwise.py +++ b/pyodesys/native/symcse/groupwise.py @@ -4,14 +4,13 @@ from operator import add import numpy as np -from sympy import Symbol, cse, numbered_symbols +import sympy +from sympy import numbered_symbols from sympy.codegen.ast import Assignment, Variable, float64 from sympy.codegen.rewriting import create_expand_pow_optimization -from sympy.printing.c import ccode - from .core import NullTransformer - +from .util import CPrinter, Backend, ccode expand_up_to_3 = create_expand_pow_optimization(3) @@ -32,13 +31,13 @@ class GroupwiseCSE: def __init__(self, groups, *, common_cse_template="common_cse{0}", common_ignore=(), - to_code=lambda arg: ccode(arg, math_macros={}), + code_printer=None, subsd=None, Transformer=NullTransformer, type_=float64, pre_process=pre_process, post_process=post_process, - kw_cse=None + backend=None, ): """ Parameters @@ -47,20 +46,38 @@ def __init__(self, groups, *, \\*\\*kwargs : see code for now. """ - # self.groups = groups - self._to_code = to_code + if code_printer is None: + code_printer = CPrinter() + self._code_printer = code_printer self._subsd = subsd or {} self._type = type_ self._keys, _values = zip(*groups.items()) self._spans = np.cumsum([0]+list(map(len, _values))) - self._common_cse_template = common_cse_template - self._per_g_tformrs, self._comm_tformr = self._get_all_cses( - map(pre_process, reduce(add, map(list, _values))), - common_ignore=common_ignore, - post_process=post_process, - Transformer=Transformer, - kw_cse=kw_cse or {}, + if backend is None: + backend = Backend() + self.backend = backend + _all_values = reduce(add, map(list, _values)) + _all_exprs = list(map(pre_process, _all_values)) + for e in _all_exprs: + for fs in e.free_symbols: + if not fs.is_real: + # Switching between symengine/sympy is tricky, consistently using + # real=True with Symbols allows us to assume real=True for SymEngine + # symbols (which lack support for assumptions at the time of writing). + raise NotImplementedError("Only use explicitly real valued symbols.") + repls, reds = self._common_cse( + _all_exprs, ignore=common_ignore, + symbols=numbered_symbols('cse_temporary', real=True) ) + self._comm_tformr = Transformer(repls, reds, ignore=common_ignore) + remap = self._comm_tformr.remapping_for_arrayification(template=common_cse_template) + self._comm_tformr.apply_remapping(remap) + self.n_remapped = len(remap) + + assert(len(self._comm_tformr.final_exprs) == len(reds)) + del reds + self._per_g_tformrs = self._get_g_tformrs(self._comm_tformr, Transformer=Transformer) + @property def keys(self): @@ -69,17 +86,19 @@ def keys(self): def render(self, x): """Generate a code string.""" - return self._to_code(x.xreplace(self._subsd)) + x2 = x.xreplace(self._subsd) + assert not any(x2.has(k) for k in self._subsd) + return self._code_printer.doprint(x.xreplace(self._subsd)) def _common_cse(self, all_exprs, **kwargs): - repls, reds = cse(all_exprs, **kwargs) + repls, reds = self.backend.cse(all_exprs, **kwargs) # def comm_symbols(): # idx = 0 # while True: # yield Symbol(self._common_cse_template.format(idx), real=True) # idx += 1 #cse_symbols = comm_symbols() - cse_symbols = numbered_symbols("cse_t") # local temporaries + cse_symbols = numbered_symbols("cse_t", real=True) # local temporaries comm_subs = {} for lhs, rhs in repls: for expr in reds: @@ -91,30 +110,16 @@ def _common_cse(self, all_exprs, **kwargs): [r.xreplace(comm_subs) for r in reds] ) - def _get_all_cses(self, all_exprs, *, common_ignore, post_process, - Transformer, kw_cse): - repls, reds = self._common_cse( - all_exprs, ignore=common_ignore, - symbols=numbered_symbols('cse_temporary'), **kw_cse) - comm_tformer = Transformer(repls, reds, ignore=common_ignore) - - assert(len(comm_tformer.final_exprs) == len(reds)) - del reds - - remap = comm_tformer.remapping_for_arrayification(template=self._common_cse_template) - comm_tformer.apply_remapping(remap) - self.n_remapped = len(remap) - - grp = {} + def _get_g_tformrs(self, comm_tformr, Transformer): + per_g = {} for i, k in enumerate(self._keys): - g_repls, g_exprs = cse( - comm_tformer.final_exprs[slice(*self._spans[i:i+2])], - symbols=numbered_symbols("cse"), **kw_cse - ) - g_tformer = Transformer(g_repls, g_exprs, parent=comm_tformer) - grp[k] = g_tformer - - return (grp, comm_tformer) + g_repls, g_exprs = self.backend.cse( + comm_tformr.final_exprs[slice(*self._spans[i:i+2])], + symbols=numbered_symbols("cse", real=True)) + g_tformr = Transformer(g_repls, g_exprs, parent=comm_tformr) + per_g[k] = g_tformr + + return per_g @staticmethod def _declare(stmts, *, pred, type_=float64): diff --git a/pyodesys/native/symcse/util.py b/pyodesys/native/symcse/util.py new file mode 100644 index 00000000..39dd4c33 --- /dev/null +++ b/pyodesys/native/symcse/util.py @@ -0,0 +1,241 @@ +"""Utilities used in the package.""" +import os +import sys +from functools import reduce +from operator import mul, add + +import sympy +from sympy.printing.c import C99CodePrinter +from sympy.printing.cxx import CXX17CodePrinter +from sympy.printing.pycode import PythonCodePrinter + +try: + import symengine as se +except ModuleNotFoundError: + se = None +else: + from symengine.lib.symengine_wrapper import sympy2symengine + + +def partn(it, pred): + """Partition an iterable into False/True groups based on predicate.""" + result = ([], []) + for elem in it: + result[pred(elem)].append(elem) + return result + + +def prod(it): + return reduce(mul, it) + + +def map_multi(funcs, its): + return (f(it) for f, it in zip(funcs, its)) + + +def idty(x): + """Identity operattion (idempotent).""" + return x + + +class _UnevaluatedExprPrinterMixin: + def _print_UnevaluatedExpr(self, arg): + return "(%s)" % super()._print_UnevaluatedExpr(arg) + + +class CXXPrinter(_UnevaluatedExprPrinterMixin, CXX17CodePrinter): + """Patched C++-printer (https://github.com/sympy/sympy/issues/21955).""" + + +class CPrinter(_UnevaluatedExprPrinterMixin, C99CodePrinter): + """Patched C-printer (https://github.com/sympy/sympy/issues/21955).""" + + +class PyPrinter(_UnevaluatedExprPrinterMixin, PythonCodePrinter): + """Patched Python-printer (https://github.com/sympy/sympy/issues/21955).""" + + +def _cse_symengine(exprs, *, se2sympy, ignore=(), symbols=None, **kwargs): + exprs = list(exprs) + se_exprs = [sympy2symengine(e, raise_error=True) for e in exprs] + repl, red = se.cse(se_exprs, **kwargs) + repl = [(se2sympy(lhs), se2sympy(rhs)) for lhs, rhs in repl] + red = [se2sympy(e) for e in red] + if ignore: + def has_ig(e): + return any(e.has(ig) for ig in ignore) + + def is_only_ig(e): + return all(s in ignore for s in e.free_symbols) + + keep, reintro = {}, {} + for lhs, rhs in repl: + rem = rhs.xreplace(reintro) # remaining + if not has_ig(rem): + for k, v in keep.items(): + if v == rem: + reintro[lhs] = k + break + else: + keep[lhs] = rem + continue + + candidates = [] + for part in sympy.preorder_traversal(rem): + if not has_ig(part): + candidates.append(part) + elif part.is_Add: + good, bad = partn(part.args, has_ig) + if good: + candidates.append(sum(good)) + elif part.is_Mul: + good, bad = partn(part.args, has_ig) + if good: + candidates.append(prod(good)) + if candidates: + part = sorted(candidates, key=sympy.count_ops)[-1] + if part.count_ops() == 0: + reintro[lhs] = rem + else: + for k, v in keep.items(): + if part == k or part == v: + reintro[lhs] = rem + break + else: + keep[lhs] = part + reintro[lhs] = rem.subs(part, lhs) + else: + reintro[lhs] = rem + repl = list(keep.items()) + red = [e.xreplace(reintro) for e in red] + if symbols is not None: + remap = {} + updated = [] + for lhs, rhs in repl: + remap[lhs] = next(symbols) + updated.append((remap[lhs], rhs.xreplace(remap))) + repl = updated + red = [e.xreplace(remap) for e in red] + + return repl, red + + + +class Backend: + """Allow optional use of SymEngine.""" + + def __init__(self, use_symengine=None, kw_cse=None, assume_real=True): + """Initialize a symbolic backend. + + Parameters + ---------- + use_symengine: bool + kw_cse: keywords to use in CSE + + """ + if use_symengine is None: + _req_backend = os.environ.get("SYMCSE_BACKEND", "").lower() + if _req_backend == "symengine": + use_symengine = True + elif _req_backend == "sympy": + use_symengine = False + elif _req_backend == "": + use_symengine = se is not None + else: + raise ValueError("Unknown SYMCXSE_BACKEND: %s" % _req_backend) + if use_symengine and se is None: + raise ValueError("symengine missing (pip install symengine)") + self.use_symengine = use_symengine + self.kw_cse = kw_cse + self.assume_real = assume_real + + + def Symbol(self, name): + if self.use_symengine: + return se.Symbol(name) # https://github.com/symengine/symengine.py/issues/286 + else: + return sympy.Symbol(name, real=self.assume_real) + + def sympy2se(self, x): + if hasattr(x, '_sympy_'): + return x # looks like that's already a SymEngine object + return sympy2symengine(x, raise_error=True) + + def se2sympy(self, x): + if not hasattr(x, '_sympy_'): + assert isinstance(x, sympy.Basic) + return x # looks like that's already a SymPy object + tmp = x._sympy_() + return tmp.xreplace({s: sympy.Symbol(s.name, real=self.assume_real) for s in tmp.free_symbols}) + + def cse(self, exprs, **kwargs): + """Perform common sub-expression elimination.""" + exprs = list(exprs) + new_kw = dict(**(self.kw_cse or {}), **kwargs) + if self.use_symengine: + return _cse_symengine(exprs, se2sympy=self.se2sympy, **new_kw) + else: + return sympy.cse(exprs, **new_kw) + + def _elems_colmaj(self, mtx): + if not mtx: + return [] + result = reduce(add, mtx.T.tolist()) + if self.use_symengine: + return [self.se2sympy(e) for e in result] + else: + return result + + def _col_vec(self, v): + args = len(v), 1, tuple(v) + if self.use_symengine: + return se.Matrix(*args) + else: + return sympy.Matrix(*args) + + def jacobian(self, exprs, wrt): + args = len(exprs), 1, exprs + if self.use_symengine: + return se.Matrix(*args).jacobian(self._col_vec(wrt)) + else: + return sympy.Matrix(*args).jacobian(wrt) + + def _LU(self, mat): + if self.use_symengine: + _L, _U = self.sympy2se(mat).LU() + L, U = map(self.se2sympy, [_L, _U]) + else: + L, U, piv = mat.LUdecomposition() + if piv: + raise NotImplementedError("TODO") + for i in range(L.cols): + for j in range(i, L.cols): + L[i, j] = U[i, j] + return L + + def matmul(self, A, b): + if self.use_symengine: + return A.mul_matrix(b) + else: + return A @ b + + def lambdify(self, args, exprs): + if self.use_symengine: + return se.Lambdify(args, exprs) + else: + return sympy.lambdify(args, exprs) + + +def ccode(arg, **kwargs): + p = CPrinter(settings=(kwargs or dict(math_macros={}))) + return p.doprint(arg) + + +def cxxcode(arg, **kwargs): + p = CXXPrinter(settings=(kwargs or dict(math_macros={}))) + return p.doprint(arg) + + +def pycode(arg): + p = PyPrinter() + return p.doprint(arg) From 5752213dd959dc5ef9603c2bbfaa1ef8195f91d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 30 Aug 2021 23:22:49 +0200 Subject: [PATCH 039/102] mostly passing --- pyodesys/native/_base.py | 36 +++++++++---------- .../sources/odesys_anyode_iterative.hpp | 3 +- .../native/sources/odesys_anyode_template.cpp | 12 ++++--- pyodesys/native/symcse/groupwise.py | 3 +- pyodesys/native/symcse/util.py | 10 ++++-- pyodesys/native/tests/_tests.py | 8 +++-- pyodesys/native/tests/test_cvode.py | 5 +-- 7 files changed, 45 insertions(+), 32 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 865d20a7..3919df9a 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -43,7 +43,7 @@ logger = logging.getLogger(__name__) _compile_kwargs = { - 'options': ['warn', 'pic', 'fast', 'openmp'], + 'options': ['warn', 'pic', 'debug', 'openmp'], # DO-NOT-MERGE!!! debug/fast 'std': 'c++11', 'include_dirs': [np.get_include(), pkg_resources.resource_filename(__name__, 'sources')], 'libraries': [], @@ -84,7 +84,7 @@ def expr_is_zero(self, i): return self.gw.exprs(self.k)[i] == 0 def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): - return self.gw.render(Assignment(_r(assign_to(i)), self.gw.exprs(self.k)[i])) + ";" + return self.gw.render(Assignment(_r(assign_to(i)), self.gw.exprs(self.k)[i])) class _AssignerPlain(_AssignerBase): @@ -103,7 +103,7 @@ def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): class _NativeCodeBase(Cpp_Code): - """ Base class for generated code. + """Base class for generated code. Note kwargs ``namespace_override`` which allows the user to customize the variables used when rendering the template. @@ -214,39 +214,35 @@ def variables(self): v = () jtimes_exprs = () - if jtimes is not False: - subsd.update({k: self.odesys.be.Symbol('v[%d]' % idx) for - idx, k in enumerate(v)}) - first_step = self.odesys.first_step_expr if first_step is not None: - first_step_cses, first_step_exprs = cse_cb( - [first_step], - symbols=self.odesys.be.numbered_symbols('cse')) + all_exprs["first_step"] = [first_step] if self.odesys.roots is not None: - roots_cses, roots_exprs = cse_cb( - self.odesys.roots, - symbols=self.odesys.be.numbered_symbols('cse')) - + all_exprs["roots"] = self.odesys.roots subsd = {k: self.odesys.be.Symbol('y[%d]' % idx) for idx, k in enumerate(self.odesys.dep)} - subsd[self.odesys.indep] = self.odesys.be.Symbol('x') + if self.odesys.indep is not None: + subsd[self.odesys.indep] = self.odesys.be.Symbol('x') subsd.update({k: self.odesys.be.Symbol('m_p[%d]' % idx) for idx, k in enumerate(self.odesys.params)}) + if jtimes is not False: + subsd.update({k: self.odesys.be.Symbol('v[%d]' % idx) for + idx, k in enumerate(v)}) + if self.use_cse: if self.compensated_summation: - from .compensated import _NeumaierTransformer as Transformer + from .symcse.compensated import _NeumaierTransformer as Transformer else: from .symcse.core import NullTransformer as Transformer + ignore = (() if self.odesys.indep is None else (self.odesys.indep,)) + self.odesys.dep + v gw = GroupwiseCSE( all_exprs, common_cse_template="m_cse[{}]", - common_ignore=(self.odesys.indep,) + self.odesys.dep + v, -# to_code=lambda x: self.odesys.be.ccode(x, math_macros={}), + common_ignore=ignore, subsd=subsd, Transformer=Transformer ) @@ -306,8 +302,8 @@ def _cses(k, assign_to=lambda i: _r("out[%d]" % i)): 'assign': assigners["roots"] }, p_invariants=None if all_invar == () else { - 'cses': cses["invariants"], - 'assign': assigners["invariants"], + 'cses': cses["invar"], + 'assign': assigners["invar"], 'n_invar': len(all_invar) }, p_nroots=self.odesys.nroots, diff --git a/pyodesys/native/sources/odesys_anyode_iterative.hpp b/pyodesys/native/sources/odesys_anyode_iterative.hpp index 705d7b88..2103ae45 100644 --- a/pyodesys/native/sources/odesys_anyode_iterative.hpp +++ b/pyodesys/native/sources/odesys_anyode_iterative.hpp @@ -5,10 +5,11 @@ namespace odesys_anyode { template struct OdeSys : public AnyODE::OdeSysIterativeBase { std::vector m_p; - std::vector m_p_cse; + std::vector m_cse; std::vector m_atol; std::vector m_upper_bounds; std::vector m_lower_bounds; + std::vector m_invar; std::vector m_invar0; Real_t m_rtol; Real_t m_get_dx_max_factor; diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 1f7800fa..b3792897 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -3,6 +3,9 @@ <%doc> This is file is a mako template for a C++ source file defining the ODE system. +<%! +import sympy +%> // User provided system description: ${p_odesys.description} // Names of dependent variables: ${p_odesys.names} // Names of parameters: ${p_odesys.param_names} @@ -145,13 +148,13 @@ namespace odesys_anyode { AnyODE::Status OdeSys::rhs(realtype x, const realtype * const ANYODE_RESTRICT y, - realtype * const ANYODE_RESTRICT out) { + realtype * const ANYODE_RESTRICT f) { %if isinstance(p_rhs, str): ${p_rhs} %else: ${"AnyODE::ignore(x);" if p_odesys.autonomous_exprs else ""} ${p_rhs["cses"]} - ${p_rhs["assign"].all()} + ${p_rhs["assign"].all(assign_to=lambda i: sympy.Symbol("f[%d]" % i))} this->nfev++; %if p_support_recoverable_error: if (m_error_outside_bounds){ @@ -179,7 +182,7 @@ namespace odesys_anyode { %if p_invariants is not None: if (m_max_invariant_violation != 0.0){ ${p_invariants["cses"]} - ${p_invariants["assign"].all(assign_to=lambda i: "m_invar[%d]" % i)}} + ${p_invariants["assign"].all(assign_to=lambda i: "m_invar[%d]" % i)} for (int idx=0; idx<${p_invariants["n_invar"]}; ++idx) { if (std::abs(m_invar[idx] - m_invar0[idx]) > ((m_max_invariant_violation > 0) ? m_max_invariant_violation @@ -294,7 +297,8 @@ namespace odesys_anyode { ${p_first_step["cses"]} ${"" if p_odesys.indep in p_odesys.first_step_expr.free_symbols else "AnyODE::ignore(x);"} ${"" if any([yi in p_odesys.first_step_expr.free_symbols for yi in p_odesys.dep]) else "AnyODE::ignore(y);"} - return ${p_first_step["expr"]}; + const auto ${p_first_step["assign"](0, assign_to=lambda _: "result")} + return result; %endif } diff --git a/pyodesys/native/symcse/groupwise.py b/pyodesys/native/symcse/groupwise.py index 1f916db7..f8511859 100644 --- a/pyodesys/native/symcse/groupwise.py +++ b/pyodesys/native/symcse/groupwise.py @@ -49,7 +49,7 @@ def __init__(self, groups, *, if code_printer is None: code_printer = CPrinter() self._code_printer = code_printer - self._subsd = subsd or {} + self._subsd = {sympy.Symbol(k.name, real=True): v for k, v in (subsd or {}).items()} self._type = type_ self._keys, _values = zip(*groups.items()) self._spans = np.cumsum([0]+list(map(len, _values))) @@ -58,6 +58,7 @@ def __init__(self, groups, *, self.backend = backend _all_values = reduce(add, map(list, _values)) _all_exprs = list(map(pre_process, _all_values)) + _all_exprs = [e.replace(lambda s: s.is_Symbol, lambda s: sympy.Symbol(s.name, real=True)) for e in _all_exprs] for e in _all_exprs: for fs in e.free_symbols: if not fs.is_real: diff --git a/pyodesys/native/symcse/util.py b/pyodesys/native/symcse/util.py index 39dd4c33..ad41184a 100644 --- a/pyodesys/native/symcse/util.py +++ b/pyodesys/native/symcse/util.py @@ -42,6 +42,12 @@ class _UnevaluatedExprPrinterMixin: def _print_UnevaluatedExpr(self, arg): return "(%s)" % super()._print_UnevaluatedExpr(arg) + def _print_Integer(self, arg): + if abs(arg) > 2**53: + return self._print(sympy.Float(arg)) + else: + return super()._print_Integer(arg) + class CXXPrinter(_UnevaluatedExprPrinterMixin, CXX17CodePrinter): """Patched C++-printer (https://github.com/sympy/sympy/issues/21955).""" @@ -62,6 +68,8 @@ def _cse_symengine(exprs, *, se2sympy, ignore=(), symbols=None, **kwargs): repl = [(se2sympy(lhs), se2sympy(rhs)) for lhs, rhs in repl] red = [se2sympy(e) for e in red] if ignore: + ignore = [se2sympy(sympy2symengine(ig)) for ig in ignore] + def has_ig(e): return any(e.has(ig) for ig in ignore) @@ -116,11 +124,9 @@ def is_only_ig(e): updated.append((remap[lhs], rhs.xreplace(remap))) repl = updated red = [e.xreplace(remap) for e in red] - return repl, red - class Backend: """Allow optional use of SymEngine.""" diff --git a/pyodesys/native/tests/_tests.py b/pyodesys/native/tests/_tests.py index d3e980cd..e8c33b10 100644 --- a/pyodesys/native/tests/_tests.py +++ b/pyodesys/native/tests/_tests.py @@ -340,7 +340,7 @@ def _test_return_on_error_success(NativeSys): assert np.allclose(result.yout[:nreached, :], ref, rtol=1e-8, atol=1e-8) -def _test_render_native_code_cse(NativeSys): +def _test_render_native_code_cse(NativeSys, compensated): # regression test taken from chempy from pyodesys.symbolic import SymbolicSys from sympy import symbols, log, exp @@ -419,7 +419,11 @@ def _solve(odesys, **kwargs): ref = _solve(symbolic, **kw) assert ref.info['success'] - native = NativeSys.from_other(symbolic) # <-- regression test, optional: save_temp=True + native = NativeSys.from_other( + symbolic, + compensated_summation=compensated, + #save_temp=True, + ) # regression test: sol = _solve(native, **kw) assert sol.info['success'] diff --git a/pyodesys/native/tests/test_cvode.py b/pyodesys/native/tests/test_cvode.py index 07485a03..b208e92f 100644 --- a/pyodesys/native/tests/test_cvode.py +++ b/pyodesys/native/tests/test_cvode.py @@ -241,8 +241,9 @@ def f(t, y, p): @requires('pycvodes', 'sympy') -def test_render_native_cse_regression(): - _test_render_native_code_cse(NativeSys) +@pytest.mark.parametrize('compensated', [False, True]) +def test_render_native_cse_regression(compensated): + _test_render_native_code_cse(NativeSys, compensated=compensated) @requires('sym', 'pycvodes') From 895c91ef3e1a391674c717a6dc50a11a6bf08cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 30 Aug 2021 23:31:03 +0200 Subject: [PATCH 040/102] grab compensated_summation kwarg --- pyodesys/native/_base.py | 11 +++++++---- pyodesys/native/symcse/ordered_add.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 pyodesys/native/symcse/ordered_add.py diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 3919df9a..ee54b460 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -328,14 +328,17 @@ def __init__(self, *args, native_code_kw=None, **kwargs): namespace_override = kwargs.pop('namespace_override', {}) namespace_extend = kwargs.pop('namespace_extend', {}) save_temp = kwargs.pop('save_temp', False) + compensated_summation = kwargs.pop('compensated_summation', False) if 'init_indep' not in kwargs: # we need to trigger append_iv for when invariants are used kwargs['init_indep'] = True kwargs['init_dep'] = True super(_NativeSysBase, self).__init__(*args, **kwargs) - self._native = self._NativeCode(self, save_temp=save_temp, - namespace_override=namespace_override, - namespace_extend=namespace_extend, - **(native_code_kw or {})) + self._native = self._NativeCode( + self, save_temp=save_temp, + compensated_summation=compensated_summation, + namespace_override=namespace_override, + namespace_extend=namespace_extend, + **(native_code_kw or {})) def integrate(self, *args, **kwargs): integrator = kwargs.pop('integrator', 'native') diff --git a/pyodesys/native/symcse/ordered_add.py b/pyodesys/native/symcse/ordered_add.py new file mode 100644 index 00000000..f6032b35 --- /dev/null +++ b/pyodesys/native/symcse/ordered_add.py @@ -0,0 +1,19 @@ +"""Utilities used in the package.""" + +from sympy import Expr, UnevaluatedExpr +from sympy.printing.precedence import PRECEDENCE, precedence +from sympy.printing.c import C99CodePrinter +from sympy.printing.cxx import CXX17CodePrinter + + +def ordered_add(*args): + if len(args) == 0: + return 0 + elif (len(args)) == 1: + return args[0] + a, b, *rest = args + ue = UnevaluatedExpr(a+b) + if len(rest) == 0: + return ue + else: + return ordered_add(ue, *rest) From 0895b933c458b45bd15917d00e9230ea07fdad5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 31 Aug 2021 22:22:04 +0200 Subject: [PATCH 041/102] update symcse --- pyodesys/native/_base.py | 8 ++++---- .../native/sources/odesys_anyode_template.cpp | 3 +++ pyodesys/native/symcse/compensated.py | 8 ++++---- pyodesys/native/symcse/groupwise.py | 9 ++------- pyodesys/native/symcse/ordered_add.py | 8 +++----- pyodesys/native/symcse/util.py | 18 ++++++++++++++++++ 6 files changed, 34 insertions(+), 20 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index ee54b460..7a7d7f35 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -178,9 +178,9 @@ def __init__(self, odesys, *args, use_cse=None, **kwargs): self.compensated_summation = kwargs.pop("compensated_summation", os.environ.get("PYODESYS_COMPENSATED_SUMMATION", "0") == "1") super().__init__(*args, logger=logger, **kwargs) - def _ccode(self, expr, subsd): - expr_x = expr.xreplace(subsd) - return self.odesys.be.ccode(expr_x) + # def _ccode(self, expr, subsd): + # expr_x = expr.xreplace(subsd) + # return self.odesys.be.ccode(expr_x) def variables(self): ny = self.odesys.ny @@ -347,7 +347,7 @@ def integrate(self, *args, **kwargs): else: kwargs['integrator'] = 'native' - return super(_NativeSysBase, self).integrate(*args, **kwargs) + return super().integrate(*args, **kwargs) def rhs(self, intern_t, intern_y, intern_p): return self._native.mod.rhs(intern_t, intern_y, intern_p) diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index b3792897..412ae02f 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -19,6 +19,9 @@ import sympy #include ${inc} %endfor +// sympy wraps real around symbols which actually are real: +#define re(x) (x) + namespace { // anonymous namespace for user-defined helper functions std::vector p_odesys_names ${"" if p_odesys.names is None else '= {"%s"}' % '", "'.join(p_odesys.names)}; %if p_anon is not None: diff --git a/pyodesys/native/symcse/compensated.py b/pyodesys/native/symcse/compensated.py index dc48cb0a..4204ad8e 100644 --- a/pyodesys/native/symcse/compensated.py +++ b/pyodesys/native/symcse/compensated.py @@ -141,9 +141,9 @@ def remapping_for_arrayification(self, template="m_glob[{0}]"): def _mk_Neu(self, terms, lhs): pfx = str(next(self._tmp_var)) if lhs is None else str(lhs) - accum = Symbol(pfx+'a') - carry = Symbol(pfx+'c') - tempv = Symbol(pfx+'t') + accum = Symbol(pfx+'a', real=True) + carry = Symbol(pfx+'c', real=True) + tempv = Symbol(pfx+'t', real=True) na = _NeumaierAdd(terms, accum, carry, tempv) self._all_accum[accum] = na self._all_carry[carry] = na @@ -217,7 +217,7 @@ def _pass_50_to_stmnts(self, lhs, rhs, *, statements): for neu in map(self.created.get, postorder_traversal(rhs)): if neu is None: continue - for t_neu in map(self.created.get, neu.terms): + for t_neu in map(self.created.get, postorder_traversal(neu.terms)): if t_neu is None: continue if t_neu not in self.expanded: diff --git a/pyodesys/native/symcse/groupwise.py b/pyodesys/native/symcse/groupwise.py index f8511859..d8e9feb5 100644 --- a/pyodesys/native/symcse/groupwise.py +++ b/pyodesys/native/symcse/groupwise.py @@ -59,6 +59,7 @@ def __init__(self, groups, *, _all_values = reduce(add, map(list, _values)) _all_exprs = list(map(pre_process, _all_values)) _all_exprs = [e.replace(lambda s: s.is_Symbol, lambda s: sympy.Symbol(s.name, real=True)) for e in _all_exprs] + common_ignore = [sympy.Symbol(ig.name, real=True) for ig in common_ignore] for e in _all_exprs: for fs in e.free_symbols: if not fs.is_real: @@ -68,7 +69,7 @@ def __init__(self, groups, *, raise NotImplementedError("Only use explicitly real valued symbols.") repls, reds = self._common_cse( _all_exprs, ignore=common_ignore, - symbols=numbered_symbols('cse_temporary', real=True) + symbols=numbered_symbols('cse_comm_locl', real=True) ) self._comm_tformr = Transformer(repls, reds, ignore=common_ignore) remap = self._comm_tformr.remapping_for_arrayification(template=common_cse_template) @@ -93,12 +94,6 @@ def render(self, x): def _common_cse(self, all_exprs, **kwargs): repls, reds = self.backend.cse(all_exprs, **kwargs) - # def comm_symbols(): - # idx = 0 - # while True: - # yield Symbol(self._common_cse_template.format(idx), real=True) - # idx += 1 - #cse_symbols = comm_symbols() cse_symbols = numbered_symbols("cse_t", real=True) # local temporaries comm_subs = {} for lhs, rhs in repls: diff --git a/pyodesys/native/symcse/ordered_add.py b/pyodesys/native/symcse/ordered_add.py index f6032b35..6fec35a8 100644 --- a/pyodesys/native/symcse/ordered_add.py +++ b/pyodesys/native/symcse/ordered_add.py @@ -1,18 +1,16 @@ """Utilities used in the package.""" -from sympy import Expr, UnevaluatedExpr -from sympy.printing.precedence import PRECEDENCE, precedence -from sympy.printing.c import C99CodePrinter -from sympy.printing.cxx import CXX17CodePrinter +from .util import UnevaluatedRealPropagatingExpr def ordered_add(*args): + """Preserve the order of terms.""" if len(args) == 0: return 0 elif (len(args)) == 1: return args[0] a, b, *rest = args - ue = UnevaluatedExpr(a+b) + ue = UnevaluatedRealPropagatingExpr(a+b) if len(rest) == 0: return ue else: diff --git a/pyodesys/native/symcse/util.py b/pyodesys/native/symcse/util.py index ad41184a..b14b90d6 100644 --- a/pyodesys/native/symcse/util.py +++ b/pyodesys/native/symcse/util.py @@ -38,6 +38,13 @@ def idty(x): return x +class UnevaluatedRealPropagatingExpr(sympy.UnevaluatedExpr): + """Propagate .is_real, but nothings else from wrapped expression.""" + + def _eval_is_real(self): + return self.args[0].is_real + + class _UnevaluatedExprPrinterMixin: def _print_UnevaluatedExpr(self, arg): return "(%s)" % super()._print_UnevaluatedExpr(arg) @@ -48,6 +55,17 @@ def _print_Integer(self, arg): else: return super()._print_Integer(arg) + @staticmethod + def _replace_re(arg): + if isinstance(arg, sympy.UnevaluatedExpr) and arg.args[0].is_real: + return UnevaluatedRealPropagatingExpr(arg.args[0]) + else: + return sympy.re(arg) + + def doprint(self, x): + result = super().doprint(x.replace(sympy.re, self._replace_re)) + return result + class CXXPrinter(_UnevaluatedExprPrinterMixin, CXX17CodePrinter): """Patched C++-printer (https://github.com/sympy/sympy/issues/21955).""" From dcc4ab86883d9f684b2000b1bb64d2b351f16957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 1 Sep 2021 01:06:21 +0200 Subject: [PATCH 042/102] Fix symcse issue --- pyodesys/native/_base.py | 3 ++- pyodesys/native/symcse/groupwise.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 7a7d7f35..7d78dccb 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -244,7 +244,8 @@ def variables(self): common_cse_template="m_cse[{}]", common_ignore=ignore, subsd=subsd, - Transformer=Transformer + Transformer=Transformer, + pre_process=None ) def not_arr(s): diff --git a/pyodesys/native/symcse/groupwise.py b/pyodesys/native/symcse/groupwise.py index d8e9feb5..55ed53f4 100644 --- a/pyodesys/native/symcse/groupwise.py +++ b/pyodesys/native/symcse/groupwise.py @@ -57,7 +57,7 @@ def __init__(self, groups, *, backend = Backend() self.backend = backend _all_values = reduce(add, map(list, _values)) - _all_exprs = list(map(pre_process, _all_values)) + _all_exprs = list(map(pre_process, _all_values) if pre_process else _all_values) _all_exprs = [e.replace(lambda s: s.is_Symbol, lambda s: sympy.Symbol(s.name, real=True)) for e in _all_exprs] common_ignore = [sympy.Symbol(ig.name, real=True) for ig in common_ignore] for e in _all_exprs: From a5d9bd0e0c151a7403eec28d1b27585d5fcd741d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 1 Sep 2021 01:22:48 +0200 Subject: [PATCH 043/102] also test use_cse --- pyodesys/native/_base.py | 2 -- pyodesys/native/tests/_tests.py | 9 ++++++--- pyodesys/native/tests/test_gsl.py | 5 +++-- pyodesys/native/tests/test_odeint.py | 3 ++- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 7d78dccb..030a250c 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -329,14 +329,12 @@ def __init__(self, *args, native_code_kw=None, **kwargs): namespace_override = kwargs.pop('namespace_override', {}) namespace_extend = kwargs.pop('namespace_extend', {}) save_temp = kwargs.pop('save_temp', False) - compensated_summation = kwargs.pop('compensated_summation', False) if 'init_indep' not in kwargs: # we need to trigger append_iv for when invariants are used kwargs['init_indep'] = True kwargs['init_dep'] = True super(_NativeSysBase, self).__init__(*args, **kwargs) self._native = self._NativeCode( self, save_temp=save_temp, - compensated_summation=compensated_summation, namespace_override=namespace_override, namespace_extend=namespace_extend, **(native_code_kw or {})) diff --git a/pyodesys/native/tests/_tests.py b/pyodesys/native/tests/_tests.py index e8c33b10..a53417ee 100644 --- a/pyodesys/native/tests/_tests.py +++ b/pyodesys/native/tests/_tests.py @@ -95,8 +95,11 @@ class TransformedNativeSys(TransformedSys, NativeSys): assert np.allclose(yout, ref, rtol=rtol*forgive, atol=atol*forgive) -def _test_Decay_nonnegative(NativeSys): - odesys = NativeSys.from_other(_get_decay3(lower_bounds=[0]*3)) +def _test_Decay_nonnegative(NativeSys, use_cse): + odesys = NativeSys.from_other( + _get_decay3(lower_bounds=[0]*3), + native_code_kw=dict(compensated_summation=compensated), + ) y0, k = [3., 2., 1.], [3.5, 2.5, 0] xout, yout, info = odesys.integrate([1e-10, 1], y0, k, integrator='native') ref = np.array(bateman_full(y0, k, xout - xout[0], exp=np.exp)).T @@ -421,7 +424,7 @@ def _solve(odesys, **kwargs): native = NativeSys.from_other( symbolic, - compensated_summation=compensated, + native_code_kw=dict(compensated_summation=compensated), #save_temp=True, ) # regression test: sol = _solve(native, **kw) diff --git a/pyodesys/native/tests/test_gsl.py b/pyodesys/native/tests/test_gsl.py index 1e692852..dd1f5cc7 100644 --- a/pyodesys/native/tests/test_gsl.py +++ b/pyodesys/native/tests/test_gsl.py @@ -74,8 +74,9 @@ def test_chained_multi_native(reduced): @requires('pygslodeiv2') -def test_Decay_nonnegative(): - _test_Decay_nonnegative(NativeSys) +@pytest.mark.parametrize('use_cse', [False, True]) +def test_Decay_nonnegative(use_cse): + _test_Decay_nonnegative(NativeSys, use_cse=use_cse) @requires('pygslodeiv2') diff --git a/pyodesys/native/tests/test_odeint.py b/pyodesys/native/tests/test_odeint.py index 3339e4c4..a75a5412 100644 --- a/pyodesys/native/tests/test_odeint.py +++ b/pyodesys/native/tests/test_odeint.py @@ -63,8 +63,9 @@ def test_PartiallySolved_symmetric_native_multi(): @requires('pyodeint') +@pytest.mark.parametrize('use_cse', [False, True]) def test_Decay_nonnegative(): - _test_Decay_nonnegative(NativeSys) + _test_Decay_nonnegative(NativeSys, use_cse=use_cse) @requires('pyodeint') From bd314e6583a836154c62e2280bd1f0b7cd5adc1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 1 Sep 2021 10:03:32 +0200 Subject: [PATCH 044/102] test with cse on/off --- .drone.yml | 14 +++++++------- pyodesys/native/_base.py | 3 +-- pyodesys/native/sources/odesys_anyode.hpp | 3 ++- pyodesys/native/sources/odesys_anyode_template.cpp | 1 + pyodesys/native/tests/test_cvode.py | 5 +++-- pyodesys/native/tests/test_odeint.py | 2 +- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.drone.yml b/.drone.yml index 8007ca72..76301943 100644 --- a/.drone.yml +++ b/.drone.yml @@ -17,19 +17,19 @@ pipeline: ttl: 90 # liftetime in days # get-pre-reqs: - # image: bjodah/bjodahimg20dev:21.7.c + # image: bjodah/bjodahimg20dev:21.8.a # commands: # - mkdir ./tmp # - git clone --recurse-submodules https://github.com/bjodah/pycvodes.git ./tmp/pycvodes # test-py2: - # image: bjodah/bjodahimg20dev:21.7.c + # image: bjodah/bjodahimg20dev:21.8.a # environment: # - CC=gcc-11 # - CXX=g++-11 # - OMP_NUM_THREADS=1 # - ANYODE_NUM_THREADS=2 - # - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.76.0/include + # - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.77.0/include # - LIBRARY_PATH=/usr/lib/x86_64-linux-gnu # - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu # - SUNDBASE=/opt/sundials-5.7.0-release @@ -39,13 +39,13 @@ pipeline: test-sund-5.7.0-rel: group: testing - image: bjodah/bjodahimg20dot:21.7.c + image: bjodah/bjodahimg20dot:21.8.a environment: - CC=gcc-11 - CXX=g++-11 - OMP_NUM_THREADS=1 - ANYODE_NUM_THREADS=2 - - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.76.0/include + - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.77.0/include - LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - SUNDBASE=/opt/sundials-5.7.0-release @@ -57,13 +57,13 @@ pipeline: test-sund-5.7.0-extended: group: testing - image: bjodah/bjodahimg20dot:21.7.c + image: bjodah/bjodahimg20dot:21.8.a environment: - CC=gcc-11 - CXX=g++-11 - OMP_NUM_THREADS=1 - ANYODE_NUM_THREADS=2 - - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.76.0/include + - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.77.0/include - LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - SUNDBASE=/opt/sundials-5.7.0-extended diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 030a250c..54cc086b 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -262,8 +262,7 @@ def _cses(k, assign_to=lambda i: _r("out[%d]" % i)): logger.info("Not using common subexpression elimination (disabled by PYODESYS_NATIVE_CSE)") n_common_cses=0 common_cses="" - - assigners = {k: _AssignerPlain(k, all_exprs) for k in all_exprsself} + assigners = {k: _AssignerPlain(k, all_exprs) for k in all_exprs} ns = dict( _message_for_rendered=[ diff --git a/pyodesys/native/sources/odesys_anyode.hpp b/pyodesys/native/sources/odesys_anyode.hpp index 08a5def1..521ca8e1 100644 --- a/pyodesys/native/sources/odesys_anyode.hpp +++ b/pyodesys/native/sources/odesys_anyode.hpp @@ -5,10 +5,11 @@ namespace odesys_anyode { template struct OdeSys : public AnyODE::OdeSysBase { std::vector m_p; - std::vector m_p_cse; + std::vector m_cse; std::vector m_atol; std::vector m_upper_bounds; std::vector m_lower_bounds; + std::vector m_invar; std::vector m_invar0; Real_t m_rtol; Real_t m_get_dx_max_factor; diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 412ae02f..545bccbd 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -123,6 +123,7 @@ namespace odesys_anyode { m_invar0.resize(${p_invariants["n_invar"]}); m_invar0.resize(${p_invariants["n_invar"]}); //realtype * const out = m_invar0.data(); + ${p_invariants["cses"]} ${p_invariants["assign"].all(assign_to=lambda i: "m_invar0[%d]" % i)} } %endif diff --git a/pyodesys/native/tests/test_cvode.py b/pyodesys/native/tests/test_cvode.py index b208e92f..01d891ba 100644 --- a/pyodesys/native/tests/test_cvode.py +++ b/pyodesys/native/tests/test_cvode.py @@ -88,8 +88,9 @@ def test_chained_multi_native_nonnegative(): @requires('pycvodes') -def test_Decay_nonnegative(): - _test_Decay_nonnegative(NativeSys) +@pytest.mark.parametrize('use_cse', [False, True]) +def test_Decay_nonnegative(use_cse): + _test_Decay_nonnegative(NativeSys, use_cse=use_cse) @requires('pycvodes') diff --git a/pyodesys/native/tests/test_odeint.py b/pyodesys/native/tests/test_odeint.py index a75a5412..ed717c6b 100644 --- a/pyodesys/native/tests/test_odeint.py +++ b/pyodesys/native/tests/test_odeint.py @@ -64,7 +64,7 @@ def test_PartiallySolved_symmetric_native_multi(): @requires('pyodeint') @pytest.mark.parametrize('use_cse', [False, True]) -def test_Decay_nonnegative(): +def test_Decay_nonnegative(use_cse): _test_Decay_nonnegative(NativeSys, use_cse=use_cse) From e657c1dfa42a47210b14ab78ef53213dd596a061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 1 Sep 2021 15:27:27 +0200 Subject: [PATCH 045/102] fixes in native._base --- pyodesys/native/_base.py | 17 ++++++------ .../native/sources/odesys_anyode_template.cpp | 2 +- pyodesys/native/symcse/compensated.py | 7 +---- pyodesys/native/symcse/util.py | 2 +- pyodesys/native/tests/_tests.py | 27 ++++++++++++------- pyodesys/native/tests/test_cvode.py | 7 ++--- pyodesys/native/tests/test_gsl.py | 7 ++--- pyodesys/native/tests/test_odeint.py | 7 ++--- 8 files changed, 42 insertions(+), 34 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 54cc086b..52c06823 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import (absolute_import, division, print_function) +from collections import defaultdict from datetime import datetime as dt from functools import reduce import logging @@ -88,18 +89,18 @@ def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): class _AssignerPlain(_AssignerBase): - def __init__(self, k, all_exprs): + def __init__(self, k, all_exprs, *, subsd): self.k = k - self.all_exrs = all_exprs + self.all_exprs = {k: [e.xreplace(subsd) for e in v] for k, v in all_exprs.items()} self.n = len(all_exprs[k]) def expr_is_zero(self, i): - return self.all_exprs[i] == 0 + return self.all_exprs[self.k][i] == 0 def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): - return self.odesys.be.ccode( + return sympy.ccode( Assignment(_r(assign_to(i)), self.all_exprs[self.k][i]) - ) + ';' + ) class _NativeCodeBase(Cpp_Code): @@ -261,8 +262,9 @@ def _cses(k, assign_to=lambda i: _r("out[%d]" % i)): else: logger.info("Not using common subexpression elimination (disabled by PYODESYS_NATIVE_CSE)") n_common_cses=0 + cses = defaultdict(lambda: "// use_cse==False") common_cses="" - assigners = {k: _AssignerPlain(k, all_exprs) for k in all_exprs} + assigners = {k: _AssignerPlain(k, all_exprs, subsd=subsd) for k in all_exprs} ns = dict( _message_for_rendered=[ @@ -327,13 +329,12 @@ class _NativeSysBase(SymbolicSys): def __init__(self, *args, native_code_kw=None, **kwargs): namespace_override = kwargs.pop('namespace_override', {}) namespace_extend = kwargs.pop('namespace_extend', {}) - save_temp = kwargs.pop('save_temp', False) if 'init_indep' not in kwargs: # we need to trigger append_iv for when invariants are used kwargs['init_indep'] = True kwargs['init_dep'] = True super(_NativeSysBase, self).__init__(*args, **kwargs) self._native = self._NativeCode( - self, save_temp=save_temp, + self, namespace_override=namespace_override, namespace_extend=namespace_extend, **(native_code_kw or {})) diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 545bccbd..03d162c3 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -275,7 +275,7 @@ namespace odesys_anyode { %endfor if (dfdt){ %for idx in range(p_odesys.ny**2, p_jac_dense["assign"].n): - ${p_jac_dense["assign"](i, assign_to=lambda _: "dfdt[%d]" % (idx - p_odesys.ny**2))} + ${p_jac_dense["assign"](idx, assign_to=lambda _: "dfdt[%d]" % (idx - p_odesys.ny**2))} %endfor } this->njev++; diff --git a/pyodesys/native/symcse/compensated.py b/pyodesys/native/symcse/compensated.py index 4204ad8e..0db0f6b6 100644 --- a/pyodesys/native/symcse/compensated.py +++ b/pyodesys/native/symcse/compensated.py @@ -217,12 +217,7 @@ def _pass_50_to_stmnts(self, lhs, rhs, *, statements): for neu in map(self.created.get, postorder_traversal(rhs)): if neu is None: continue - for t_neu in map(self.created.get, postorder_traversal(neu.terms)): - if t_neu is None: - continue - if t_neu not in self.expanded: - statements.extend(t_neu.to_statements( - self.created, self.expanded, self.do_swap)) + self._pass_50_to_stmnts(lhs, neu.terms, statements=statements) if neu not in self.expanded: statements.extend(neu.to_statements( self.created, self.expanded, self.do_swap)) diff --git a/pyodesys/native/symcse/util.py b/pyodesys/native/symcse/util.py index b14b90d6..23faf74d 100644 --- a/pyodesys/native/symcse/util.py +++ b/pyodesys/native/symcse/util.py @@ -120,7 +120,7 @@ def is_only_ig(e): candidates.append(prod(good)) if candidates: part = sorted(candidates, key=sympy.count_ops)[-1] - if part.count_ops() == 0: + if part.count_ops() == 0 or len(part.free_symbols) == 0: reintro[lhs] = rem else: for k, v in keep.items(): diff --git a/pyodesys/native/tests/_tests.py b/pyodesys/native/tests/_tests.py index a53417ee..f4c21517 100644 --- a/pyodesys/native/tests/_tests.py +++ b/pyodesys/native/tests/_tests.py @@ -18,7 +18,9 @@ def _test_NativeSys(NativeSys, **kwargs): - native = NativeSys.from_callback(vdp_f, 2, 1) + native = NativeSys.from_callback( + vdp_f, 2, 1#, native_code_kw=dict(save_temp=True) + ) assert native.ny == 2 assert len(native.params) == 1 xout, yout, info = native.integrate([0, 1, 2], [1, 0], params=[2.0], **kwargs) @@ -95,10 +97,10 @@ class TransformedNativeSys(TransformedSys, NativeSys): assert np.allclose(yout, ref, rtol=rtol*forgive, atol=atol*forgive) -def _test_Decay_nonnegative(NativeSys, use_cse): +def _test_Decay_nonnegative(NativeSys, use_cse, compensated): odesys = NativeSys.from_other( _get_decay3(lower_bounds=[0]*3), - native_code_kw=dict(compensated_summation=compensated), + native_code_kw=dict(use_cse=use_cse, compensated_summation=compensated), ) y0, k = [3., 2., 1.], [3.5, 2.5, 0] xout, yout, info = odesys.integrate([1e-10, 1], y0, k, integrator='native') @@ -245,10 +247,15 @@ def _test_NativeSys__first_step_cb(NativeSys, forgive=20): def _test_NativeSys__first_step_cb_source_code(NativeSys, log10myconst, should_succeed, forgive=20, **kwargs): dec3 = _get_decay3() - odesys = NativeSys.from_other(dec3, namespace_override={ - 'p_first_step': 'return good_const()*y[0];', - 'p_anon': 'double good_const(){ return std::pow(10, %.5g); }' % log10myconst - }, namespace_extend={'p_includes': ['']}) + odesys = NativeSys.from_other( + dec3, + namespace_override={ + 'p_first_step': 'AnyODE::ignore(x); return good_const()*y[0];', + 'p_anon': 'double good_const(){ return std::pow(10, %.5g); }' % log10myconst + }, + namespace_extend={'p_includes': ['']}, + native_code_kw=dict(save_temp=True), + ) y0, k = [.7, 0, 0], [1e23, 2, 3.] xout, yout, info = odesys.integrate(5, y0, k, integrator='native', **kwargs) ref = np.array(bateman_full(y0, k, xout - xout[0], exp=np.exp)).T @@ -424,8 +431,10 @@ def _solve(odesys, **kwargs): native = NativeSys.from_other( symbolic, - native_code_kw=dict(compensated_summation=compensated), - #save_temp=True, + native_code_kw=dict( + #save_temp=True, + compensated_summation=compensated + ), ) # regression test: sol = _solve(native, **kw) assert sol.info['success'] diff --git a/pyodesys/native/tests/test_cvode.py b/pyodesys/native/tests/test_cvode.py index 01d891ba..955b3a62 100644 --- a/pyodesys/native/tests/test_cvode.py +++ b/pyodesys/native/tests/test_cvode.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import (absolute_import, division, print_function) +from itertools import product import numpy as np import pytest @@ -88,9 +89,9 @@ def test_chained_multi_native_nonnegative(): @requires('pycvodes') -@pytest.mark.parametrize('use_cse', [False, True]) -def test_Decay_nonnegative(use_cse): - _test_Decay_nonnegative(NativeSys, use_cse=use_cse) +@pytest.mark.parametrize('use_cse,compensated', product([False, True], [False, True])) +def test_Decay_nonnegative(use_cse, compensated): + _test_Decay_nonnegative(NativeSys, use_cse=use_cse, compensated=compensated) @requires('pycvodes') diff --git a/pyodesys/native/tests/test_gsl.py b/pyodesys/native/tests/test_gsl.py index dd1f5cc7..ac239728 100644 --- a/pyodesys/native/tests/test_gsl.py +++ b/pyodesys/native/tests/test_gsl.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import (absolute_import, division, print_function) +from itertools import product import pytest from pyodesys.util import requires @@ -74,9 +75,9 @@ def test_chained_multi_native(reduced): @requires('pygslodeiv2') -@pytest.mark.parametrize('use_cse', [False, True]) -def test_Decay_nonnegative(use_cse): - _test_Decay_nonnegative(NativeSys, use_cse=use_cse) +@pytest.mark.parametrize('use_cse,compensated', product([False, True], [False, True])) +def test_Decay_nonnegative(use_cse, compensated): + _test_Decay_nonnegative(NativeSys, use_cse=use_cse, compensated=compensated) @requires('pygslodeiv2') diff --git a/pyodesys/native/tests/test_odeint.py b/pyodesys/native/tests/test_odeint.py index ed717c6b..050dfb78 100644 --- a/pyodesys/native/tests/test_odeint.py +++ b/pyodesys/native/tests/test_odeint.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import (absolute_import, division, print_function) +from itertools import product from pyodesys.util import requires import pytest @@ -63,9 +64,9 @@ def test_PartiallySolved_symmetric_native_multi(): @requires('pyodeint') -@pytest.mark.parametrize('use_cse', [False, True]) -def test_Decay_nonnegative(use_cse): - _test_Decay_nonnegative(NativeSys, use_cse=use_cse) +@pytest.mark.parametrize('use_cse,compensated', product([False, True], [False, True])) +def test_Decay_nonnegative(use_cse, compensated): + _test_Decay_nonnegative(NativeSys, use_cse=use_cse, compensated=compensated) @requires('pyodeint') From 8c4cd3513d320c44f6d0209d9ab639cbd8193494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Thu, 2 Sep 2021 15:59:41 +0200 Subject: [PATCH 046/102] refactoring --- pyodesys/native/_base.py | 31 +++++++++++++++---- .../native/sources/odesys_anyode_template.cpp | 4 +-- pyodesys/native/symcse/groupwise.py | 12 ++++--- pyodesys/native/symcse/util.py | 9 +++++- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 52c06823..97e0796c 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -102,6 +102,20 @@ def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): Assignment(_r(assign_to(i)), self.all_exprs[self.k][i]) ) +def _init_use_cse(use_cse): + if use_cse is None: + use_cse = os.getenv('PYODESYS_NATIVE_CSE', '1') == '1' + if not use_cse: + logger.info("Not using common subexpression elimination (disabled by PYODESYS_NATIVE_CSE)") + if use_cse is False: + use_cse = {} + else: + if isinstance(use_cse, dict): + if 'pre_process' not in use_cse: + use_cse['pre_process'] = None + else: + use_cse = dict(pre_process=None) + return use_cse class _NativeCodeBase(Cpp_Code): """Base class for generated code. @@ -140,9 +154,8 @@ def __init__(self, odesys, *args, use_cse=None, **kwargs): raise ModuleNotFoundError("failed to import compile_sources from pycompilation") if odesys.nroots > 0 and not self._support_roots: raise ValueError("%s does not support nroots > 0" % self.__class__.__name__) - if use_cse is None: - use_cse = os.getenv('PYODESYS_NATIVE_CSE', '1') == '1' - self.use_cse = use_cse + self.use_cse = _init_use_cse(use_cse) + self.namespace_override = kwargs.pop('namespace_override', {}) self.namespace_extend = kwargs.pop('namespace_extend', {}) self.tempdir_basename = '_pycodeexport_pyodesys_%s' % self.__class__.__name__ @@ -237,8 +250,13 @@ def variables(self): if self.use_cse: if self.compensated_summation: from .symcse.compensated import _NeumaierTransformer as Transformer + if isinstance(self.compensated_summation, dict): + transformer_kw = self.compensated_summation + else: + transformer_kw = None else: from .symcse.core import NullTransformer as Transformer + transformer_kw = None ignore = (() if self.odesys.indep is None else (self.odesys.indep,)) + self.odesys.dep + v gw = GroupwiseCSE( all_exprs, @@ -246,7 +264,8 @@ def variables(self): common_ignore=ignore, subsd=subsd, Transformer=Transformer, - pre_process=None + transformer_kw=transformer_kw, + **self.use_cse ) def not_arr(s): @@ -260,7 +279,6 @@ def _cses(k, assign_to=lambda i: _r("out[%d]" % i)): assigners = {k: _AssignerGW(k, gw) for k in gw.keys} else: - logger.info("Not using common subexpression elimination (disabled by PYODESYS_NATIVE_CSE)") n_common_cses=0 cses = defaultdict(lambda: "// use_cse==False") common_cses="" @@ -310,7 +328,8 @@ def _cses(k, assign_to=lambda i: _r("out[%d]" % i)): }, p_nroots=self.odesys.nroots, p_constructor=[], - p_get_dx_max=False + p_get_dx_max=False, + p_info_comment_codegen=f"{self.use_cse=}, {self.compensated_summation=}" ) ns.update(self.namespace_default) ns.update(self.namespace) diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 03d162c3..329dc5d8 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -9,6 +9,7 @@ import sympy // User provided system description: ${p_odesys.description} // Names of dependent variables: ${p_odesys.names} // Names of parameters: ${p_odesys.param_names} +// Code-generation details: ${p_info_comment_codegen} #include #include @@ -121,7 +122,6 @@ namespace odesys_anyode { ${"" if p_odesys.append_iv else 'throw std::runtime_error("append_iv not set to True")'} const realtype * const y = params + ${len(p_odesys.params)}; m_invar0.resize(${p_invariants["n_invar"]}); - m_invar0.resize(${p_invariants["n_invar"]}); //realtype * const out = m_invar0.data(); ${p_invariants["cses"]} ${p_invariants["assign"].all(assign_to=lambda i: "m_invar0[%d]" % i)} @@ -191,7 +191,7 @@ namespace odesys_anyode { if (std::abs(m_invar[idx] - m_invar0[idx]) > ((m_max_invariant_violation > 0) ? m_max_invariant_violation : std::abs(m_max_invariant_violation*m_invar0[idx]) /*- m_max_invariant_violation*/)) { - std::cerr << "Invariant (" << idx << ") violation at x=" << x << "\n"; + std::clog << "Invariant (" << idx << ") violation at x=" << x << "\n"; return AnyODE::Status::recoverable_error; } } diff --git a/pyodesys/native/symcse/groupwise.py b/pyodesys/native/symcse/groupwise.py index 55ed53f4..194cee5d 100644 --- a/pyodesys/native/symcse/groupwise.py +++ b/pyodesys/native/symcse/groupwise.py @@ -38,6 +38,7 @@ def __init__(self, groups, *, pre_process=pre_process, post_process=post_process, backend=None, + transformer_kw=None ): """ Parameters @@ -48,6 +49,8 @@ def __init__(self, groups, *, """ if code_printer is None: code_printer = CPrinter() + if transformer_kw is None: + transformer_kw = {} self._code_printer = code_printer self._subsd = {sympy.Symbol(k.name, real=True): v for k, v in (subsd or {}).items()} self._type = type_ @@ -71,14 +74,15 @@ def __init__(self, groups, *, _all_exprs, ignore=common_ignore, symbols=numbered_symbols('cse_comm_locl', real=True) ) - self._comm_tformr = Transformer(repls, reds, ignore=common_ignore) + self._comm_tformr = Transformer(repls, reds, ignore=common_ignore, **transformer_kw) remap = self._comm_tformr.remapping_for_arrayification(template=common_cse_template) self._comm_tformr.apply_remapping(remap) self.n_remapped = len(remap) assert(len(self._comm_tformr.final_exprs) == len(reds)) del reds - self._per_g_tformrs = self._get_g_tformrs(self._comm_tformr, Transformer=Transformer) + self._per_g_tformrs = self._get_g_tformrs( + self._comm_tformr, Transformer=Transformer, transformer_kw=transformer_kw) @property @@ -106,13 +110,13 @@ def _common_cse(self, all_exprs, **kwargs): [r.xreplace(comm_subs) for r in reds] ) - def _get_g_tformrs(self, comm_tformr, Transformer): + def _get_g_tformrs(self, comm_tformr, *, Transformer, transformer_kw): per_g = {} for i, k in enumerate(self._keys): g_repls, g_exprs = self.backend.cse( comm_tformr.final_exprs[slice(*self._spans[i:i+2])], symbols=numbered_symbols("cse", real=True)) - g_tformr = Transformer(g_repls, g_exprs, parent=comm_tformr) + g_tformr = Transformer(g_repls, g_exprs, parent=comm_tformr, **transformer_kw) per_g[k] = g_tformr return per_g diff --git a/pyodesys/native/symcse/util.py b/pyodesys/native/symcse/util.py index 23faf74d..3f116b1b 100644 --- a/pyodesys/native/symcse/util.py +++ b/pyodesys/native/symcse/util.py @@ -134,6 +134,8 @@ def is_only_ig(e): reintro[lhs] = rem repl = list(keep.items()) red = [e.xreplace(reintro) for e in red] + recreated = [e.subs(reversed(repl)) for e in red] + assert recreated == exprs if symbols is not None: remap = {} updated = [] @@ -164,7 +166,7 @@ def __init__(self, use_symengine=None, kw_cse=None, assume_real=True): elif _req_backend == "sympy": use_symengine = False elif _req_backend == "": - use_symengine = se is not None + use_symengine = False # se is not None else: raise ValueError("Unknown SYMCXSE_BACKEND: %s" % _req_backend) if use_symengine and se is None: @@ -249,6 +251,11 @@ def lambdify(self, args, exprs): else: return sympy.lambdify(args, exprs) +class BackendWithDisabledCSE(Backend): + + def cse(self, exprs, **kwargs): + return [], exprs + def ccode(arg, **kwargs): p = CPrinter(settings=(kwargs or dict(math_macros={}))) From 253e6048b890be8fc42d50ac7cc3de19bd376912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Thu, 2 Sep 2021 23:56:50 +0200 Subject: [PATCH 047/102] comparison of all GroupwiseCSE configs --- pyodesys/native/_base.py | 106 ++++++++------------------ pyodesys/native/symcse/compensated.py | 9 ++- pyodesys/native/symcse/groupwise.py | 22 ++++-- pyodesys/native/symcse/util.py | 6 +- 4 files changed, 59 insertions(+), 84 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 97e0796c..be679e14 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -63,9 +63,6 @@ def get_compile_kwargs(): _obj_suffix = '.o' # os.path.splitext(_ext_suffix)[0] + '.o' # '.obj' -class _AssignerBase: - def all(self, **kwargs): - return "\n".join(self(i, **kwargs) for i in range(self.n)) def _r(s): @@ -75,12 +72,15 @@ def _r(s): return sympy.Symbol(s, real=True) -class _AssignerGW(_AssignerBase): +class _AssignerGW: def __init__(self, k, gw): self.k = k self.gw = gw self.n = len(gw.exprs(k)) + def all(self, **kwargs): + return "\n".join(self(i, **kwargs) for i in range(self.n)) + def expr_is_zero(self, i): return self.gw.exprs(self.k)[i] == 0 @@ -88,35 +88,6 @@ def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): return self.gw.render(Assignment(_r(assign_to(i)), self.gw.exprs(self.k)[i])) -class _AssignerPlain(_AssignerBase): - def __init__(self, k, all_exprs, *, subsd): - self.k = k - self.all_exprs = {k: [e.xreplace(subsd) for e in v] for k, v in all_exprs.items()} - self.n = len(all_exprs[k]) - - def expr_is_zero(self, i): - return self.all_exprs[self.k][i] == 0 - - def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): - return sympy.ccode( - Assignment(_r(assign_to(i)), self.all_exprs[self.k][i]) - ) - -def _init_use_cse(use_cse): - if use_cse is None: - use_cse = os.getenv('PYODESYS_NATIVE_CSE', '1') == '1' - if not use_cse: - logger.info("Not using common subexpression elimination (disabled by PYODESYS_NATIVE_CSE)") - if use_cse is False: - use_cse = {} - else: - if isinstance(use_cse, dict): - if 'pre_process' not in use_cse: - use_cse['pre_process'] = None - else: - use_cse = dict(pre_process=None) - return use_cse - class _NativeCodeBase(Cpp_Code): """Base class for generated code. @@ -147,18 +118,21 @@ class _NativeCodeBase(Cpp_Code): # `namespace_override` is set in init # `namespace_extend` is set in init - def __init__(self, odesys, *args, use_cse=None, **kwargs): + def __init__(self, odesys, *args, groupwise_kw=None, **kwargs): + self.groupwise_kw = groupwise_kw if Cpp_Code is object: raise ModuleNotFoundError("failed to import Cpp_Code from pycodeexport") if compile_sources is None: raise ModuleNotFoundError("failed to import compile_sources from pycompilation") if odesys.nroots > 0 and not self._support_roots: raise ValueError("%s does not support nroots > 0" % self.__class__.__name__) - self.use_cse = _init_use_cse(use_cse) self.namespace_override = kwargs.pop('namespace_override', {}) self.namespace_extend = kwargs.pop('namespace_extend', {}) - self.tempdir_basename = '_pycodeexport_pyodesys_%s' % self.__class__.__name__ + self.tempdir_basename = '_pycodeexport_pyodesys_%s_%s' % ( + self.__class__.__name__, + ''.join(filter(lambda x: str.isalnum(x) or x in '_-', str(odesys.description).replace(' ', '_'))) + ) self.obj_files = self.obj_files + ('%s%s' % (self.wrapper_name, _obj_suffix),) self.so_file = '%s%s' % (self.wrapper_name, '.so') _wrapper_src = pkg_resources.resource_filename( @@ -189,7 +163,7 @@ def __init__(self, odesys, *args, use_cse=None, **kwargs): shutil.rmtree(tmpdir) if not os.path.exists(_dest): raise OSError("Failed to place prebuilt file at: %s" % _dest) - self.compensated_summation = kwargs.pop("compensated_summation", os.environ.get("PYODESYS_COMPENSATED_SUMMATION", "0") == "1") + #self.compensated_summation = kwargs.pop("compensated_summation", os.environ.get("PYODESYS_COMPENSATED_SUMMATION", "0") == "1") super().__init__(*args, logger=logger, **kwargs) # def _ccode(self, expr, subsd): @@ -247,42 +221,26 @@ def variables(self): subsd.update({k: self.odesys.be.Symbol('v[%d]' % idx) for idx, k in enumerate(v)}) - if self.use_cse: - if self.compensated_summation: - from .symcse.compensated import _NeumaierTransformer as Transformer - if isinstance(self.compensated_summation, dict): - transformer_kw = self.compensated_summation - else: - transformer_kw = None - else: - from .symcse.core import NullTransformer as Transformer - transformer_kw = None - ignore = (() if self.odesys.indep is None else (self.odesys.indep,)) + self.odesys.dep + v - gw = GroupwiseCSE( - all_exprs, - common_cse_template="m_cse[{}]", - common_ignore=ignore, - subsd=subsd, - Transformer=Transformer, - transformer_kw=transformer_kw, - **self.use_cse - ) - - def not_arr(s): - return '[' not in s.name - - def _cses(k, assign_to=lambda i: _r("out[%d]" % i)): - return CodeBlock(*gw.statements(k, declare=not_arr)) - cses = {k: gw.render(_cses(k)) for k in gw.keys} - n_common_cses = gw.n_remapped - common_cses = gw.render(CodeBlock(*gw.common_statements(declare=not_arr))) - - assigners = {k: _AssignerGW(k, gw) for k in gw.keys} - else: - n_common_cses=0 - cses = defaultdict(lambda: "// use_cse==False") - common_cses="" - assigners = {k: _AssignerPlain(k, all_exprs, subsd=subsd) for k in all_exprs} + ignore = (() if self.odesys.indep is None else (self.odesys.indep,)) + self.odesys.dep + v + gw = GroupwiseCSE( + all_exprs, + common_cse_template="m_cse[{}]", + common_ignore=ignore, + subsd=subsd, + # Transformer=Transformer, + # transformer_kw=transformer_kw, + **self.groupwise_kw #use_cse + ) + + def not_arr(s): + return '[' not in s.name + + def _cses(k, assign_to=lambda i: _r("out[%d]" % i)): + return CodeBlock(*gw.statements(k, declare=not_arr)) + cses = {k: gw.render(_cses(k)) for k in gw.keys} + n_common_cses = gw.n_remapped + common_cses = gw.render(CodeBlock(*gw.common_statements(declare=not_arr))) + assigners = {k: _AssignerGW(k, gw) for k in gw.keys} ns = dict( _message_for_rendered=[ @@ -329,7 +287,7 @@ def _cses(k, assign_to=lambda i: _r("out[%d]" % i)): p_nroots=self.odesys.nroots, p_constructor=[], p_get_dx_max=False, - p_info_comment_codegen=f"{self.use_cse=}, {self.compensated_summation=}" + p_info_comment_codegen=f"{self.groupwise_kw=}" ) ns.update(self.namespace_default) ns.update(self.namespace) diff --git a/pyodesys/native/symcse/compensated.py b/pyodesys/native/symcse/compensated.py index 0db0f6b6..e5883401 100644 --- a/pyodesys/native/symcse/compensated.py +++ b/pyodesys/native/symcse/compensated.py @@ -36,7 +36,7 @@ def _ccode(self, printer): terms = ", ".join(map(printer._print, self.terms)) return f"NA({terms} /*{str(self.accum)[:-1]}*/)" - def to_statements(self, existing, expanded, do_swap=False): + def to_statements(self, existing, expanded, *, transients, do_swap=False): """Transform into statements.""" neum, ordinary = [], [] for term in self.terms: @@ -53,6 +53,10 @@ def to_statements(self, existing, expanded, do_swap=False): st.append(Assignment(self.carry, 0)) for elem in ordinary: + if elem.count_ops(): + tr = next(transients) + st.append(Assignment(tr, elem)) + elem = tr st.extend(_NeumaierAdd._impl_add(self.accum, self.carry, elem, self.temp, do_swap)) expanded.add(self) return st @@ -220,7 +224,8 @@ def _pass_50_to_stmnts(self, lhs, rhs, *, statements): self._pass_50_to_stmnts(lhs, neu.terms, statements=statements) if neu not in self.expanded: statements.extend(neu.to_statements( - self.created, self.expanded, self.do_swap)) + self.created, self.expanded, do_swap=self.do_swap, + transients=self._tmp_var)) return rhs def _pass_60_xrepl(self, lhs, rhs, *, statements): diff --git a/pyodesys/native/symcse/groupwise.py b/pyodesys/native/symcse/groupwise.py index 194cee5d..7997dcb3 100644 --- a/pyodesys/native/symcse/groupwise.py +++ b/pyodesys/native/symcse/groupwise.py @@ -16,8 +16,10 @@ def pre_process(expr): - """Simplify, expand & factor.""" - return expr.simplify(rational=True).expand().factor() + """Simplify, expand & factor. + + Example: expr.simplify(rational=True).expand().factor() """ + return expr.factor() def post_process(expr): @@ -35,8 +37,8 @@ def __init__(self, groups, *, subsd=None, Transformer=NullTransformer, type_=float64, - pre_process=pre_process, - post_process=post_process, + pre_process=None, + post_process=None, backend=None, transformer_kw=None ): @@ -74,6 +76,9 @@ def __init__(self, groups, *, _all_exprs, ignore=common_ignore, symbols=numbered_symbols('cse_comm_locl', real=True) ) + if post_process: + repls = [(s, post_process(e)) for s, e in repls] + reds = [post_process(e) for e in reds] self._comm_tformr = Transformer(repls, reds, ignore=common_ignore, **transformer_kw) remap = self._comm_tformr.remapping_for_arrayification(template=common_cse_template) self._comm_tformr.apply_remapping(remap) @@ -82,7 +87,9 @@ def __init__(self, groups, *, assert(len(self._comm_tformr.final_exprs) == len(reds)) del reds self._per_g_tformrs = self._get_g_tformrs( - self._comm_tformr, Transformer=Transformer, transformer_kw=transformer_kw) + self._comm_tformr, Transformer=Transformer, + transformer_kw=transformer_kw, + post_process=post_process) @property @@ -110,12 +117,15 @@ def _common_cse(self, all_exprs, **kwargs): [r.xreplace(comm_subs) for r in reds] ) - def _get_g_tformrs(self, comm_tformr, *, Transformer, transformer_kw): + def _get_g_tformrs(self, comm_tformr, *, Transformer, transformer_kw, post_process): per_g = {} for i, k in enumerate(self._keys): g_repls, g_exprs = self.backend.cse( comm_tformr.final_exprs[slice(*self._spans[i:i+2])], symbols=numbered_symbols("cse", real=True)) + if post_process: + g_repls = [(s, post_process(e)) for s, e in g_repls] + g_exprs = [post_process(e) for e in g_exprs] g_tformr = Transformer(g_repls, g_exprs, parent=comm_tformr, **transformer_kw) per_g[k] = g_tformr diff --git a/pyodesys/native/symcse/util.py b/pyodesys/native/symcse/util.py index 3f116b1b..bbdefbd5 100644 --- a/pyodesys/native/symcse/util.py +++ b/pyodesys/native/symcse/util.py @@ -134,8 +134,10 @@ def is_only_ig(e): reintro[lhs] = rem repl = list(keep.items()) red = [e.xreplace(reintro) for e in red] - recreated = [e.subs(reversed(repl)) for e in red] - assert recreated == exprs + # recreated = [e.subs(reversed(repl)) for e in red] + # for e1, e2 in zip(recreated, exprs): + # assert (e1-e2).simplify() == 0 + if symbols is not None: remap = {} updated = [] From bd104a6100e281739e6f0475d5b33ea4667e1f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 7 Sep 2021 14:36:30 +0200 Subject: [PATCH 048/102] added summation_cxx --- pyodesys/native/_base.py | 26 +++--- pyodesys/native/cvode.py | 2 +- .../sources/summation_cxx/accumulator.hpp | 71 ++++++++++++++++ .../sources/summation_cxx/compensated.hpp | 54 +++++++++++++ .../native/sources/summation_cxx/macros.hpp | 81 +++++++++++++++++++ pyodesys/native/symcse/compensated.py | 48 +++++++++-- pyodesys/native/symcse/groupwise.py | 14 ++-- pyodesys/native/symcse/util.py | 18 ++++- 8 files changed, 287 insertions(+), 27 deletions(-) create mode 100644 pyodesys/native/sources/summation_cxx/accumulator.hpp create mode 100644 pyodesys/native/sources/summation_cxx/compensated.hpp create mode 100644 pyodesys/native/sources/summation_cxx/macros.hpp diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index be679e14..c28c652f 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -52,8 +52,15 @@ } -def get_compile_kwargs(): +def get_compile_kwargs(kwargs): kw = copy.deepcopy(_compile_kwargs) + for k in "define include_dirs libraries flags ldflags".split(): + if k in kwargs: + if k in kw: + kw[k].extend(kwargs.pop(k)) + else: + kw[k] = kwargs.pop(k) + if options := os.environ.get("PYODESYS_OPTIONS"): kw['options'] = options.split(',') return kw @@ -73,10 +80,11 @@ def _r(s): class _AssignerGW: - def __init__(self, k, gw): + def __init__(self, k, gw, wrap_out=lambda x: x): self.k = k self.gw = gw self.n = len(gw.exprs(k)) + self.wrap_out = wrap_out def all(self, **kwargs): return "\n".join(self(i, **kwargs) for i in range(self.n)) @@ -85,7 +93,8 @@ def expr_is_zero(self, i): return self.gw.exprs(self.k)[i] == 0 def __call__(self, i, assign_to=lambda i: _r("out[%s]" % i)): - return self.gw.render(Assignment(_r(assign_to(i)), self.gw.exprs(self.k)[i])) + out = self.wrap_out(self.gw.exprs(self.k)[i]) + return self.gw.render(Assignment(_r(assign_to(i)), out)) class _NativeCodeBase(Cpp_Code): @@ -118,8 +127,9 @@ class _NativeCodeBase(Cpp_Code): # `namespace_override` is set in init # `namespace_extend` is set in init - def __init__(self, odesys, *args, groupwise_kw=None, **kwargs): + def __init__(self, odesys, *args, groupwise_kw=None, assigner_kw=None, **kwargs): self.groupwise_kw = groupwise_kw + self.assigner_kw = assigner_kw or {} if Cpp_Code is object: raise ModuleNotFoundError("failed to import Cpp_Code from pycodeexport") if compile_sources is None: @@ -235,12 +245,12 @@ def variables(self): def not_arr(s): return '[' not in s.name - def _cses(k, assign_to=lambda i: _r("out[%d]" % i)): + def _cses(k): return CodeBlock(*gw.statements(k, declare=not_arr)) cses = {k: gw.render(_cses(k)) for k in gw.keys} n_common_cses = gw.n_remapped common_cses = gw.render(CodeBlock(*gw.common_statements(declare=not_arr))) - assigners = {k: _AssignerGW(k, gw) for k in gw.keys} + assigners = {k: _AssignerGW(k, gw, **self.assigner_kw) for k in gw.keys} ns = dict( _message_for_rendered=[ @@ -304,16 +314,12 @@ class _NativeSysBase(SymbolicSys): _native_name = None def __init__(self, *args, native_code_kw=None, **kwargs): - namespace_override = kwargs.pop('namespace_override', {}) - namespace_extend = kwargs.pop('namespace_extend', {}) if 'init_indep' not in kwargs: # we need to trigger append_iv for when invariants are used kwargs['init_indep'] = True kwargs['init_dep'] = True super(_NativeSysBase, self).__init__(*args, **kwargs) self._native = self._NativeCode( self, - namespace_override=namespace_override, - namespace_extend=namespace_extend, **(native_code_kw or {})) def integrate(self, *args, **kwargs): diff --git a/pyodesys/native/cvode.py b/pyodesys/native/cvode.py index b40a192b..f15b9058 100644 --- a/pyodesys/native/cvode.py +++ b/pyodesys/native/cvode.py @@ -36,7 +36,7 @@ class NativeCvodeCode(_NativeCodeBase): _support_roots = True def __init__(self, *args, **kwargs): - self.compile_kwargs = get_compile_kwargs() + self.compile_kwargs = get_compile_kwargs(kwargs) self.compile_kwargs['define'] = ['PYCVODES_NO_KLU={}'.format("0" if config.get('KLU', True) else "1"), 'PYCVODES_NO_LAPACK={}'.format("0" if config.get('LAPACK', True) else "1"), 'ANYODE_NO_LAPACK={}'.format("0" if config.get('LAPACK', True) else "1")] diff --git a/pyodesys/native/sources/summation_cxx/accumulator.hpp b/pyodesys/native/sources/summation_cxx/accumulator.hpp new file mode 100644 index 00000000..475fd1c8 --- /dev/null +++ b/pyodesys/native/sources/summation_cxx/accumulator.hpp @@ -0,0 +1,71 @@ +#pragma once +#include "summation_cxx/compensated.hpp" +#include +#include +#include +#include + +namespace summation_cxx { + +template +struct Accumulator : public detail::Operators> { + static constexpr Compensation compensation_scheme { scheme }; + +private: + std::array data {}; + +public: + T& accum() { return data.data()[0]; } + T& carry() { return data.data()[1]; } + const T& accum() const { return data.data()[0]; } + const T& carry() const { return data.data()[1]; } + +public: + Accumulator() = default; + // cppcheck-suppress noExplicitConstructor + Accumulator(T accum) + { + data[0] = accum; + } + explicit Accumulator(T accum, T carry) + { + data[0] = accum; + data[1] = carry; + } + void clear() { data.clear(); } + + template + static constexpr T sum(const std::array& arr) + { + Accumulator ta {}; +#if defined(__clang__) +#pragma unroll 16 +#elif defined(__GNUC__) +#pragma GCC unroll 16 +#endif + for (const auto& e : arr) { + // cppcheck-suppress useStlAlgorithm + ta += e; + } + return ta.template to(); + } +}; +template +using AccumulatorKahan = Accumulator; +template +using AccumulatorNeumaier = Accumulator; +template +using AccumulatorNeumaierSwap = Accumulator; + +template +T pow(const Accumulator& base, T exponent) +{ + return std::pow(base.accum(), exponent); +} +template +T pow(const Accumulator& base, int exponent) +{ + return std::pow(base.accum(), static_cast(exponent)); +} + +} diff --git a/pyodesys/native/sources/summation_cxx/compensated.hpp b/pyodesys/native/sources/summation_cxx/compensated.hpp new file mode 100644 index 00000000..ee31159c --- /dev/null +++ b/pyodesys/native/sources/summation_cxx/compensated.hpp @@ -0,0 +1,54 @@ +#pragma once +#ifdef __FAST_MATH__ +#error fast math enabled (/fp:fast, -ffast-math), this would negate compensation. +#endif +#include "summation_cxx/macros.hpp" +#include // std::size_t + +namespace summation_cxx { +enum class Compensation { NONE, + KAHAN, + NEUMAIER, + NEUMAIER_SWAP }; + +namespace /* anonymous */ { + template + SMMTNCXX_PREFER_INLINE void accum_kahan_destructive( + T& SMMTNCXX_RESTRICT accu, + T& SMMTNCXX_RESTRICT carry, + T& SMMTNCXX_RESTRICT elem) + { + elem -= carry; + const T tmp = accu + elem; + carry = T { tmp - accu } - elem; + accu = tmp; + } + template + SMMTNCXX_PREFER_INLINE void accum_kahan( + T& SMMTNCXX_RESTRICT accu, + T& SMMTNCXX_RESTRICT carry, + const T& SMMTNCXX_RESTRICT elem) + { + T y = elem; + accum_kahan_destructive(accu, carry, y); + } + + template + SMMTNCXX_PREFER_INLINE void accum_neumaier( + T& SMMTNCXX_RESTRICT acm, + T& SMMTNCXX_RESTRICT carry, + const T& SMMTNCXX_RESTRICT elem) + { + SMMTNCXX_NEUMAIER_ADD(acm, carry, elem, T, tmp, false); + } + + template + SMMTNCXX_PREFER_INLINE void accum_neumaier_swap( + T& SMMTNCXX_RESTRICT acm, + T& SMMTNCXX_RESTRICT carry, + const T& SMMTNCXX_RESTRICT elem) + { + SMMTNCXX_NEUMAIER_ADD(acm, carry, elem, T, tmp, true); + } +} +} diff --git a/pyodesys/native/sources/summation_cxx/macros.hpp b/pyodesys/native/sources/summation_cxx/macros.hpp new file mode 100644 index 00000000..3fb9f9ea --- /dev/null +++ b/pyodesys/native/sources/summation_cxx/macros.hpp @@ -0,0 +1,81 @@ +#pragma once +#ifndef SMMTNCXX_RESTRICT +#if defined(__GNUC__) +#define SMMTNCXX_RESTRICT __restrict__ +#elif defined(_MSC_VER) && _MSC_VER >= 1400 +#define SMMTNCXX_RESTRICT __restrict +// #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L +// #define SMMTNCXX_RESTRICT restrict +#else +#define SMMTNCXX_RESTRICT +#endif +#endif + +#ifndef SMMTNCXX_PREFER_INLINE +#if defined(__GNUC__) +#define SMMTNCXX_PREFER_INLINE __attribute__((flatten)) +#elif defined(_MSC_VER) && _MSC_VER >= 1400 +#define SMMTNCXX_PREFER_INLINE __forceinline +#else +#define SMMTNCXX_PREFER_INLINE +#endif +#endif + +#if defined(__cpp_concepts) && __cpp_concepts >= 201907L +#define SMMTNCXX_FWD_IT_CONCEPT std::forward_iterator +#define SMMTNCXX_RND_IT_CONCEPT std::random_access_iterator +#else +#define SMMTNCXX_FWD_IT_CONCEPT typename +#define SMMTNCXX_RND_IT_CONCEPT typename +#endif + +// Math macros to support e.g. __float128 without std lib support: +#ifndef SMMTNCXX_ABS +#define SMMTNCXX_ABS(x) (((x) < 0) ? -(x) : (x)) +#endif + +#ifndef SMMTNCXX_NEUMAIER_BRANCH +// see test/bench.cpp +#define SMMTNCXX_NEUMAIER_BRANCH 1 +#endif + +#if defined(SMMTNCXX_NEUMAIER_SWAP) +#error "API has changed, update your compilation flags accordingly" +#endif + +#define SMMTNCXX_SWP_TMP_(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ + TYPE TMP; \ + if (DO_SWAP) { \ + if (CARRY == 0 && ACCUM != 0 && SMMTNCXX_ABS(ELEM) > SMMTNCXX_ABS(ACCUM)) { \ + TMP = ACCUM; \ + ACCUM = CARRY; \ + CARRY = TMP; \ + } \ + } \ + TMP = (ACCUM) + (ELEM); + +#if SMMTNCXX_NEUMAIER_BRANCH == 1 +#define SMMTNCXX_NEUMAIER_ADD(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ + do { \ + SMMTNCXX_SWP_TMP_(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ + if (SMMTNCXX_ABS(TMP) > SMMTNCXX_ABS(ELEM)) { \ + CARRY += TYPE { (ACCUM) - (TMP) } + (ELEM); \ + } else { \ + CARRY += TYPE { (ELEM) - (TMP) } + (ACCUM); \ + } \ + ACCUM = (TMP); \ + } while (0) +#else +#define SMMTNCXX_NEUMAIER_ADD(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ + do { \ + SMMTNCXX_SWP_TMP_(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ + T SMMTNCXX_dat_[2] = { \ + T { (ELEM) - (TMP) } + (ACCUM), \ + T { (ACCUM) - (TMP) } + (ELEM) \ + }; \ + CARRY += SMMTNCXX_dat_[SMMTNCXX_ABS(TMP) > SMMTNCXX_ABS(ELEM)]; \ + ACCUM = (TMP); \ + } while (0) +#endif +#undef SMMTNCXX_CXX_SWP_TMP_ +#define SMMTNCXX_NEUMAIER_FINALIZE(ACCUM, CARRY) ((ACCUM) + (CARRY)) diff --git a/pyodesys/native/symcse/compensated.py b/pyodesys/native/symcse/compensated.py index e5883401..c30bd329 100644 --- a/pyodesys/native/symcse/compensated.py +++ b/pyodesys/native/symcse/compensated.py @@ -8,9 +8,9 @@ """ from collections import defaultdict from functools import reduce -from operator import add +from operator import add, mul from sympy import ( - Abs, Add, And, Eq, Expr, Lt, Ne, numbered_symbols, Piecewise, + Abs, Add, And, Eq, Expr, Lt, Mul, Ne, numbered_symbols, Piecewise, postorder_traversal, Symbol, Tuple ) from sympy.codegen import Assignment, aug_assign, CodeBlock @@ -40,24 +40,56 @@ def to_statements(self, existing, expanded, *, transients, do_swap=False): """Transform into statements.""" neum, ordinary = [], [] for term in self.terms: - if term in existing: - neum.append(existing[term]) + ex, other = [], [] + for factor in term.as_ordered_factors(): + if factor in existing: + ex.append(factor) + else: + other.append(factor) + if len(ex) == 1: # TODO: handle >1 + neum.append((reduce(mul, other) if other else 1, existing[ex[0]])) else: ordinary.append(term) st = [] if neum: - st.append(Assignment(self.accum, sum(na.accum for na in neum))) - st.append(Assignment(self.carry, sum(na.carry for na in neum))) + if True: + st.append(Assignment(self.accum, neum[0][0]*neum[0][1].accum)) + ordinary.extend([oth*na.accum for oth, na in neum[1:]]) + else: + st.append(Assignment(self.accum, sum(oth*na.accum for oth, na in neum))) + + st.append(Assignment(self.carry, sum(oth*na.carry for oth, na in neum))) else: st.append(Assignment(self.accum, ordinary.pop(0))) st.append(Assignment(self.carry, 0)) for elem in ordinary: - if elem.count_ops(): + nops = elem.count_ops() + if nops == 1 and isinstance(elem, Mul): + nops = max(nops, (-elem).count_ops()) + if nops: + # for fs in elem.free_symbols: + # if fs not in existing: + # continue + # ex = existing[fs] + # if ex not in expanded: + # continue + # e = elem.subs(fs, ex.accum + ex.carry) + # _accu, _pure_carry = [], [] + # for term in e.as_ordered_terms(): + # if ex.carry in term and ex.accum not in term: + # _pure_carry.append(term) + # else: + # _accum.append(term) + # if _pure_carry: + # ... + # ea = elem.subs(fs, ex.accum) + # ec = elem.sbus(fs, ex.carry) tr = next(transients) st.append(Assignment(tr, elem)) elem = tr - st.extend(_NeumaierAdd._impl_add(self.accum, self.carry, elem, self.temp, do_swap)) + st.extend(_NeumaierAdd._impl_add( + self.accum, self.carry, elem, self.temp, do_swap)) expanded.add(self) return st diff --git a/pyodesys/native/symcse/groupwise.py b/pyodesys/native/symcse/groupwise.py index 7997dcb3..c5f35e86 100644 --- a/pyodesys/native/symcse/groupwise.py +++ b/pyodesys/native/symcse/groupwise.py @@ -54,7 +54,6 @@ def __init__(self, groups, *, if transformer_kw is None: transformer_kw = {} self._code_printer = code_printer - self._subsd = {sympy.Symbol(k.name, real=True): v for k, v in (subsd or {}).items()} self._type = type_ self._keys, _values = zip(*groups.items()) self._spans = np.cumsum([0]+list(map(len, _values))) @@ -82,6 +81,8 @@ def __init__(self, groups, *, self._comm_tformr = Transformer(repls, reds, ignore=common_ignore, **transformer_kw) remap = self._comm_tformr.remapping_for_arrayification(template=common_cse_template) self._comm_tformr.apply_remapping(remap) + _subsd = {sympy.Symbol(k.name, real=True): v for k, v in (subsd or {}).items()} + self._comm_tformr.apply_remapping(_subsd) self.n_remapped = len(remap) assert(len(self._comm_tformr.final_exprs) == len(reds)) @@ -89,7 +90,9 @@ def __init__(self, groups, *, self._per_g_tformrs = self._get_g_tformrs( self._comm_tformr, Transformer=Transformer, transformer_kw=transformer_kw, - post_process=post_process) + post_process=post_process, + subsd=_subsd + ) @property @@ -99,9 +102,7 @@ def keys(self): def render(self, x): """Generate a code string.""" - x2 = x.xreplace(self._subsd) - assert not any(x2.has(k) for k in self._subsd) - return self._code_printer.doprint(x.xreplace(self._subsd)) + return self._code_printer.doprint(x) def _common_cse(self, all_exprs, **kwargs): repls, reds = self.backend.cse(all_exprs, **kwargs) @@ -117,7 +118,7 @@ def _common_cse(self, all_exprs, **kwargs): [r.xreplace(comm_subs) for r in reds] ) - def _get_g_tformrs(self, comm_tformr, *, Transformer, transformer_kw, post_process): + def _get_g_tformrs(self, comm_tformr, *, Transformer, transformer_kw, post_process, subsd): per_g = {} for i, k in enumerate(self._keys): g_repls, g_exprs = self.backend.cse( @@ -127,6 +128,7 @@ def _get_g_tformrs(self, comm_tformr, *, Transformer, transformer_kw, post_proce g_repls = [(s, post_process(e)) for s, e in g_repls] g_exprs = [post_process(e) for e in g_exprs] g_tformr = Transformer(g_repls, g_exprs, parent=comm_tformr, **transformer_kw) + g_tformr.apply_remapping(subsd) per_g[k] = g_tformr return per_g diff --git a/pyodesys/native/symcse/util.py b/pyodesys/native/symcse/util.py index bbdefbd5..ee8afa28 100644 --- a/pyodesys/native/symcse/util.py +++ b/pyodesys/native/symcse/util.py @@ -5,6 +5,7 @@ from operator import mul, add import sympy +from sympy.codegen.ast import float80 from sympy.printing.c import C99CodePrinter from sympy.printing.cxx import CXX17CodePrinter from sympy.printing.pycode import PythonCodePrinter @@ -253,19 +254,32 @@ def lambdify(self, args, exprs): else: return sympy.lambdify(args, exprs) + class BackendWithDisabledCSE(Backend): def cse(self, exprs, **kwargs): return [], exprs +default_settings = dict( + math_macros={}, + type_mappings={float80: "long double"} +) + + def ccode(arg, **kwargs): - p = CPrinter(settings=(kwargs or dict(math_macros={}))) + for k, v in default_settings.items(): + if k not in kwargs: + kwargs[k] = v + p = CPrinter(settings=kwargs) return p.doprint(arg) def cxxcode(arg, **kwargs): - p = CXXPrinter(settings=(kwargs or dict(math_macros={}))) + for k, v in default_settings.items(): + if k not in kwargs: + kwargs[k] = v + p = CXXPrinter(settings=kwargs) return p.doprint(arg) From 87d3edbdf88970382faaabb870147b7b646120eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 14 Sep 2021 13:28:24 +0200 Subject: [PATCH 049/102] compensation: sxx --- pyodesys/native/_base.py | 18 +-- .../native/sources/odesys_anyode_template.cpp | 11 +- .../sources/summation_cxx/accumulator.hpp | 21 ++++ .../sources/summation_cxx/compensated.hpp | 1 + pyodesys/native/symcse/compensated.py | 109 ++++++++++++------ pyodesys/native/symcse/core.py | 29 +++-- pyodesys/native/symcse/groupwise.py | 97 ++++++++++------ pyodesys/native/symcse/util.py | 30 +++-- 8 files changed, 214 insertions(+), 102 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index c28c652f..e1327dec 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -13,7 +13,7 @@ import tempfile import sympy -from sympy.codegen.ast import CodeBlock, Assignment +from sympy.codegen.ast import CodeBlock, Assignment, float64 import numpy as np import pkg_resources @@ -127,8 +127,10 @@ class _NativeCodeBase(Cpp_Code): # `namespace_override` is set in init # `namespace_extend` is set in init - def __init__(self, odesys, *args, groupwise_kw=None, assigner_kw=None, **kwargs): + def __init__(self, odesys, *args, groupwise_kw=None, assigner_kw=None, + types=None, **kwargs): self.groupwise_kw = groupwise_kw + self.types = types self.assigner_kw = assigner_kw or {} if Cpp_Code is object: raise ModuleNotFoundError("failed to import Cpp_Code from pycodeexport") @@ -173,7 +175,8 @@ def __init__(self, odesys, *args, groupwise_kw=None, assigner_kw=None, **kwargs) shutil.rmtree(tmpdir) if not os.path.exists(_dest): raise OSError("Failed to place prebuilt file at: %s" % _dest) - #self.compensated_summation = kwargs.pop("compensated_summation", os.environ.get("PYODESYS_COMPENSATED_SUMMATION", "0") == "1") + self.compensated_summation = kwargs.pop("compensated_summation", os.environ.get( + "PYODESYS_COMPENSATED_SUMMATION", "0") == "1") super().__init__(*args, logger=logger, **kwargs) # def _ccode(self, expr, subsd): @@ -244,12 +247,12 @@ def variables(self): def not_arr(s): return '[' not in s.name - + types = self.types or defaultdict(lambda: (lambda lhs, rhs: (float64, rhs))) def _cses(k): - return CodeBlock(*gw.statements(k, declare=not_arr)) + return CodeBlock(*gw.statements(k, declare=not_arr, type_=types[k])) cses = {k: gw.render(_cses(k)) for k in gw.keys} n_common_cses = gw.n_remapped - common_cses = gw.render(CodeBlock(*gw.common_statements(declare=not_arr))) + common_cses = gw.render(CodeBlock(*gw.common_statements(declare=not_arr, type_=types[None]))) assigners = {k: _AssignerGW(k, gw, **self.assigner_kw) for k in gw.keys} ns = dict( @@ -297,7 +300,8 @@ def _cses(k): p_nroots=self.odesys.nroots, p_constructor=[], p_get_dx_max=False, - p_info_comment_codegen=f"{self.groupwise_kw=}" + p_info_comment_codegen=f"{self.groupwise_kw=}", + p_compensated_summation=self.compensated_summation ) ns.update(self.namespace_default) ns.update(self.namespace) diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 329dc5d8..460c6667 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -20,6 +20,10 @@ import sympy #include ${inc} %endfor +%if p_compensated_summation: +#include "summation_cxx/ranged.hpp" +%endif + // sympy wraps real around symbols which actually are real: #define re(x) (x) @@ -152,7 +156,12 @@ namespace odesys_anyode { AnyODE::Status OdeSys::rhs(realtype x, const realtype * const ANYODE_RESTRICT y, - realtype * const ANYODE_RESTRICT f) { + realtype * const ANYODE_RESTRICT out) { + %if p_compensated_summation: + summation_cxx::RangedAccumulatorNeumaier f(out); + %else: + realtype * const f = out; + %endif %if isinstance(p_rhs, str): ${p_rhs} %else: diff --git a/pyodesys/native/sources/summation_cxx/accumulator.hpp b/pyodesys/native/sources/summation_cxx/accumulator.hpp index 475fd1c8..a06fbafb 100644 --- a/pyodesys/native/sources/summation_cxx/accumulator.hpp +++ b/pyodesys/native/sources/summation_cxx/accumulator.hpp @@ -49,6 +49,27 @@ struct Accumulator : public detail::Operators> } return ta.template to(); } + +private: + template + static constexpr void from_(Accumulator& acu, U arg, Us... args) + { + acu += arg; + if constexpr (sizeof...(args) > 0) { + from_(acu, args...); + } + } + +public: + template + static constexpr Accumulator from(U arg, Us... args) + { + Accumulator acu { arg }; + if constexpr (sizeof...(args) > 0) { + Accumulator::from_(acu, args...); + } + return acu; + } }; template using AccumulatorKahan = Accumulator; diff --git a/pyodesys/native/sources/summation_cxx/compensated.hpp b/pyodesys/native/sources/summation_cxx/compensated.hpp index ee31159c..33b9e43f 100644 --- a/pyodesys/native/sources/summation_cxx/compensated.hpp +++ b/pyodesys/native/sources/summation_cxx/compensated.hpp @@ -48,6 +48,7 @@ namespace /* anonymous */ { T& SMMTNCXX_RESTRICT carry, const T& SMMTNCXX_RESTRICT elem) { + // cppcheck-suppress redundantAssignment SMMTNCXX_NEUMAIER_ADD(acm, carry, elem, T, tmp, true); } } diff --git a/pyodesys/native/symcse/compensated.py b/pyodesys/native/symcse/compensated.py index c30bd329..3bc0c842 100644 --- a/pyodesys/native/symcse/compensated.py +++ b/pyodesys/native/symcse/compensated.py @@ -10,8 +10,19 @@ from functools import reduce from operator import add, mul from sympy import ( - Abs, Add, And, Eq, Expr, Lt, Mul, Ne, numbered_symbols, Piecewise, - postorder_traversal, Symbol, Tuple + Abs, + Add, + And, + Eq, + Expr, + Lt, + Mul, + Ne, + numbered_symbols, + Piecewise, + postorder_traversal, + Symbol, + Tuple, ) from sympy.codegen import Assignment, aug_assign, CodeBlock from sympy.codegen.ast import AssignmentBase, Token, While, break_ @@ -21,15 +32,13 @@ def If(cond, body): - return While(cond, CodeBlock( - *body, - break_ - )) + return While(cond, CodeBlock(*body, break_)) + class _NeumaierAdd(Token, Expr): """Represents KBN compensated summation.""" - __slots__ = ('terms', 'accum', 'carry', 'temp') + __slots__ = ("terms", "accum", "carry", "temp") _construct_terms = staticmethod(lambda args: Tuple(*args)) def _ccode(self, printer): @@ -53,12 +62,14 @@ def to_statements(self, existing, expanded, *, transients, do_swap=False): st = [] if neum: if True: - st.append(Assignment(self.accum, neum[0][0]*neum[0][1].accum)) - ordinary.extend([oth*na.accum for oth, na in neum[1:]]) + st.append(Assignment(self.accum, neum[0][0] * neum[0][1].accum)) + ordinary.extend([oth * na.accum for oth, na in neum[1:]]) else: - st.append(Assignment(self.accum, sum(oth*na.accum for oth, na in neum))) + st.append( + Assignment(self.accum, sum(oth * na.accum for oth, na in neum)) + ) - st.append(Assignment(self.carry, sum(oth*na.carry for oth, na in neum))) + st.append(Assignment(self.carry, sum(oth * na.carry for oth, na in neum))) else: st.append(Assignment(self.accum, ordinary.pop(0))) st.append(Assignment(self.carry, 0)) @@ -88,8 +99,9 @@ def to_statements(self, existing, expanded, *, transients, do_swap=False): tr = next(transients) st.append(Assignment(tr, elem)) elem = tr - st.extend(_NeumaierAdd._impl_add( - self.accum, self.carry, elem, self.temp, do_swap)) + st.extend( + _NeumaierAdd._impl_add(self.accum, self.carry, elem, self.temp, do_swap) + ) expanded.add(self) return st @@ -106,16 +118,19 @@ def _impl_add(accum, carry, elem, temp, do_swap=False): pw = Piecewise((big_temp, Abs(temp) > abs_elem), (big_elem, True)) statements = [ Assignment(temp, accum + elem), - aug_assign(carry, '+', pw), - Assignment(accum, temp) + aug_assign(carry, "+", pw), + Assignment(accum, temp), ] if do_swap: return [ - If(And(Eq(carry, 0), Ne(accum, 0), Lt(Abs(accum), abs_elem)), [ + If( + And(Eq(carry, 0), Ne(accum, 0), Lt(Abs(accum), abs_elem)), + [ Assignment(temp, accum), Assignment(accum, carry), - Assignment(carry, temp) - ]) + Assignment(carry, temp), + ], + ) ] + statements else: return statements @@ -125,7 +140,7 @@ def _impl_finalize(accum, carry): return Add(accum, carry) -class _NeumaierTransformer(NullTransformer): +class NeumaierTransformer(NullTransformer): """Transform Add instances in CSEs to use compensated sum. Parameters @@ -135,8 +150,20 @@ class _NeumaierTransformer(NullTransformer): and 100 (all passes). """ - def __init__(self, repl, red, *, tmp_pfx="t", neu_pfx="n", up_to_debug=100, - limit=3, parent=None, ignore=None, do_swap=False): + def __init__( + self, + repl, + red, + *, + tmp_pfx="t", + trs_pfx="r", + neu_pfx="n", + up_to_debug=100, + limit=3, + parent=None, + ignore=None, + do_swap=False, + ): self.repl = repl self.red = red self.limit = limit @@ -150,9 +177,10 @@ def __init__(self, repl, red, *, tmp_pfx="t", neu_pfx="n", up_to_debug=100, self.expanded = set() self._analysis = defaultdict(int) self._tmp_var = numbered_symbols(tmp_pfx) + self._trs_var = numbered_symbols(trs_pfx) self._neu_var = numbered_symbols(neu_pfx) self.passes = [] - for p in filter(lambda n: n.startswith('_pass_'), dir(self)): + for p in filter(lambda n: n.startswith("_pass_"), dir(self)): null, rest = p.split("_pass_") assert null == "" num, *_ = rest.split("_") @@ -170,16 +198,16 @@ def remapping_for_arrayification(self, template="m_glob[{0}]"): for st in self.statements: if st.lhs in remapping or st.lhs in self._all_tempv: continue - #if st.lhs in self._all_accum or st.lhs in self._all_carry: + # if st.lhs in self._all_accum or st.lhs in self._all_carry: remapping[st.lhs] = Symbol(template.format(i), real=True) i = i + 1 return remapping def _mk_Neu(self, terms, lhs): pfx = str(next(self._tmp_var)) if lhs is None else str(lhs) - accum = Symbol(pfx+'a', real=True) - carry = Symbol(pfx+'c', real=True) - tempv = Symbol(pfx+'t', real=True) + accum = Symbol(pfx + "a", real=True) + carry = Symbol(pfx + "c", real=True) + tempv = Symbol(pfx + "t", real=True) na = _NeumaierAdd(terms, accum, carry, tempv) self._all_accum[accum] = na self._all_carry[carry] = na @@ -200,8 +228,10 @@ def _single_pass(self, statements, pass_): elif hasattr(st, "body"): assert isinstance(st.body, CodeBlock) new_body = CodeBlock(*self._single_pass(st.body.args, pass_)) - new_args = (new_body if attr == 'body' else getattr(st, attr) - for attr in st.__slots__) + new_args = ( + new_body if attr == "body" else getattr(st, attr) + for attr in st.__slots__ + ) new_stmts.append(st.__class__(*new_args)) else: new_stmts.append(st) # no-op (e.g. BreakToken instance) @@ -232,8 +262,9 @@ def _pass_10_create_nodes(self, lhs, rhs, *, statements, debug=False): while True: for _add in filter(lambda x: x.is_Add, postorder_traversal(new_rhs)): - score = self._analysis.get(lhs, 0) + reduce(add, [ - self._analysis.get(k, 1) for k in _add.args]) + score = self._analysis.get(lhs, 0) + reduce( + add, [self._analysis.get(k, 1) for k in _add.args] + ) if score >= self.limit or any(self._is_Neu(arg) for arg in _add.args): na = self._mk_Neu(_add.args, lhs) if _add is rhs and lhs is not None: @@ -255,9 +286,14 @@ def _pass_50_to_stmnts(self, lhs, rhs, *, statements): continue self._pass_50_to_stmnts(lhs, neu.terms, statements=statements) if neu not in self.expanded: - statements.extend(neu.to_statements( - self.created, self.expanded, do_swap=self.do_swap, - transients=self._tmp_var)) + statements.extend( + neu.to_statements( + self.created, + self.expanded, + do_swap=self.do_swap, + transients=self._trs_var, + ) + ) return rhs def _pass_60_xrepl(self, lhs, rhs, *, statements): @@ -286,12 +322,13 @@ def _group(self, x): return result def _has(self, term): - return term in self.created or term in self._all_accum or term in self._all_carry + return ( + term in self.created or term in self._all_accum or term in self._all_carry + ) def _pass_95_group(self, lhs, rhs, *, statements): new_rhs = rhs.replace( - lambda s: s.is_Add and any(self._has(t) for t in s.args), - self._group + lambda s: s.is_Add and any(self._has(t) for t in s.args), self._group ) return new_rhs diff --git a/pyodesys/native/symcse/core.py b/pyodesys/native/symcse/core.py index 53cf32d0..99776afe 100644 --- a/pyodesys/native/symcse/core.py +++ b/pyodesys/native/symcse/core.py @@ -11,8 +11,11 @@ def __init__(self, repl, red, parent=None, ignore=None): self.statements = [Assignment(lhs, rhs) for lhs, rhs in repl] self.final_exprs = red - def statements_with_declarations(self, pred=lambda s: '[' not in s.name, - type_=float64): + def statements_with_declarations( + self, + pred=lambda s: "[" not in s.name, + type_=(lambda lhs, rhs: (float64, rhs)) + ): """Conditionally add declarations.""" return self._declare(self.statements, pred=pred, type_=type_) @@ -32,16 +35,20 @@ def _as_decl(ctx_seen, statements, is_const, *, pred, type_, lift=False): for st in statements: if isinstance(st, Assignment) and st.lhs not in ctx_seen and pred(st.lhs): ctx_seen.add(st.lhs) - var = Variable(st.lhs, type=type_, attrs=[value_const] if - is_const[st.lhs] == 1 else []) + t, new_rhs = type_(st.lhs, st.rhs) + var = Variable( + st.lhs, + type=t, + attrs=[value_const] if is_const[st.lhs] == 1 else [], + ) if lift: lifted.append(var.as_Declaration()) else: - st = var.as_Declaration(value=st.rhs) + st = var.as_Declaration(value=new_rhs) elif hasattr(st, "body"): body_stmnts, body_lifted = NullTransformer._as_decl( - ctx_seen, st.body.args, is_const, - pred=pred, type_=type_, lift=True) + ctx_seen, st.body.args, is_const, pred=pred, type_=type_, lift=True + ) if lift: lifted.extend(body_lifted) else: @@ -51,12 +58,14 @@ def _as_decl(ctx_seen, statements, is_const, *, pred, type_, lift=False): new_stmnts.append(st) return new_stmnts, lifted - def _declare(self, stmts, *, pred=lambda x: True, type_=float64): + def _declare(self, stmts, *, pred=lambda x: True, type_=(lambda lhs, rhs: (float64, rhs))): seen = set() is_const = defaultdict(int) self._is_const(is_const, stmts) - new_stmnts, lifted = self._as_decl(seen, stmts, is_const, pred=pred, lift=False, type_=type_) - assert(len(lifted) == 0) + new_stmnts, lifted = self._as_decl( + seen, stmts, is_const, pred=pred, lift=False, type_=type_ + ) + assert len(lifted) == 0 return new_stmnts def apply_remapping(self, remapping): diff --git a/pyodesys/native/symcse/groupwise.py b/pyodesys/native/symcse/groupwise.py index c5f35e86..6b1ac4af 100644 --- a/pyodesys/native/symcse/groupwise.py +++ b/pyodesys/native/symcse/groupwise.py @@ -1,5 +1,6 @@ """Handle common cses among groups of code.""" +from collections import defaultdict from functools import reduce from operator import add import numpy as np @@ -18,7 +19,7 @@ def pre_process(expr): """Simplify, expand & factor. - Example: expr.simplify(rational=True).expand().factor() """ + Example: expr.simplify(rational=True).expand().factor()""" return expr.factor() @@ -30,18 +31,21 @@ def post_process(expr): class GroupwiseCSE: """Eliminate common sub-expressions from groups of expressions.""" - def __init__(self, groups, *, - common_cse_template="common_cse{0}", - common_ignore=(), - code_printer=None, - subsd=None, - Transformer=NullTransformer, - type_=float64, - pre_process=None, - post_process=None, - backend=None, - transformer_kw=None - ): + def __init__( + self, + groups, + *, + common_cse_template="common_cse{0}", + common_ignore=(), + code_printer=None, + subsd=None, + transformers=None, + type_=(lambda lhs, rhs: (float64, rhs)), + pre_process=None, + post_process=None, + backend=None, + transformer_kws=None + ): """ Parameters ---------- @@ -49,20 +53,25 @@ def __init__(self, groups, *, \\*\\*kwargs : see code for now. """ + if transformers is None: + transformers = defaultdict(lambda: NullTransformer) if code_printer is None: code_printer = CPrinter() - if transformer_kw is None: - transformer_kw = {} + if transformer_kws is None: + transformer_kws = defaultdict(dict) self._code_printer = code_printer self._type = type_ self._keys, _values = zip(*groups.items()) - self._spans = np.cumsum([0]+list(map(len, _values))) + self._spans = np.cumsum([0] + list(map(len, _values))) if backend is None: backend = Backend() self.backend = backend _all_values = reduce(add, map(list, _values)) _all_exprs = list(map(pre_process, _all_values) if pre_process else _all_values) - _all_exprs = [e.replace(lambda s: s.is_Symbol, lambda s: sympy.Symbol(s.name, real=True)) for e in _all_exprs] + _all_exprs = [ + e.replace(lambda s: s.is_Symbol, lambda s: sympy.Symbol(s.name, real=True)) + for e in _all_exprs + ] common_ignore = [sympy.Symbol(ig.name, real=True) for ig in common_ignore] for e in _all_exprs: for fs in e.free_symbols: @@ -70,31 +79,38 @@ def __init__(self, groups, *, # Switching between symengine/sympy is tricky, consistently using # real=True with Symbols allows us to assume real=True for SymEngine # symbols (which lack support for assumptions at the time of writing). - raise NotImplementedError("Only use explicitly real valued symbols.") + raise NotImplementedError( + "Only use explicitly real valued symbols." + ) repls, reds = self._common_cse( - _all_exprs, ignore=common_ignore, - symbols=numbered_symbols('cse_comm_locl', real=True) + _all_exprs, + ignore=common_ignore, + symbols=numbered_symbols("cse_comm_locl", real=True), ) if post_process: repls = [(s, post_process(e)) for s, e in repls] reds = [post_process(e) for e in reds] - self._comm_tformr = Transformer(repls, reds, ignore=common_ignore, **transformer_kw) - remap = self._comm_tformr.remapping_for_arrayification(template=common_cse_template) + self._comm_tformr = transformers[None]( + repls, reds, ignore=common_ignore, **transformer_kws[None] + ) + remap = self._comm_tformr.remapping_for_arrayification( + template=common_cse_template + ) self._comm_tformr.apply_remapping(remap) _subsd = {sympy.Symbol(k.name, real=True): v for k, v in (subsd or {}).items()} self._comm_tformr.apply_remapping(_subsd) self.n_remapped = len(remap) - assert(len(self._comm_tformr.final_exprs) == len(reds)) + assert len(self._comm_tformr.final_exprs) == len(reds) del reds self._per_g_tformrs = self._get_g_tformrs( - self._comm_tformr, Transformer=Transformer, - transformer_kw=transformer_kw, + self._comm_tformr, + transformers=transformers, + transformer_kws=transformer_kws, post_process=post_process, - subsd=_subsd + subsd=_subsd, ) - @property def keys(self): """Retrieve the keys of the groups.""" @@ -115,19 +131,24 @@ def _common_cse(self, all_exprs, **kwargs): break return ( [(lhs.xreplace(comm_subs), rhs.xreplace(comm_subs)) for lhs, rhs in repls], - [r.xreplace(comm_subs) for r in reds] + [r.xreplace(comm_subs) for r in reds], ) - def _get_g_tformrs(self, comm_tformr, *, Transformer, transformer_kw, post_process, subsd): + def _get_g_tformrs( + self, comm_tformr, *, transformers, transformer_kws, post_process, subsd + ): per_g = {} for i, k in enumerate(self._keys): g_repls, g_exprs = self.backend.cse( - comm_tformr.final_exprs[slice(*self._spans[i:i+2])], - symbols=numbered_symbols("cse", real=True)) + comm_tformr.final_exprs[slice(*self._spans[i : i + 2])], + symbols=numbered_symbols("cse", real=True), + ) if post_process: g_repls = [(s, post_process(e)) for s, e in g_repls] g_exprs = [post_process(e) for e in g_exprs] - g_tformr = Transformer(g_repls, g_exprs, parent=comm_tformr, **transformer_kw) + g_tformr = transformers[k]( + g_repls, g_exprs, parent=comm_tformr, **transformer_kws[k] + ) g_tformr.apply_remapping(subsd) per_g[k] = g_tformr @@ -144,17 +165,21 @@ def _declare(stmts, *, pred, type_=float64): result.append(st) return result - def common_statements(self, declare=False): + def common_statements(self, declare=False, type_=None): """Initialize the common sub-expressions among the groups.""" if declare: - return self._comm_tformr.statements_with_declarations(pred=declare, type_=self._type) + return self._comm_tformr.statements_with_declarations( + pred=declare, type_=type_ or self._type + ) else: return self._comm_tformr.statements - def statements(self, gk, declare=False): + def statements(self, gk, declare=False, type_=None): """Initialize the group specific sub-expressions.""" if declare: - return self._per_g_tformrs[gk].statements_with_declarations(pred=declare, type_=self._type) + return self._per_g_tformrs[gk].statements_with_declarations( + pred=declare, type_=type_ or self._type + ) else: return self._per_g_tformrs[gk].statements diff --git a/pyodesys/native/symcse/util.py b/pyodesys/native/symcse/util.py index ee8afa28..252b37eb 100644 --- a/pyodesys/native/symcse/util.py +++ b/pyodesys/native/symcse/util.py @@ -5,7 +5,7 @@ from operator import mul, add import sympy -from sympy.codegen.ast import float80 +from sympy.codegen.ast import float80, String, Token from sympy.printing.c import C99CodePrinter from sympy.printing.cxx import CXX17CodePrinter from sympy.printing.pycode import PythonCodePrinter @@ -45,17 +45,24 @@ class UnevaluatedRealPropagatingExpr(sympy.UnevaluatedExpr): def _eval_is_real(self): return self.args[0].is_real +class OperatorDot(Token): + __slots__ = ('lhs', 'rhs') + #_construct_lhs = String + #_construct_rhs = String class _UnevaluatedExprPrinterMixin: def _print_UnevaluatedExpr(self, arg): return "(%s)" % super()._print_UnevaluatedExpr(arg) def _print_Integer(self, arg): - if abs(arg) > 2**53: + if abs(arg) > 2 ** 53: return self._print(sympy.Float(arg)) else: return super()._print_Integer(arg) + def _print_OperatorDot(self, arg): + return self._print(arg.lhs) + '.' + self._print(arg.rhs) + @staticmethod def _replace_re(arg): if isinstance(arg, sympy.UnevaluatedExpr) and arg.args[0].is_real: @@ -178,24 +185,27 @@ def __init__(self, use_symengine=None, kw_cse=None, assume_real=True): self.kw_cse = kw_cse self.assume_real = assume_real - def Symbol(self, name): if self.use_symengine: - return se.Symbol(name) # https://github.com/symengine/symengine.py/issues/286 + return se.Symbol( + name + ) # https://github.com/symengine/symengine.py/issues/286 else: return sympy.Symbol(name, real=self.assume_real) def sympy2se(self, x): - if hasattr(x, '_sympy_'): + if hasattr(x, "_sympy_"): return x # looks like that's already a SymEngine object return sympy2symengine(x, raise_error=True) def se2sympy(self, x): - if not hasattr(x, '_sympy_'): + if not hasattr(x, "_sympy_"): assert isinstance(x, sympy.Basic) return x # looks like that's already a SymPy object tmp = x._sympy_() - return tmp.xreplace({s: sympy.Symbol(s.name, real=self.assume_real) for s in tmp.free_symbols}) + return tmp.xreplace( + {s: sympy.Symbol(s.name, real=self.assume_real) for s in tmp.free_symbols} + ) def cse(self, exprs, **kwargs): """Perform common sub-expression elimination.""" @@ -256,15 +266,11 @@ def lambdify(self, args, exprs): class BackendWithDisabledCSE(Backend): - def cse(self, exprs, **kwargs): return [], exprs -default_settings = dict( - math_macros={}, - type_mappings={float80: "long double"} -) +default_settings = dict(math_macros={}, type_mappings={float80: "long double"}) def ccode(arg, **kwargs): From 68e5e6cd558c84e2977632b4cce6b416fbfef221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 14 Sep 2021 15:59:37 +0200 Subject: [PATCH 050/102] forgot impl --- pyodesys/native/_base.py | 14 +- .../native/sources/summation_cxx/impl.hpp | 140 ++++++++++++++++++ 2 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 pyodesys/native/sources/summation_cxx/impl.hpp diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index e1327dec..33914231 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -45,7 +45,7 @@ _compile_kwargs = { 'options': ['warn', 'pic', 'debug', 'openmp'], # DO-NOT-MERGE!!! debug/fast - 'std': 'c++11', + 'std': 'c++17', 'include_dirs': [np.get_include(), pkg_resources.resource_filename(__name__, 'sources')], 'libraries': [], 'cplus': True, @@ -127,11 +127,11 @@ class _NativeCodeBase(Cpp_Code): # `namespace_override` is set in init # `namespace_extend` is set in init - def __init__(self, odesys, *args, groupwise_kw=None, assigner_kw=None, + def __init__(self, odesys, *args, groupwise_kw=None, assigner_kws=None, types=None, **kwargs): self.groupwise_kw = groupwise_kw self.types = types - self.assigner_kw = assigner_kw or {} + self.assigner_kws = assigner_kws or defaultdict(dict) if Cpp_Code is object: raise ModuleNotFoundError("failed to import Cpp_Code from pycodeexport") if compile_sources is None: @@ -184,12 +184,10 @@ def __init__(self, odesys, *args, groupwise_kw=None, assigner_kw=None, # return self.odesys.be.ccode(expr_x) def variables(self): - ny = self.odesys.ny if self.odesys.band is not None: raise NotImplementedError("Banded jacobian not yet implemented.") all_invar = tuple(self.odesys.all_invariants()) - ninvar = len(all_invar) jac = self.odesys.get_jac() nnz = self.odesys.nnz all_exprs = dict( @@ -199,13 +197,9 @@ def variables(self): if jac is not False and nnz < 0: jac_dfdx = list(reduce(add, jac.tolist() + self.odesys.get_dfdx().tolist())) all_exprs["jac_dfdt"] = jac_dfdx - nj = len(jac_dfdx) elif jac is not False and nnz >= 0: jac_dfdx = list(reduce(add, jac.tolist())) all_exprs["jac_dfdt"] = jac_dfdx - nj = len(jac_dfdx) - else: - nj = 0 jtimes = self.odesys.get_jtimes() if jtimes is not False: @@ -253,7 +247,7 @@ def _cses(k): cses = {k: gw.render(_cses(k)) for k in gw.keys} n_common_cses = gw.n_remapped common_cses = gw.render(CodeBlock(*gw.common_statements(declare=not_arr, type_=types[None]))) - assigners = {k: _AssignerGW(k, gw, **self.assigner_kw) for k in gw.keys} + assigners = {k: _AssignerGW(k, gw, **self.assigner_kws[k]) for k in gw.keys} ns = dict( _message_for_rendered=[ diff --git a/pyodesys/native/sources/summation_cxx/impl.hpp b/pyodesys/native/sources/summation_cxx/impl.hpp new file mode 100644 index 00000000..1ebeb6ab --- /dev/null +++ b/pyodesys/native/sources/summation_cxx/impl.hpp @@ -0,0 +1,140 @@ +#pragma once +#include +#include +#include + +namespace summation_cxx { +template +struct Accumulator; +template +struct AccuView; +namespace detail { + template + struct Operators; +} +} +namespace summation_cxx::detail { +template +struct Operators { + typedef T underlying_type; + typedef Accumulator accumulator_type; + typedef AccuView view_type; + +#define ACCUM(cv_qual) static_cast(this)->accum() +#define CARRY(cv_qual) static_cast(this)->carry() + template + U to() const + { + if constexpr (Derived::compensation_scheme == Compensation::KAHAN) { + return ACCUM(const); + } else if constexpr (Derived::compensation_scheme == Compensation::NEUMAIER || Derived::compensation_scheme == Compensation::NEUMAIER_SWAP) { + if constexpr (sizeof(T) > sizeof(U)) { + return ACCUM(const) + CARRY(const); + } else { + return static_cast(ACCUM(const)) + static_cast(CARRY(const)); + } + } else { + assert(false); + } + return U { 0 } / U { 0 }; /* unreachable code, but would return NaN */ + } + Derived& operator+=(T arg) + { + if constexpr (scheme == Compensation::KAHAN) { + accum_kahan_destructive(ACCUM(), CARRY(), arg); + } else if constexpr (scheme == Compensation::NEUMAIER) { + accum_neumaier(ACCUM(), CARRY(), arg); + } else if constexpr (scheme == Compensation::NEUMAIER_SWAP) { + accum_neumaier_swap(ACCUM(), CARRY(), arg); + } else { + assert(false); + } + return *(static_cast(this)); + } + Derived& operator-=(T arg) + { + Derived& self = *(static_cast(this)); + self += -arg; + return self; + } + + void operator=(const T& arg) + { + ACCUM() = arg; + CARRY() = 0; + } + void operator/=(const T& arg) + { + ACCUM() /= arg; + CARRY() /= arg; + } + void operator*=(const T& arg) + { + ACCUM() *= arg; + CARRY() *= arg; + } + void operator+=(const accumulator_type& other) + { + *this += other.accum(); + CARRY() += other.carry(); + } + void operator-=(const accumulator_type& other) + { + *this -= other.accum(); + CARRY() -= other.carry(); + } + accumulator_type operator*(const T& arg) const + { + return accumulator_type(ACCUM(const) * arg, CARRY(const) * arg); + } + accumulator_type operator*(const accumulator_type& other) const + { + return accumulator_type(ACCUM(const) * other.accum(), + CARRY(const) * other.accum() + ACCUM(const) * other.carry() + CARRY(const) * other.carry()); + } + accumulator_type operator/(const accumulator_type& other) const + { + const T denom = other.template to(); + return accumulator_type { ACCUM(const) / denom, CARRY(const) / denom }; + } + accumulator_type operator+(const accumulator_type& other) const + { + return accumulator_type(ACCUM(const) + other.accum(), CARRY(const) + other.carry()); + } + accumulator_type operator-(const accumulator_type& other) const + { + return accumulator_type(ACCUM(const) - other.accum(), CARRY(const) - other.carry()); + } + accumulator_type operator+() const + { + return accumulator_type(ACCUM(const), CARRY(const)); + } + accumulator_type operator-() const + { + return accumulator_type(-ACCUM(const), -CARRY(const)); + } +}; +#define SMMTNCXX_COMMUTATIVE_OP(OP) \ + template \ + typename Derived::accumulator_type operator OP( \ + const typename Derived::underlying_type& arg_a, const Derived& arg_b) \ + { \ + return arg_b OP arg_a; /* multiplication is commutative */ \ + } +SMMTNCXX_COMMUTATIVE_OP(*) +SMMTNCXX_COMMUTATIVE_OP(+) +#define SMMTNCXX_PROMOTING_OP(OP) \ + template \ + typename Derived::accumulator_type operator OP( \ + const typename Derived::underlying_type& arg_a, const Derived& arg_b) \ + { \ + return Derived { arg_a } OP arg_b; /* multiplication is commutative */ \ + } +SMMTNCXX_PROMOTING_OP(-) + +#undef SMMTNCXX_COMMUTATIVE_OP +#undef SMMTNCXX_PROMOTING_OP +#undef ACCUM +#undef CARRY + +} From 16a9f6dfeaeb02b04ee91f3660818343d18b90d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 6 Dec 2021 15:59:29 +0100 Subject: [PATCH 051/102] make groupwise_kw optional --- pyodesys/native/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 33914231..8c7dfd84 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -236,7 +236,7 @@ def variables(self): subsd=subsd, # Transformer=Transformer, # transformer_kw=transformer_kw, - **self.groupwise_kw #use_cse + **(self.groupwise_kw or {}) #use_cse ) def not_arr(s): From f66dcc489478ceee15dc39610fffaacd8491aa1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 11 Jan 2022 17:21:37 +0100 Subject: [PATCH 052/102] git:// -> https:// --- .drone.yml | 2 +- .gitmodules | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index 76301943..5d58ab4f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3,7 +3,7 @@ clone: image: plugins/git recursive: true submodule_override: - external/anyode: git://github.com/bjodah/anyode.git + external/anyode: https://github.com/bjodah/anyode.git pipeline: restore-cache: diff --git a/.gitmodules b/.gitmodules index 48899e90..62c0b98e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "external/anyode"] path = external/anyode - url = git://github.com/bjodah/anyode + url = https://github.com/bjodah/anyode branch = master \ No newline at end of file From 26c3fc641bc36c7ab9a43b7eaff0a18327bb14ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 31 Jan 2022 11:16:24 +0100 Subject: [PATCH 053/102] scalar ode --- examples/_scalar_ode.ipynb | 160 +++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 examples/_scalar_ode.ipynb diff --git a/examples/_scalar_ode.ipynb b/examples/_scalar_ode.ipynb new file mode 100644 index 00000000..faaf1bf1 --- /dev/null +++ b/examples/_scalar_ode.ipynb @@ -0,0 +1,160 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "0e2171ed", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from pyodesys.symbolic import SymbolicSys\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0ab26a9", + "metadata": {}, + "outputs": [], + "source": [ + "sys1 = SymbolicSys.from_callback(lambda t, y, p: [-y[0]/t], 1)\n", + "sys1.exprs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd0b5fe4", + "metadata": {}, + "outputs": [], + "source": [ + "def vary(sys, kws):\n", + " res = [sys.integrate((1, 1e20), [1.0], atol=1e-50, nsteps=64000, **kw) for kw in kws]\n", + " return res" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4796d001", + "metadata": {}, + "outputs": [], + "source": [ + "kws = [\n", + " dict(integrator='cvode', method='bdf', rtol=1e-6),\n", + " dict(integrator='cvode', method='adams', rtol=1e-10),\n", + " dict(integrator='gsl', method='bsimp', rtol=1e-10),\n", + " dict(integrator='gsl', method='msbdf', rtol=1e-10)\n", + "]\n", + "\n", + "res1 = vary(sys1, kws)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1323811", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_variation(res, kws):\n", + " fig, axes = plt.subplots(1, len(res), figsize=(16,6), dpi=150)\n", + " for ax, r, kw in zip(axes, res, kws):\n", + " #r.plot(ax=ax)\n", + " rx = 1/r.xout\n", + " ax.plot(r.xout, np.abs(r.yout.squeeze() - rx)/rx, label='observed')\n", + " ax.set_xscale('log')\n", + " ax.set_yscale('linear')\n", + " ax.set_ylabel('|relative error|')\n", + "\n", + " random_walk = np.sqrt(1.0+np.array(range(r.xout.size))) # non-systematic errors (no bias)\n", + " #ax.plot(r.xout, random_walk*kw['rtol'], label='prognosis')\n", + "\n", + " ax.legend()\n", + "\n", + " ax2 = ax.twinx()\n", + " ax2.plot(r.xout[:-1], np.diff(r.xout), color='k', linewidth=0.5\n", + " #, ls='None', marker='.', markersize=0.5, alpha=0.3\n", + " )\n", + " ax2.set_yscale('log')\n", + " ax2.set_ylim([1e-12, 1e20])\n", + " ax2.set_ylabel('step size')\n", + "\n", + " ax.set_title(f\"{kw['integrator']}\\n{kw['method']}\\n{r.info['n_steps']=}\\n{r.info['time_cpu']=}\\n \")\n", + "\n", + " fig.tight_layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33d96a20", + "metadata": {}, + "outputs": [], + "source": [ + "plot_variation(res1, kws)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c876e70", + "metadata": {}, + "outputs": [], + "source": [ + "sys2 = sys1.as_autonomous()\n", + "sys2.exprs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e6a2083", + "metadata": {}, + "outputs": [], + "source": [ + "plot_variation(vary(sys2, kws), kws)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65b2d5cd", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd167f60", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 66dc9e69d53ae6372cf43f3578ce57cd7cdf7ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 31 Jan 2022 21:49:46 +0100 Subject: [PATCH 054/102] sort legend --- pyodesys/plotting.py | 5 +++-- pyodesys/results.py | 26 +++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/pyodesys/plotting.py b/pyodesys/plotting.py index 384a34d0..07edddb3 100644 --- a/pyodesys/plotting.py +++ b/pyodesys/plotting.py @@ -123,9 +123,10 @@ def plot_kwargs_cb(idx, lines=False, markers=False, labels=None): clr = plot_kwargs_cb(idx)['c'] ax.fill_between(x, _y[:, idx] - yerr[:, idx], _y[:, idx] + yerr[:, idx], facecolor=clr, alpha=.3) - if isinstance(yscale, str) and 'linthreshy' in yscale: + if isinstance(yscale, str) and 'linthresh' in yscale: + yscale = yscale.replace('linthreshy', 'linthresh') arg, kw = yscale.split(';') - thresh = eval('dict(%s)' % kw)['linthreshy'] + thresh = eval('dict(%s)' % kw)['linthresh'] ax.axhline(thresh, linewidth=.5, linestyle='--', color='k', alpha=.5) ax.axhline(-thresh, linewidth=.5, linestyle='--', color='k', alpha=.5) diff --git a/pyodesys/results.py b/pyodesys/results.py index cfe7c27c..784043bc 100644 --- a/pyodesys/results.py +++ b/pyodesys/results.py @@ -178,7 +178,7 @@ def stiffness(self, xyp=None, eigenvals_cb=None): return (np.abs(singular_values).max(axis=-1) / np.abs(singular_values).min(axis=-1)) - def _plot(self, cb, x=None, y=None, legend=None, **kwargs): + def _plot(self, cb, x=None, y=None, legend=None, sort_legend=False, **kwargs): if x is None: x = self.xout if y is None: @@ -198,9 +198,26 @@ def _plot(self, cb, x=None, y=None, legend=None, **kwargs): if legend is None: if (kwargs.get('latex_names') or None) is not None or (kwargs['names'] or None) is not None: legend = True - return cb(x, y, legend=legend, **kwargs) + ax = cb(x, y, legend=legend, **kwargs) + if sort_legend: + handles, labels = ax.get_legend_handles_labels() + # sort both labels and handles by labels + # if kwargs.get('names', None) is None: + # sel = slice(None) + # else: + # sel = [n in kwargs['names'] for n in self.odesys.names] + reorder = np.argsort(y[-1, kwargs.get('indices', slice(None))])[::-1] + if isinstance(legend, dict): + legend_kw = legend + elif not legend: + return ax + else: + legend_kw = dict() + ax.legend([handles[i] for i in reorder], [labels[i] for i in reorder], **legend_kw) + return ax - def plot(self, info_vlines_kw=None, between=None, deriv=False, title_info=0, **kwargs): + def plot(self, info_vlines_kw=None, between=None, deriv=False, title_info=0, + **kwargs): """ Plots the integrated dependent variables from last integration. Parameters @@ -216,6 +233,7 @@ def plot(self, info_vlines_kw=None, between=None, deriv=False, title_info=0, **k \\*\\*kwargs: See :func:`pyodesys.plotting.plot_result` """ + if between is not None: if 'x' in kwargs or 'y' in kwargs: raise ValueError("x/y & between given.") @@ -230,7 +248,9 @@ def plot(self, info_vlines_kw=None, between=None, deriv=False, title_info=0, **k if 'y' in kwargs: raise ValueError("Cannot give both deriv=True and y.") kwargs['y'] = self.odesys.f_cb(*self._internals()) + ax = self._plot(plot_result, **kwargs) + if title_info: ax.set_title( (getattr(self.odesys, 'description', None) or '') + From 3fbf822ce6f2f7c88b66da8405c1779c2a280c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 8 Jun 2022 11:37:38 +0200 Subject: [PATCH 055/102] fix last tests for use with GroupwiseCSE --- pyodesys/native/_base.py | 6 +- .../native/sources/odesys_anyode_template.cpp | 7 +- .../sources/summation_cxx/compensated.hpp | 36 +++--- .../native/sources/summation_cxx/impl.hpp | 42 +++--- .../native/sources/summation_cxx/macros.hpp | 65 +++++----- .../native/sources/summation_cxx/ranged.hpp | 122 ++++++++++++++++++ .../native/sources/summation_cxx/view.hpp | 41 ++++++ pyodesys/native/symcse/ordered_add.py | 2 +- pyodesys/native/tests/_tests.py | 20 +-- 9 files changed, 263 insertions(+), 78 deletions(-) create mode 100644 pyodesys/native/sources/summation_cxx/ranged.hpp create mode 100644 pyodesys/native/sources/summation_cxx/view.hpp diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 8c7dfd84..02ad9522 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -228,15 +228,15 @@ def variables(self): subsd.update({k: self.odesys.be.Symbol('v[%d]' % idx) for idx, k in enumerate(v)}) - ignore = (() if self.odesys.indep is None else (self.odesys.indep,)) + self.odesys.dep + v + common_ignore = (() if self.odesys.indep is None else (self.odesys.indep,)) + self.odesys.dep + v gw = GroupwiseCSE( all_exprs, common_cse_template="m_cse[{}]", - common_ignore=ignore, + common_ignore=common_ignore, subsd=subsd, # Transformer=Transformer, # transformer_kw=transformer_kw, - **(self.groupwise_kw or {}) #use_cse + **(self.groupwise_kw or {}) ) def not_arr(s): diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 460c6667..7ad05b95 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -158,7 +158,8 @@ namespace odesys_anyode { const realtype * const ANYODE_RESTRICT y, realtype * const ANYODE_RESTRICT out) { %if p_compensated_summation: - summation_cxx::RangedAccumulatorNeumaier f(out); + summation_cxx::RangedAccumulatorNeumaier f(${p_odesys.ny}); + f.init(out); %else: realtype * const f = out; %endif @@ -168,6 +169,10 @@ namespace odesys_anyode { ${"AnyODE::ignore(x);" if p_odesys.autonomous_exprs else ""} ${p_rhs["cses"]} ${p_rhs["assign"].all(assign_to=lambda i: sympy.Symbol("f[%d]" % i))} + %if p_compensated_summation: + f.commit(); + %endif + this->nfev++; %if p_support_recoverable_error: if (m_error_outside_bounds){ diff --git a/pyodesys/native/sources/summation_cxx/compensated.hpp b/pyodesys/native/sources/summation_cxx/compensated.hpp index 33b9e43f..d5c07709 100644 --- a/pyodesys/native/sources/summation_cxx/compensated.hpp +++ b/pyodesys/native/sources/summation_cxx/compensated.hpp @@ -13,10 +13,10 @@ enum class Compensation { NONE, namespace /* anonymous */ { template - SMMTNCXX_PREFER_INLINE void accum_kahan_destructive( - T& SMMTNCXX_RESTRICT accu, - T& SMMTNCXX_RESTRICT carry, - T& SMMTNCXX_RESTRICT elem) + SXX_PREFER_INLINE void accum_kahan_destructive( + T& SXX_RESTRICT accu, + T& SXX_RESTRICT carry, + T& SXX_RESTRICT elem) { elem -= carry; const T tmp = accu + elem; @@ -24,32 +24,32 @@ namespace /* anonymous */ { accu = tmp; } template - SMMTNCXX_PREFER_INLINE void accum_kahan( - T& SMMTNCXX_RESTRICT accu, - T& SMMTNCXX_RESTRICT carry, - const T& SMMTNCXX_RESTRICT elem) + SXX_PREFER_INLINE void accum_kahan( + T& SXX_RESTRICT accu, + T& SXX_RESTRICT carry, + const T& SXX_RESTRICT elem) { T y = elem; accum_kahan_destructive(accu, carry, y); } template - SMMTNCXX_PREFER_INLINE void accum_neumaier( - T& SMMTNCXX_RESTRICT acm, - T& SMMTNCXX_RESTRICT carry, - const T& SMMTNCXX_RESTRICT elem) + SXX_PREFER_INLINE void accum_neumaier( + T& SXX_RESTRICT acm, + T& SXX_RESTRICT carry, + const T& SXX_RESTRICT elem) { - SMMTNCXX_NEUMAIER_ADD(acm, carry, elem, T, tmp, false); + SXX_NEUMAIER_ADD(acm, carry, elem, T, tmp, false); } template - SMMTNCXX_PREFER_INLINE void accum_neumaier_swap( - T& SMMTNCXX_RESTRICT acm, - T& SMMTNCXX_RESTRICT carry, - const T& SMMTNCXX_RESTRICT elem) + SXX_PREFER_INLINE void accum_neumaier_swap( + T& SXX_RESTRICT acm, + T& SXX_RESTRICT carry, + const T& SXX_RESTRICT elem) { // cppcheck-suppress redundantAssignment - SMMTNCXX_NEUMAIER_ADD(acm, carry, elem, T, tmp, true); + SXX_NEUMAIER_ADD(acm, carry, elem, T, tmp, true); } } } diff --git a/pyodesys/native/sources/summation_cxx/impl.hpp b/pyodesys/native/sources/summation_cxx/impl.hpp index 1ebeb6ab..a7d1d408 100644 --- a/pyodesys/native/sources/summation_cxx/impl.hpp +++ b/pyodesys/native/sources/summation_cxx/impl.hpp @@ -58,10 +58,12 @@ struct Operators { return self; } - void operator=(const T& arg) + Derived& operator=(const T arg) { + Derived& self = *(static_cast(this)); ACCUM() = arg; CARRY() = 0; + return self; } void operator/=(const T& arg) { @@ -73,15 +75,19 @@ struct Operators { ACCUM() *= arg; CARRY() *= arg; } - void operator+=(const accumulator_type& other) + Derived& operator+=(const accumulator_type& other) { - *this += other.accum(); - CARRY() += other.carry(); + Derived& self = *(static_cast(this)); + self += other.accum(); + self /*CARRY()*/ += other.carry(); + return self; } - void operator-=(const accumulator_type& other) + Derived& operator-=(const accumulator_type& other) { - *this -= other.accum(); - CARRY() -= other.carry(); + Derived& self = *(static_cast(this)); + self -= other.accum(); + self /*CARRY()*/ -= other.carry(); + return self; } accumulator_type operator*(const T& arg) const { @@ -99,11 +105,15 @@ struct Operators { } accumulator_type operator+(const accumulator_type& other) const { - return accumulator_type(ACCUM(const) + other.accum(), CARRY(const) + other.carry()); + Derived cpy = *(static_cast(this)); + cpy += other; + return cpy; } accumulator_type operator-(const accumulator_type& other) const { - return accumulator_type(ACCUM(const) - other.accum(), CARRY(const) - other.carry()); + Derived cpy = *(static_cast(this)); + cpy -= other; + return cpy; } accumulator_type operator+() const { @@ -114,26 +124,26 @@ struct Operators { return accumulator_type(-ACCUM(const), -CARRY(const)); } }; -#define SMMTNCXX_COMMUTATIVE_OP(OP) \ +#define SXX_COMMUTATIVE_OP(OP) \ template \ typename Derived::accumulator_type operator OP( \ const typename Derived::underlying_type& arg_a, const Derived& arg_b) \ { \ return arg_b OP arg_a; /* multiplication is commutative */ \ } -SMMTNCXX_COMMUTATIVE_OP(*) -SMMTNCXX_COMMUTATIVE_OP(+) -#define SMMTNCXX_PROMOTING_OP(OP) \ +SXX_COMMUTATIVE_OP(*) +SXX_COMMUTATIVE_OP(+) +#define SXX_PROMOTING_OP(OP) \ template \ typename Derived::accumulator_type operator OP( \ const typename Derived::underlying_type& arg_a, const Derived& arg_b) \ { \ return Derived { arg_a } OP arg_b; /* multiplication is commutative */ \ } -SMMTNCXX_PROMOTING_OP(-) +SXX_PROMOTING_OP(-) -#undef SMMTNCXX_COMMUTATIVE_OP -#undef SMMTNCXX_PROMOTING_OP +#undef SXX_COMMUTATIVE_OP +#undef SXX_PROMOTING_OP #undef ACCUM #undef CARRY diff --git a/pyodesys/native/sources/summation_cxx/macros.hpp b/pyodesys/native/sources/summation_cxx/macros.hpp index 3fb9f9ea..af6c9731 100644 --- a/pyodesys/native/sources/summation_cxx/macros.hpp +++ b/pyodesys/native/sources/summation_cxx/macros.hpp @@ -1,64 +1,67 @@ #pragma once -#ifndef SMMTNCXX_RESTRICT +#ifndef SXX_RESTRICT #if defined(__GNUC__) -#define SMMTNCXX_RESTRICT __restrict__ +#define SXX_RESTRICT __restrict__ #elif defined(_MSC_VER) && _MSC_VER >= 1400 -#define SMMTNCXX_RESTRICT __restrict +#define SXX_RESTRICT __restrict // #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L -// #define SMMTNCXX_RESTRICT restrict +// #define SXX_RESTRICT restrict #else -#define SMMTNCXX_RESTRICT +#define SXX_RESTRICT #endif #endif -#ifndef SMMTNCXX_PREFER_INLINE +#ifndef SXX_PREFER_INLINE #if defined(__GNUC__) -#define SMMTNCXX_PREFER_INLINE __attribute__((flatten)) +#define SXX_PREFER_INLINE __attribute__((flatten)) #elif defined(_MSC_VER) && _MSC_VER >= 1400 -#define SMMTNCXX_PREFER_INLINE __forceinline +#define SXX_PREFER_INLINE __forceinline #else -#define SMMTNCXX_PREFER_INLINE +#define SXX_PREFER_INLINE #endif #endif #if defined(__cpp_concepts) && __cpp_concepts >= 201907L -#define SMMTNCXX_FWD_IT_CONCEPT std::forward_iterator -#define SMMTNCXX_RND_IT_CONCEPT std::random_access_iterator +#include +#define SXX_FWD_IT_CONCEPT std::forward_iterator +#define SXX_RND_IT_CONCEPT std::random_access_iterator +#define SXX_UNSIGNED_INTEGRAL std::unsigned_integral #else -#define SMMTNCXX_FWD_IT_CONCEPT typename -#define SMMTNCXX_RND_IT_CONCEPT typename +#define SXX_FWD_IT_CONCEPT typename +#define SXX_RND_IT_CONCEPT typename +#define SXX_UNSIGNED_INTEGRAL typename #endif // Math macros to support e.g. __float128 without std lib support: -#ifndef SMMTNCXX_ABS -#define SMMTNCXX_ABS(x) (((x) < 0) ? -(x) : (x)) +#ifndef SXX_ABS +#define SXX_ABS(x) (((x) < 0) ? -(x) : (x)) #endif -#ifndef SMMTNCXX_NEUMAIER_BRANCH +#ifndef SXX_NEUMAIER_BRANCH // see test/bench.cpp -#define SMMTNCXX_NEUMAIER_BRANCH 1 +#define SXX_NEUMAIER_BRANCH 1 #endif -#if defined(SMMTNCXX_NEUMAIER_SWAP) +#if defined(SXX_NEUMAIER_SWAP) #error "API has changed, update your compilation flags accordingly" #endif -#define SMMTNCXX_SWP_TMP_(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ +#define SXX_SWP_TMP_(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ TYPE TMP; \ if (DO_SWAP) { \ - if (CARRY == 0 && ACCUM != 0 && SMMTNCXX_ABS(ELEM) > SMMTNCXX_ABS(ACCUM)) { \ - TMP = ACCUM; \ + if ((CARRY) == 0 && (ACCUM) != 0 && SXX_ABS(ELEM) > (1u<<20)*SXX_ABS(ACCUM)) { \ + TMP = ACCUM; \ ACCUM = CARRY; \ CARRY = TMP; \ } \ } \ TMP = (ACCUM) + (ELEM); -#if SMMTNCXX_NEUMAIER_BRANCH == 1 -#define SMMTNCXX_NEUMAIER_ADD(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ +#if SXX_NEUMAIER_BRANCH == 1 +#define SXX_NEUMAIER_ADD(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ do { \ - SMMTNCXX_SWP_TMP_(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ - if (SMMTNCXX_ABS(TMP) > SMMTNCXX_ABS(ELEM)) { \ + SXX_SWP_TMP_(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ + if (SXX_ABS(TMP) > SXX_ABS(ELEM)) { \ CARRY += TYPE { (ACCUM) - (TMP) } + (ELEM); \ } else { \ CARRY += TYPE { (ELEM) - (TMP) } + (ACCUM); \ @@ -66,16 +69,16 @@ ACCUM = (TMP); \ } while (0) #else -#define SMMTNCXX_NEUMAIER_ADD(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ +#define SXX_NEUMAIER_ADD(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ do { \ - SMMTNCXX_SWP_TMP_(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ - T SMMTNCXX_dat_[2] = { \ + SXX_SWP_TMP_(ACCUM, CARRY, ELEM, TYPE, TMP, DO_SWAP) \ + T SXX_dat_[2] = { \ T { (ELEM) - (TMP) } + (ACCUM), \ T { (ACCUM) - (TMP) } + (ELEM) \ }; \ - CARRY += SMMTNCXX_dat_[SMMTNCXX_ABS(TMP) > SMMTNCXX_ABS(ELEM)]; \ + CARRY += SXX_dat_[SXX_ABS(TMP) > SXX_ABS(ELEM)]; \ ACCUM = (TMP); \ } while (0) #endif -#undef SMMTNCXX_CXX_SWP_TMP_ -#define SMMTNCXX_NEUMAIER_FINALIZE(ACCUM, CARRY) ((ACCUM) + (CARRY)) +#undef SXX_CXX_SWP_TMP_ +#define SXX_NEUMAIER_FINALIZE(ACCUM, CARRY) ((ACCUM) + (CARRY)) diff --git a/pyodesys/native/sources/summation_cxx/ranged.hpp b/pyodesys/native/sources/summation_cxx/ranged.hpp new file mode 100644 index 00000000..f52f7706 --- /dev/null +++ b/pyodesys/native/sources/summation_cxx/ranged.hpp @@ -0,0 +1,122 @@ +#pragma once +#include +#include // std::memset +#include // std::make_unique +#include +#include +#include + +namespace summation_cxx { +template +struct RangedAccumulator { + typedef T underlying_type; + typedef std::conditional_t, T, U> target_type; + typedef Accumulator accumulator_type; + typedef AccuView view_type; + +private: + target_type* tgt {}; + std::unique_ptr storage {}; + std::size_t sz {}; + bool cumulative {}; + +public: + RangedAccumulator() = default; + RangedAccumulator(std::size_t sz) + : storage(std::make_unique(sz * 2)) + , sz(sz) + { + } + void init(target_type* target, bool cumulative = false) + { + tgt = target; + this->cumulative = cumulative; + if (sz > 0 /* UB to call memset over zero bytes. */) { + // doing this only makes sense if commit() is not always called. + std::memset(storage.get(), 0x00, sizeof(underlying_type) * sz * 2); + } + } + view_type operator[](std::size_t idx) + { + return view_type { &storage[idx * 2] }; + } + const view_type operator[](std::size_t idx) const + { + return view_type { &storage[idx * 2] }; + } + + void commit() const + { +#if defined(SUMMTNCXX_DISTRUST_OPTIMIZING_COMPILERS) +#define SXX_OUTPUT(OP) \ + if constexpr (scheme == Compensation::KAHAN) { \ + this->tgt[i] OP this->storage[i * 2]; \ + } else if constexpr (scheme == Compensation::NEUMAIER || + Derived::compensation_scheme == Compensation::NEUMAIER_SWAP) + { + this->tgt[i] OP this->storage[i * 2] + this->storage[i * 2 + 1]; + } + else + { + assert(false); + } +#else +#define SXX_OUTPUT(OP) this->tgt[i] OP(*this)[i].template to(); +#endif +#define SXX_LOOP \ + std::size_t i = 0; \ + i < this->sz; \ + ++i + if (cumulative) { + for (SXX_LOOP) { + SXX_OUTPUT(+=) + } + } else { + for (SXX_LOOP) { + SXX_OUTPUT(=) + } + } +#undef SXX_LOOP +#undef SXX_OUTPUT + } +}; +template +using RangedAccumulatorKahan = RangedAccumulator; +template +using RangedAccumulatorNeumaier = RangedAccumulator; +template +using RangedAccumulatorNeumaierSwap = RangedAccumulator; + +/// Simplifies writing generic code against ranged.hpp, no compensation: +template +struct RangedUncompensatedView { + typedef T underlying_type; + typedef std::conditional_t, T, U> target_type; + typedef T accumulator_type; + typedef target_type& view_type; + +protected: + target_type* tgt {}; + std::size_t sz {}; + +public: + RangedUncompensatedView() = default; + RangedUncompensatedView(std::size_t sz) + : sz(sz) + { + } + void init(target_type* target, bool cumulative = false) + { + tgt = target; + if (!cumulative && (sz > 0 /* UB to call memset over zero bytes. */)) { + // may be skipped e.g. if we know target is already zero-initialized + std::memset(target, 0x00, sizeof(target_type) * sz); + } + } + view_type operator[](std::size_t idx) + { + return tgt[idx]; + } + void commit() const { } // no-op +}; +} diff --git a/pyodesys/native/sources/summation_cxx/view.hpp b/pyodesys/native/sources/summation_cxx/view.hpp new file mode 100644 index 00000000..48eb272a --- /dev/null +++ b/pyodesys/native/sources/summation_cxx/view.hpp @@ -0,0 +1,41 @@ +#pragma once +#include + +namespace summation_cxx { + +template +struct AccuView : public detail::Operators> { + static constexpr Compensation compensation_scheme { scheme }; + +private: + T* ptr; + +public: + T& accum() { return ptr[0]; } + T& carry() { return ptr[1]; } + const T& accum() const { return ptr[0]; } + const T& carry() const { return ptr[1]; } + +public: + AccuView() = delete; + AccuView & operator=(const AccuView&) = delete; + using detail::Operators>::operator=; + // cppcheck-suppress noExplicitConstructor + AccuView(T* data) + : ptr(data) + { + assert(data); + } + Accumulator deepcopy() + { + return Accumulator { this->accum(), this->carry() }; + } +}; + +template +using AccuViewKahan = AccuView; +template +using AccuViewNeumaier = AccuView; +template +using AccuViewNeumaierSwap = AccuView; +} diff --git a/pyodesys/native/symcse/ordered_add.py b/pyodesys/native/symcse/ordered_add.py index 6fec35a8..221478a5 100644 --- a/pyodesys/native/symcse/ordered_add.py +++ b/pyodesys/native/symcse/ordered_add.py @@ -10,7 +10,7 @@ def ordered_add(*args): elif (len(args)) == 1: return args[0] a, b, *rest = args - ue = UnevaluatedRealPropagatingExpr(a+b) + ue = UnevaluatedRealPropagatingExpr(a + b) if len(rest) == 0: return ue else: diff --git a/pyodesys/native/tests/_tests.py b/pyodesys/native/tests/_tests.py index f4c21517..9755153d 100644 --- a/pyodesys/native/tests/_tests.py +++ b/pyodesys/native/tests/_tests.py @@ -13,6 +13,7 @@ ) from pyodesys.tests.bateman import bateman_full # analytic, never mind the details from pyodesys.tests.test_symbolic import decay_rhs, decay_dydt_factory, _get_decay3 +from pyodesys.native.symcse.util import BackendWithDisabledCSE sp = import_('sympy') @@ -100,7 +101,7 @@ class TransformedNativeSys(TransformedSys, NativeSys): def _test_Decay_nonnegative(NativeSys, use_cse, compensated): odesys = NativeSys.from_other( _get_decay3(lower_bounds=[0]*3), - native_code_kw=dict(use_cse=use_cse, compensated_summation=compensated), + native_code_kw=dict(groupwise_kw=None if use_cse else {'backend': BackendWithDisabledCSE()}, compensated_summation=compensated), ) y0, k = [3., 2., 1.], [3.5, 2.5, 0] xout, yout, info = odesys.integrate([1e-10, 1], y0, k, integrator='native') @@ -249,12 +250,14 @@ def _test_NativeSys__first_step_cb_source_code(NativeSys, log10myconst, should_s dec3 = _get_decay3() odesys = NativeSys.from_other( dec3, - namespace_override={ + native_code_kw=dict( + save_temp=True, + namespace_override={ 'p_first_step': 'AnyODE::ignore(x); return good_const()*y[0];', 'p_anon': 'double good_const(){ return std::pow(10, %.5g); }' % log10myconst }, - namespace_extend={'p_includes': ['']}, - native_code_kw=dict(save_temp=True), + namespace_extend={'p_includes': ['']} + ), ) y0, k = [.7, 0, 0], [1e23, 2, 3.] xout, yout, info = odesys.integrate(5, y0, k, integrator='native', **kwargs) @@ -285,9 +288,9 @@ def roots(t, y, p, backend): def _test_NativeSys__get_dx_max_source_code(NativeSys, forgive=20, **kwargs): dec3 = _get_decay3() - odesys = NativeSys.from_other(dec3, namespace_override={ + odesys = NativeSys.from_other(dec3, native_code_kw=dict(namespace_override={ 'p_get_dx_max': """AnyODE::ignore(y); return (1.0e-4 * x + 1.0e-3);""", - }) + })) y0, k = [.7, 0, 0], [7., 2, 3.] xout, yout, info = odesys.integrate(1, y0, k, integrator='native', get_dx_max_factor=1.0, **kwargs) @@ -331,7 +334,8 @@ def _test_NativeSys__dep_by_name__single_varied(NativeSys): def _test_return_on_error_success(NativeSys): k, y0 = [4, 3], (5, 4, 2) - native = NativeSys.from_callback(decay_rhs, len(k)+1, len(k), namespace_override={ + native = NativeSys.from_callback(decay_rhs, len(k)+1, len(k), native_code_kw=dict( + namespace_override={ 'p_rhs': """ f[0] = -m_p[0]*y[0]; f[1] = m_p[0]*y[0] - m_p[1]*y[1]; @@ -340,7 +344,7 @@ def _test_return_on_error_success(NativeSys): this->nfev++; return AnyODE::Status::success; """ - }) + })) xout = [0, 0.2, 0.4, 0.6, 0.8, 1.0] result = native.integrate(xout, y0, k, atol=1e-11, rtol=1e-11, return_on_error=True, dx_max=.05) nreached = result.info['nreached'] From ae1fb9928a682e967264cdf37cb95dc36f78cbcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 14 Jun 2022 18:02:43 +0200 Subject: [PATCH 056/102] excceded lower_bound -> violated lower_bound --- pyodesys/native/sources/odesys_anyode_template.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 7ad05b95..84a3514f 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -181,7 +181,7 @@ namespace odesys_anyode { if (y[i] < m_lower_bounds[i]) { std::cerr << "Lower bound (" << m_lower_bounds[0] << ") for " << (p_odesys_names.size() ? p_odesys_names[i] : std::to_string(i)) - << " exceeded (" << y[i] << ") at x="<< x << "\n"; + << " violated (" << y[i] << ") at x="<< x << "\n"; return AnyODE::Status::recoverable_error; } } @@ -191,7 +191,7 @@ namespace odesys_anyode { if (y[i] > m_upper_bounds[i]) { std::cerr << "Upper bound (" << m_upper_bounds[0] << ") for " << (p_odesys_names.size() ? p_odesys_names[i] : std::to_string(i)) - << " exceeded (" << y[i] << ") at x="<< x << "\n"; + << " violates (" << y[i] << ") at x="<< x << "\n"; return AnyODE::Status::recoverable_error; } } From ca74a6608230825f072fe1e9887babebd4e10ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Fri, 19 Aug 2022 15:27:16 +0200 Subject: [PATCH 057/102] added notebook --- examples/_predefined_subexpression.ipynb | 319 +++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 examples/_predefined_subexpression.ipynb diff --git a/examples/_predefined_subexpression.ipynb b/examples/_predefined_subexpression.ipynb new file mode 100644 index 00000000..db233de2 --- /dev/null +++ b/examples/_predefined_subexpression.ipynb @@ -0,0 +1,319 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "57f784db", + "metadata": {}, + "source": [ + "## Analytic solution\n", + "Consider this system of chemical reactions:\n", + "\n", + "
\n",
+    "A -> B; k=a*f(t)\n",
+    "B -> C; k=b*f(t)\n",
+    "\n",
+    "f(t) = 1/(1s + t)\n",
+    "0 <= t\n",
+    "
\n", + "\n", + "the system of ODEs is then defined as:\n", + "$$\n", + "\\frac{\\rm{d}A}{\\rm{d}t} = -a f(t) A \\\\\n", + "\\frac{\\rm{d}B}{\\rm{d}t} = -b f(t) B + a f(t) A \\\\\n", + "\\frac{\\rm{d}C}{\\rm{d}t} = +b f(t) B\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84232383", + "metadata": {}, + "outputs": [], + "source": [ + "from sympy import symbols, Function, Tuple, Eq, dsolve, cse" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "699f042d", + "metadata": {}, + "outputs": [], + "source": [ + "t, a, b, A0, B0, C0 = symbols('t a b A0 B0 C0', real=True, nonnegative=True)\n", + "funcs_anon = f, A, B, C = symbols('f A B C', cls=Function, nonnegative=True)\n", + "funcs_t = ft, At, Bt, Ct = [_(t) for _ in funcs_anon]\n", + "funcs_dt = dAdt, dBdt, dCdt = [_.diff(t) for _ in funcs_t[1:]]\n", + "e_ft = 1/(1+t)\n", + "r1 = a*e_ft*At\n", + "r2 = b*e_ft*Bt\n", + "rhss = e_dAdt, e_dBdt, e_dCdt = -r1, -r2 + r1, r2\n", + "odes3 = Tuple(*[Eq(_1, _2) for _1, _2 in zip(funcs_dt, rhss)])\n", + "odes3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d4618a7", + "metadata": {}, + "outputs": [], + "source": [ + "sA = dsolve(odes3[0], func=At, ics={A(0): A0})\n", + "assert sA.subs(t, 0).rhs - A0 == 0\n", + "assert sA.rhs.diff(t) - odes3[0].rhs.subs({At: sA.rhs}) == 0\n", + "sA" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e178347c", + "metadata": {}, + "outputs": [], + "source": [ + "odes2 = odes3[1:].subs({sA.lhs: sA.rhs})\n", + "odes2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d98b1e2f", + "metadata": {}, + "outputs": [], + "source": [ + "# SymPy's dsolve (effectively?) hangs for below input:\n", + "#sB = dsolve(odes2[0], func=Bt, ics={B(0): B0})\n", + "# so we prescribe the analytic solution directly:\n", + "_c1 = B0 - a*A0/(b-a)\n", + "sB = Eq(Bt, (a*A0*(t+1)**-a / (b-a) + _c1*(t+1)**-b))#.simplify())\n", + "assert sB.subs(t, 0).rhs - B0 == 0\n", + "assert sB.rhs.diff(t).expand().factor() - odes2[0].rhs.subs({Bt: sB.rhs}).expand().factor() == 0\n", + "sB" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fdf775c9", + "metadata": {}, + "outputs": [], + "source": [ + "sB.rhs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6edd3b7", + "metadata": {}, + "outputs": [], + "source": [ + "odes1 = odes2[1:].subs({sB.lhs: sB.rhs})\n", + "odes1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8acfbf96", + "metadata": {}, + "outputs": [], + "source": [ + "_sC = (t+1)**(-a-b)*(b*(B0*(t+1)**a + A0*(t+1)**b) - a*(t+1)**a*(A0+B0))/(a-b)\n", + "sC = Eq(Ct, (_sC + C0 - _sC.subs(t, 0)))\n", + "assert sC.subs(t, 0).rhs - C0 == 0\n", + "assert sC.rhs.diff(t).expand().factor() - odes1[0].rhs.subs({Ct: sC.rhs}).expand().factor() == 0\n", + "sC" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0693d6ca", + "metadata": {}, + "outputs": [], + "source": [ + "analytic_rhss = [sA.rhs, sB.rhs, sC.rhs]\n", + "cses, red = cse(analytic_rhss)\n", + "cses, red" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab3c476f", + "metadata": {}, + "outputs": [], + "source": [ + "[r.subs(cses[::-1]) - ref for r, ref in zip(red, analytic_rhss)]" + ] + }, + { + "cell_type": "markdown", + "id": "16662d12", + "metadata": {}, + "source": [ + "### Deferred below\n", + "Better CSEs but implementation not yet complete." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0901a71d", + "metadata": {}, + "outputs": [], + "source": [ + "from collections import defaultdict\n", + "\n", + "def my_cse(exprs):\n", + " cses, red = cse(exprs)\n", + " new_keys, new_values = [], []\n", + " backsubs = {}\n", + " denoms = defaultdict(list)\n", + " for x, se in cses:\n", + " _a, _b = se.as_coeff_Mul()\n", + " if _a == -1 and _b.is_Symbol:\n", + " backsubs[x] = se\n", + " continue\n", + " \n", + " #if -se in new_values:\n", + " # backsubs[x] = -new_keys[new_values.index(-se)]\n", + " # continue\n", + " \n", + " new_keys.append(x)\n", + " se = se.subs(backsubs)\n", + " new_values.append(se)\n", + " \n", + " numer, denom = se.as_numer_denom()\n", + " if not denom.is_number:\n", + " denoms[denom].append((x, numer))\n", + " taken = set()\n", + " for denom, pairs1 in denoms.items():\n", + " if denom in taken:\n", + " continue\n", + " pairs2 = denoms.get(-denom, None)\n", + " if pairs2 is None:\n", + " continue\n", + " else:\n", + " taken.add(-denom)\n", + " \n", + " def _cost(numer):\n", + " return int(numer.is_number), abs(numer) if numer.is_number else numer.count_ops()\n", + " pairs12 = (pairs1, pairs2)\n", + " scores = [sorted([(x, _cost(nur)) for x, nur in pairs], key=lambda _: _[1]) for pairs in pairs12]\n", + " x1, sc1 = scores[0][0]\n", + " x2, sc2 = scores[1][0]\n", + " use1 = sc1 < sc2\n", + " x = (x1, x2)[use1]\n", + " pairs12d = [dict(pairs) for pairs in pairs12]\n", + " print(pairs12d)\n", + " nur = pairs12d[use1][x]\n", + " kk, vv = [], []\n", + " for k, v in zip(new_keys, new_values):\n", + " kk.append(k)\n", + " if k != x and k in pairs12d[0]:\n", + " side = 0\n", + " elif k != x and k in pairs12d[1]:\n", + " side = 1\n", + " else:\n", + " vv.append(v)\n", + " continue\n", + " factor = -1 if (use1 ^ side) else 1\n", + " print(x, k, v, factor)\n", + " expr = factor*x*pairs12d[side][k]/pairs12d[use1][x]\n", + " print(expr)\n", + " vv.append(expr)\n", + " \n", + " new_keys = kk\n", + " new_values = vv\n", + " \n", + " return list(zip(new_keys, new_values)), [_.subs(backsubs) for _ in red]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5cac0790", + "metadata": {}, + "outputs": [], + "source": [ + "cses2, red2 = my_cse(analytic_rhss)\n", + "assert all(r.subs(cses2[::-1]) - ref == 0 for r, ref in zip(red2, analytic_rhss))\n", + "cses2, red2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b5dda788", + "metadata": {}, + "outputs": [], + "source": [ + "cses2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5206ab28", + "metadata": {}, + "outputs": [], + "source": [ + "numer, denom = cses2[3][1].as_numer_denom()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1a160f0", + "metadata": {}, + "outputs": [], + "source": [ + "denom.as_numer_denom()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3892e8d4", + "metadata": {}, + "outputs": [], + "source": [ + "denom.is_number" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18b1a73b", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From db10d3f1f326b0a4a0564fcc071f0276423d93ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sat, 20 Aug 2022 14:25:53 +0200 Subject: [PATCH 058/102] added another notebook --- examples/_predefined_subexpression.ipynb | 116 +++++++++++++++ examples/_predefined_subexpression2.ipynb | 173 ++++++++++++++++++++++ 2 files changed, 289 insertions(+) create mode 100644 examples/_predefined_subexpression2.ipynb diff --git a/examples/_predefined_subexpression.ipynb b/examples/_predefined_subexpression.ipynb index db233de2..a0fb6c45 100644 --- a/examples/_predefined_subexpression.ipynb +++ b/examples/_predefined_subexpression.ipynb @@ -151,6 +151,122 @@ "[r.subs(cses[::-1]) - ref for r, ref in zip(red, analytic_rhss)]" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a4652f8", + "metadata": {}, + "outputs": [], + "source": [ + "from pyodesys.symbolic import SymbolicSys\n", + "#help(SymbolicSys)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "466c59ed", + "metadata": {}, + "outputs": [], + "source": [ + "def mk_odesys(autonomous):\n", + " ss = tau, cA, cB, cC = symbols('tau c_A c_B c_C')\n", + " _subs = {At: cA, Bt: cB, Ct: cC, t: tau}\n", + " exprs = [(cs, e.subs(_subs)) for cs, e in zip(ss[1:], rhss)]\n", + " names = \"[A] [B] [C]\".split()\n", + " if autonomous:\n", + " exprs += [(tau,1)]\n", + " names += [\"tau\"]\n", + " indep = None\n", + " else:\n", + " indep = tau\n", + " odesys = SymbolicSys(exprs, indep, params=[a,b], names=names)\n", + " if autonomous:\n", + " assert odesys.autonomous_exprs\n", + " return odesys" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5089cd51", + "metadata": {}, + "outputs": [], + "source": [ + "odesystems = [mk_odesys(auto) for auto in [False, True]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ec0229f", + "metadata": {}, + "outputs": [], + "source": [ + "ics = [1,0,0]\n", + "pvals = [1.3e5, 2.7]\n", + "res = []\n", + "int_kw = dict(integrator='cvode', atol=1e-12, rtol=1e-14, nsteps=5000)\n", + "for auto, odesys in enumerate(odesystems):\n", + " res.append(odesys.integrate((0,1), ics + ([0] if auto else []), pvals, **int_kw))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86db8205", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2677b792", + "metadata": {}, + "outputs": [], + "source": [ + "from pyodesys.plotting import plot_result\n", + "#help(plot_result)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a041c101", + "metadata": {}, + "outputs": [], + "source": [ + "from sympy import lambdify\n", + "ref = lambdify([t, A0, B0, C0, a, b], analytic_rhss)\n", + "ref_vals = [ref(r.xout, *ics[:3], *pvals) for r in res]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "129dff5a", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "fig, all_axes = plt.subplots(2, 2, figsize=(16, 8), dpi=200)\n", + "for i, r in enumerate(res):\n", + " r.plot(ax=all_axes[i, 0]\n", + " #, info_vlines_kw=True\n", + " )\n", + " for j, rv in enumerate(ref_vals[i]):\n", + " all_axes[i, 1].plot(r.xout, rv - r.yout[:, j], label=\"error in [%s]\" % \"ABC\"[j])\n", + " all_axes[i, 1].legend()\n", + " #axes[1].set_xlim(res.xout[[0,-1]])\n", + " #axes[1].vlines(res.xout, 0, 1e-13, transform=axes[1].get_yaxis_transform(), linewidth=0.5)\n", + " for ax in all_axes[i, :]:\n", + " ax.set_xscale('log')\n", + " print({k: v for k,v in r.info.items() if not k.startswith('internal')})" + ] + }, { "cell_type": "markdown", "id": "16662d12", diff --git a/examples/_predefined_subexpression2.ipynb b/examples/_predefined_subexpression2.ipynb new file mode 100644 index 00000000..4e79c61a --- /dev/null +++ b/examples/_predefined_subexpression2.ipynb @@ -0,0 +1,173 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "57f784db", + "metadata": {}, + "source": [ + "## Analytic solution\n", + "Consider this system of chemical reactions:\n", + "\n", + "
\n",
+    "2 A -> B; r1=a*f(t)*A*A\n",
+    "2 B -> C; r2=b*f(t)*B*B\n",
+    "\n",
+    "f(t) = 1/(k + t)\n",
+    "0 <= t\n",
+    "
\n", + "\n", + "the system of ODEs is then defined as:\n", + "$$\n", + "\\frac{\\rm{d}A}{\\rm{d}t} = -2 a f(t) A^2 \\\\\n", + "\\frac{\\rm{d}B}{\\rm{d}t} = -2 b f(t) B^2 + a f(t) A^2 \\\\\n", + "\\frac{\\rm{d}C}{\\rm{d}t} = +b f(t) B^2\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84232383", + "metadata": {}, + "outputs": [], + "source": [ + "from sympy import symbols, Function, Tuple, Eq, dsolve, cse" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "699f042d", + "metadata": {}, + "outputs": [], + "source": [ + "t, a, b, A0, B0, C0, k = symbols('t a b A0 B0 C0 k', real=True, nonnegative=True)\n", + "funcs_anon = f, A, B, C = symbols('f A B C', cls=Function, nonnegative=True)\n", + "funcs_t = ft, At, Bt, Ct = [_(t) for _ in funcs_anon]\n", + "funcs_dt = dAdt, dBdt, dCdt = [_.diff(t) for _ in funcs_t[1:]]\n", + "e_ft = 1/(k+t)\n", + "r1 = a*e_ft*At**2\n", + "r2 = b*e_ft*Bt**2\n", + "rhss = e_dAdt, e_dBdt, e_dCdt = -2*r1, -2*r2 + r1, r2\n", + "odes3 = Tuple(*[Eq(_1, _2) for _1, _2 in zip(funcs_dt, rhss)])\n", + "odes3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d4618a7", + "metadata": {}, + "outputs": [], + "source": [ + "sA = dsolve(odes3[0], func=At, ics={A(0): A0})\n", + "assert sA.subs(t, 0).rhs.expand() - A0 == 0\n", + "assert sA.rhs.diff(t) - odes3[0].rhs.subs({At: sA.rhs}) == 0\n", + "sA" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e4f8556", + "metadata": {}, + "outputs": [], + "source": [ + "sA.subs(t, 0).expand()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e178347c", + "metadata": {}, + "outputs": [], + "source": [ + "odes2 = odes3[1:].subs({sA.lhs: sA.rhs})\n", + "odes2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c405d444", + "metadata": {}, + "outputs": [], + "source": [ + "from sympy import sqrt, S, log" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d98b1e2f", + "metadata": {}, + "outputs": [], + "source": [ + "# we prescribe the analytic solution directly:\n", + "c1 = symbols('c1')\n", + "_1a = 1/(sqrt(2)*(k+t))\n", + "_1b = sqrt(a)*A0*sqrt(b)*c1\n", + "_1c = -(sqrt(2*a/b)+sqrt(2*(a-2*b)/b))\n", + "_1d = (-2*a*A0*log(k+t)+2*a*A0*log(k)+1)\n", + "_1e = -(sqrt(b)*(-sqrt(2*a/b)-sqrt(2*(a-2*b)/b)))/(2*sqrt(2)*sqrt(a))-1\n", + "_1 = _1a*_1b*_1c*_1d**_1e\n", + "_2 = A0/(k+t)*(sqrt(a*(a-2*b)) - a)\n", + "_3a = (-2*a*A0*log(k+t)+2*a*A0*log(k)+1)**(-(sqrt(b)*(-sqrt(2*a/b)-sqrt(2*(a-2*b)/b)))/sqrt(8*a))\n", + "_3b1 = -2*a*A0*log(k+t)+2*a*A0*log(k)+1\n", + "_3b2 = S(1)/2 - sqrt(a-2*b)/2/sqrt(a)\n", + "_3b = _3b1**_3b2\n", + "_3 = 2*b*(c1*(_3a + _3b))\n", + "_sB = ((k+t)*(_1+_2))/_3\n", + "_sB" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6255ddd7", + "metadata": {}, + "outputs": [], + "source": [ + "_sB.subs(t,0).expand().factor().simplify()" + ] + }, + { + "cell_type": "markdown", + "id": "ef75848a", + "metadata": {}, + "source": [ + "There's probably a typo in the analytic solution... not worth the time to investigate now." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f686b95", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From b069badd68151350d4d7704f7740325c04b84157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 25 Oct 2022 13:27:40 +0200 Subject: [PATCH 059/102] import Iterable from collections.abc --- pyodesys/native/sources/_cvode_wrapper.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyodesys/native/sources/_cvode_wrapper.pyx b/pyodesys/native/sources/_cvode_wrapper.pyx index 8242e23d..f82cc1da 100644 --- a/pyodesys/native/sources/_cvode_wrapper.pyx +++ b/pyodesys/native/sources/_cvode_wrapper.pyx @@ -5,7 +5,7 @@ # distutils: extra_link_args = -fopenmp # cython: language_level=3str -from collections import Iterable +from collections.abc import Iterable from libc.stdlib cimport malloc, free from libcpp cimport bool From 3bd63cbe9b50f3391410d14bcc638f7bcb8a33ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 31 Oct 2022 19:22:01 +0100 Subject: [PATCH 060/102] updates --- pyodesys/native/_base.py | 13 ++++++++++--- pyodesys/native/cvode.py | 17 +++++++++-------- pyodesys/native/gsl.py | 4 ++-- pyodesys/native/odeint.py | 4 ++-- pyodesys/native/symcse/compensated.py | 2 +- pyodesys/native/symcse/util.py | 2 +- pyodesys/native/tests/_tests.py | 10 ++++++---- 7 files changed, 31 insertions(+), 21 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 02ad9522..eb801bcf 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -60,6 +60,9 @@ def get_compile_kwargs(kwargs): kw[k].extend(kwargs.pop(k)) else: kw[k] = kwargs.pop(k) + else: + if k not in kw: + kw[k] = [] if options := os.environ.get("PYODESYS_OPTIONS"): kw['options'] = options.split(',') @@ -115,7 +118,7 @@ class _NativeCodeBase(Cpp_Code): namespace_default = {'p_anon': None} namespace = { - 'p_includes': ['"odesys_anyode.hpp"'], + 'p_includes': {'"odesys_anyode.hpp"'}, 'p_support_recoverable_error': False, 'p_jacobian_set_to_zero_by_solver': False, 'p_realtype': 'double', @@ -160,7 +163,6 @@ def __init__(self, odesys, *args, groupwise_kw=None, assigner_kws=None, prebuild = {_wrapper_src: _wrapper_obj} self.build_files = self.build_files + tuple(prebuild.values()) - self.odesys = odesys for _src, _dest in prebuild.items(): if not os.path.exists(_dest): @@ -301,7 +303,12 @@ def _cses(k): ns.update(self.namespace) ns.update(self.namespace_override) for k, v in self.namespace_extend.items(): - ns[k].extend(v) + if isinstance(ns[k], list): + ns[k] = ns[k] + v + elif isinstance(ns[k], set): + ns[k] = ns[k] | v + else: + raise NotImplementedError(f"Cannot extend {k} of type {type(k)}") return ns diff --git a/pyodesys/native/cvode.py b/pyodesys/native/cvode.py index f15b9058..402590eb 100644 --- a/pyodesys/native/cvode.py +++ b/pyodesys/native/cvode.py @@ -26,7 +26,7 @@ class NativeCvodeCode(_NativeCodeBase): _indextype = '#error "indextype_failed-to-import-pycvodes-or-too-old-version"' namespace = { - 'p_includes': ['"odesys_anyode_iterative.hpp"'], + 'p_includes': {'"odesys_anyode_iterative.hpp"'}, 'p_support_recoverable_error': True, 'p_jacobian_set_to_zero_by_solver': True, 'p_baseclass': 'OdeSysIterativeBase', @@ -37,15 +37,16 @@ class NativeCvodeCode(_NativeCodeBase): def __init__(self, *args, **kwargs): self.compile_kwargs = get_compile_kwargs(kwargs) - self.compile_kwargs['define'] = ['PYCVODES_NO_KLU={}'.format("0" if config.get('KLU', True) else "1"), - 'PYCVODES_NO_LAPACK={}'.format("0" if config.get('LAPACK', True) else "1"), - 'ANYODE_NO_LAPACK={}'.format("0" if config.get('LAPACK', True) else "1")] + self.compile_kwargs['define'].extend([ + 'PYCVODES_NO_KLU={}'.format("0" if config.get('KLU', True) else "1"), + 'PYCVODES_NO_LAPACK={}'.format("0" if config.get('LAPACK', True) else "1"), + 'ANYODE_NO_LAPACK={}'.format("0" if config.get('LAPACK', True) else "1") + ]) self.compile_kwargs['include_dirs'].append(get_include()) - self.compile_kwargs['libraries'].extend(_libs.get_libs().split(',')) - self.compile_kwargs['libraries'].extend([l for l in os.environ.get( + self.compile_kwargs['libraries'].extend(_libs.get_libs().split(',') + [l for l in os.environ.get( 'PYODESYS_LAPACK', "lapack,blas" if config["LAPACK"] else "").split(",") if l != ""]) - self.compile_kwargs['flags'] = [f for f in os.environ.get("PYODESYS_CVODE_FLAGS", "").split() if f] - self.compile_kwargs['ldflags'] = [f for f in os.environ.get("PYODESYS_CVODE_LDFLAGS", "").split() if f] + self.compile_kwargs['flags'].extend([f for f in os.environ.get("PYODESYS_CVODE_FLAGS", "").split() if f]) + self.compile_kwargs['ldflags'].extend([f for f in os.environ.get("PYODESYS_CVODE_LDFLAGS", "").split() if f]) super(NativeCvodeCode, self).__init__(*args, **kwargs) diff --git a/pyodesys/native/gsl.py b/pyodesys/native/gsl.py index 62bbbb6a..94e2a3de 100644 --- a/pyodesys/native/gsl.py +++ b/pyodesys/native/gsl.py @@ -4,7 +4,7 @@ import copy import os from ..util import import_ -from ._base import _NativeCodeBase, _NativeSysBase, _compile_kwargs +from ._base import _NativeCodeBase, _NativeSysBase, get_compile_kwargs _config, get_include = import_('pygslodeiv2', '_config', 'get_include') @@ -14,7 +14,7 @@ class NativeGSLCode(_NativeCodeBase): wrapper_name = '_gsl_wrapper' def __init__(self, *args, **kwargs): - self.compile_kwargs = copy.deepcopy(_compile_kwargs) + self.compile_kwargs = get_compile_kwargs(kwargs) self.compile_kwargs['include_dirs'].append(get_include()) self.compile_kwargs['libraries'].extend(_config.env['GSL_LIBS'].split(',')) self.compile_kwargs['libraries'].extend(os.environ.get('PYODESYS_BLAS', _config.env['BLAS']).split(',')) diff --git a/pyodesys/native/odeint.py b/pyodesys/native/odeint.py index a122f9bd..9dd41eea 100644 --- a/pyodesys/native/odeint.py +++ b/pyodesys/native/odeint.py @@ -4,7 +4,7 @@ import copy from ..util import import_ -from ._base import _NativeCodeBase, _NativeSysBase, _compile_kwargs +from ._base import _NativeCodeBase, _NativeSysBase, get_compile_kwargs pyodeint = import_('pyodeint') @@ -13,7 +13,7 @@ class NativeOdeintCode(_NativeCodeBase): wrapper_name = '_odeint_wrapper' def __init__(self, *args, **kwargs): - self.compile_kwargs = copy.deepcopy(_compile_kwargs) + self.compile_kwargs = get_compile_kwargs(kwargs) self.compile_kwargs['include_dirs'].append(pyodeint.get_include()) self.compile_kwargs['libraries'].extend(['m']) super(NativeOdeintCode, self).__init__(*args, **kwargs) diff --git a/pyodesys/native/symcse/compensated.py b/pyodesys/native/symcse/compensated.py index 3bc0c842..1e7cea04 100644 --- a/pyodesys/native/symcse/compensated.py +++ b/pyodesys/native/symcse/compensated.py @@ -38,7 +38,7 @@ def If(cond, body): class _NeumaierAdd(Token, Expr): """Represents KBN compensated summation.""" - __slots__ = ("terms", "accum", "carry", "temp") + _fields = __slots__ = ("terms", "accum", "carry", "temp") _construct_terms = staticmethod(lambda args: Tuple(*args)) def _ccode(self, printer): diff --git a/pyodesys/native/symcse/util.py b/pyodesys/native/symcse/util.py index 252b37eb..6803abe2 100644 --- a/pyodesys/native/symcse/util.py +++ b/pyodesys/native/symcse/util.py @@ -46,7 +46,7 @@ def _eval_is_real(self): return self.args[0].is_real class OperatorDot(Token): - __slots__ = ('lhs', 'rhs') + _fields = __slots__ = ('lhs', 'rhs') #_construct_lhs = String #_construct_rhs = String diff --git a/pyodesys/native/tests/_tests.py b/pyodesys/native/tests/_tests.py index 9755153d..244386dc 100644 --- a/pyodesys/native/tests/_tests.py +++ b/pyodesys/native/tests/_tests.py @@ -253,10 +253,12 @@ def _test_NativeSys__first_step_cb_source_code(NativeSys, log10myconst, should_s native_code_kw=dict( save_temp=True, namespace_override={ - 'p_first_step': 'AnyODE::ignore(x); return good_const()*y[0];', - 'p_anon': 'double good_const(){ return std::pow(10, %.5g); }' % log10myconst - }, - namespace_extend={'p_includes': ['']} + 'p_first_step': 'AnyODE::ignore(x); return good_const()*y[0];', + 'p_anon': 'double good_const(){ return std::pow(10, %.5g); }' % log10myconst + }, + namespace_extend={ + 'p_includes': {''} + } ), ) y0, k = [.7, 0, 0], [1e23, 2, 3.] From d2d2fe532204ba263a714b3dd6f9b29f6d9b71f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 14 Nov 2022 11:39:39 +0100 Subject: [PATCH 061/102] Update summation-cxx --- .../sources/summation_cxx/accumulator.hpp | 3 + .../sources/summation_cxx/compensated.hpp | 38 +++++- .../native/sources/summation_cxx/impl.hpp | 22 +++- .../native/sources/summation_cxx/ranged.hpp | 4 + .../native/sources/summation_cxx/view.hpp | 4 + pyodesys/native/symcse/compensated.py | 114 +++++++++++++----- pyodesys/native/symcse/groupwise.py | 4 +- pyodesys/native/symcse/util.py | 8 +- 8 files changed, 159 insertions(+), 38 deletions(-) diff --git a/pyodesys/native/sources/summation_cxx/accumulator.hpp b/pyodesys/native/sources/summation_cxx/accumulator.hpp index a06fbafb..ce6200f3 100644 --- a/pyodesys/native/sources/summation_cxx/accumulator.hpp +++ b/pyodesys/native/sources/summation_cxx/accumulator.hpp @@ -77,6 +77,9 @@ template using AccumulatorNeumaier = Accumulator; template using AccumulatorNeumaierSwap = Accumulator; +template +using AccumulatorTwoSum = Accumulator; + template T pow(const Accumulator& base, T exponent) diff --git a/pyodesys/native/sources/summation_cxx/compensated.hpp b/pyodesys/native/sources/summation_cxx/compensated.hpp index d5c07709..f6e79ed6 100644 --- a/pyodesys/native/sources/summation_cxx/compensated.hpp +++ b/pyodesys/native/sources/summation_cxx/compensated.hpp @@ -6,10 +6,14 @@ #include // std::size_t namespace summation_cxx { -enum class Compensation { NONE, - KAHAN, +enum class Compensation { + NONE, + KAHAN, // should be equivalent to FAST_TWO_SUM NEUMAIER, - NEUMAIER_SWAP }; + NEUMAIER_SWAP, + TWO_SUM, + FAST_TWO_SUM +}; namespace /* anonymous */ { template @@ -51,5 +55,33 @@ namespace /* anonymous */ { // cppcheck-suppress redundantAssignment SXX_NEUMAIER_ADD(acm, carry, elem, T, tmp, true); } + + template + SXX_PREFER_INLINE void accum_two_sum( + T& SXX_RESTRICT accu, + T& SXX_RESTRICT carry, + const T& SXX_RESTRICT elem) + { + const T s = accu + elem; + const T ap = s - elem; + const T bp = s - ap; + const T da = accu - ap; + const T db = elem - bp; + carry += da + db; + accu = s; + } + + template + SXX_PREFER_INLINE void accum_fast_two_sum( + T& SXX_RESTRICT accu, + T& SXX_RESTRICT carry, + const T& SXX_RESTRICT elem) + { + const T s = accu + elem; + const T z = s - accu; + const T t = elem - z; + carry += t; + accu = s; + } } } diff --git a/pyodesys/native/sources/summation_cxx/impl.hpp b/pyodesys/native/sources/summation_cxx/impl.hpp index a7d1d408..d46664d8 100644 --- a/pyodesys/native/sources/summation_cxx/impl.hpp +++ b/pyodesys/native/sources/summation_cxx/impl.hpp @@ -27,7 +27,11 @@ struct Operators { { if constexpr (Derived::compensation_scheme == Compensation::KAHAN) { return ACCUM(const); - } else if constexpr (Derived::compensation_scheme == Compensation::NEUMAIER || Derived::compensation_scheme == Compensation::NEUMAIER_SWAP) { + } else if constexpr (Derived::compensation_scheme == Compensation::NEUMAIER + || Derived::compensation_scheme == Compensation::NEUMAIER_SWAP + || Derived::compensation_scheme == Compensation::TWO_SUM + || Derived::compensation_scheme == Compensation::FAST_TWO_SUM + ) { if constexpr (sizeof(T) > sizeof(U)) { return ACCUM(const) + CARRY(const); } else { @@ -46,6 +50,10 @@ struct Operators { accum_neumaier(ACCUM(), CARRY(), arg); } else if constexpr (scheme == Compensation::NEUMAIER_SWAP) { accum_neumaier_swap(ACCUM(), CARRY(), arg); + } else if constexpr (scheme == Compensation::TWO_SUM) { + accum_two_sum(ACCUM(), CARRY(), arg); + } else if constexpr (scheme == Compensation::FAST_TWO_SUM) { + accum_fast_two_sum(ACCUM(), CARRY(), arg); } else { assert(false); } @@ -72,8 +80,10 @@ struct Operators { } void operator*=(const T& arg) { + const T ori {ACCUM()}; ACCUM() *= arg; CARRY() *= arg; + CARRY() += fma(ori, arg, -ACCUM()); // 2product } Derived& operator+=(const accumulator_type& other) { @@ -91,7 +101,9 @@ struct Operators { } accumulator_type operator*(const T& arg) const { - return accumulator_type(ACCUM(const) * arg, CARRY(const) * arg); + Derived cpy = *(static_cast(this)); + cpy *= arg; + return cpy; } accumulator_type operator*(const accumulator_type& other) const { @@ -109,6 +121,12 @@ struct Operators { cpy += other; return cpy; } + accumulator_type operator+(const T& arg) const + { + Derived cpy = *(static_cast(this)); + cpy += arg; + return cpy; + } accumulator_type operator-(const accumulator_type& other) const { Derived cpy = *(static_cast(this)); diff --git a/pyodesys/native/sources/summation_cxx/ranged.hpp b/pyodesys/native/sources/summation_cxx/ranged.hpp index f52f7706..464842e4 100644 --- a/pyodesys/native/sources/summation_cxx/ranged.hpp +++ b/pyodesys/native/sources/summation_cxx/ranged.hpp @@ -86,6 +86,10 @@ template using RangedAccumulatorNeumaier = RangedAccumulator; template using RangedAccumulatorNeumaierSwap = RangedAccumulator; +template +using RangedAccumulatorTwoSum = RangedAccumulator; +template +using RangedAccumulatorFastTwoSum = RangedAccumulator; /// Simplifies writing generic code against ranged.hpp, no compensation: template diff --git a/pyodesys/native/sources/summation_cxx/view.hpp b/pyodesys/native/sources/summation_cxx/view.hpp index 48eb272a..3a0228c9 100644 --- a/pyodesys/native/sources/summation_cxx/view.hpp +++ b/pyodesys/native/sources/summation_cxx/view.hpp @@ -38,4 +38,8 @@ template using AccuViewNeumaier = AccuView; template using AccuViewNeumaierSwap = AccuView; +template +using AccuViewTwoSum = AccuView; +template +using AccuViewFastTwoSum = AccuView; } diff --git a/pyodesys/native/symcse/compensated.py b/pyodesys/native/symcse/compensated.py index 1e7cea04..10f07496 100644 --- a/pyodesys/native/symcse/compensated.py +++ b/pyodesys/native/symcse/compensated.py @@ -3,7 +3,8 @@ Example usage: -$ python3 compensated_cse.py demo1 | clang-format --style=Google | batcat -pl C +$ python3 -m symcse demo-compensated-py +$ python3 -m symcse demo-compensated-c --index 3 | clang-format --style=Google | batcat -pl C """ from collections import defaultdict @@ -34,8 +35,7 @@ def If(cond, body): return While(cond, CodeBlock(*body, break_)) - -class _NeumaierAdd(Token, Expr): +class _CompensatedAdd(Token, Expr): """Represents KBN compensated summation.""" _fields = __slots__ = ("terms", "accum", "carry", "temp") @@ -45,7 +45,7 @@ def _ccode(self, printer): terms = ", ".join(map(printer._print, self.terms)) return f"NA({terms} /*{str(self.accum)[:-1]}*/)" - def to_statements(self, existing, expanded, *, transients, do_swap=False): + def to_statements(self, existing, expanded, *, transients, **kwargs): """Transform into statements.""" neum, ordinary = [], [] for term in self.terms: @@ -100,7 +100,7 @@ def to_statements(self, existing, expanded, *, transients, do_swap=False): st.append(Assignment(tr, elem)) elem = tr st.extend( - _NeumaierAdd._impl_add(self.accum, self.carry, elem, self.temp, do_swap) + self._impl_add(self.accum, self.carry, elem, self.temp, **kwargs) ) expanded.add(self) return st @@ -109,6 +109,13 @@ def finalize(self): """Close the summation.""" return self._impl_finalize(self.accum, self.carry) + @staticmethod + def _impl_finalize(accum, carry): + return Add(accum, carry) + +class _NeumaierAdd(_CompensatedAdd): + _impl_add_kws = ('do_swap',) + @staticmethod def _impl_add(accum, carry, elem, temp, do_swap=False): """Perform Kahan-Babuska-Neumaier addition.""" @@ -135,12 +142,40 @@ def _impl_add(accum, carry, elem, temp, do_swap=False): else: return statements + +class _TwoSumAdd(_CompensatedAdd): + _impl_add_kws = () + @staticmethod - def _impl_finalize(accum, carry): - return Add(accum, carry) + def _impl_add(accum, carry, elem, temp): + """Perform 2Sum addition.""" + a_prim = ordered_add(temp, -elem) + b_prim = ordered_add(temp, -a_prim) + delta_a = ordered_add(accum, -a_prim) + delta_b = ordered_add(elem, -b_prim) + statements = [ + Assignment(temp, accum + elem), + aug_assign(carry, "+", ordered_add(delta_a, delta_b)), + Assignment(accum, temp) + ] + return statements + +class _FastTwoSumAdd(_CompensatedAdd): + _impl_add_kws = () -class NeumaierTransformer(NullTransformer): + @staticmethod + def _impl_add(accum, carry, elem, temp): + """Perform Fast2Sum addition.""" + statements = [ + Assignment(temp, ordered_add(accum, elem)), + aug_assign(carry, "+", ordered_add(elem, -ordered_add(temp, -accum))), + Assignment(accum, temp), + ] + return statements + + +class _CompensationTransformer(NullTransformer): """Transform Add instances in CSEs to use compensated sum. Parameters @@ -150,6 +185,8 @@ class NeumaierTransformer(NullTransformer): and 100 (all passes). """ + _CompAdd = None # Token + def __init__( self, repl, @@ -157,12 +194,12 @@ def __init__( *, tmp_pfx="t", trs_pfx="r", - neu_pfx="n", + csum_pfx="n", up_to_debug=100, limit=3, parent=None, ignore=None, - do_swap=False, + kw_to_statements=None ): self.repl = repl self.red = red @@ -178,7 +215,7 @@ def __init__( self._analysis = defaultdict(int) self._tmp_var = numbered_symbols(tmp_pfx) self._trs_var = numbered_symbols(trs_pfx) - self._neu_var = numbered_symbols(neu_pfx) + self._csum_var = numbered_symbols(csum_pfx) self.passes = [] for p in filter(lambda n: n.startswith("_pass_"), dir(self)): null, rest = p.split("_pass_") @@ -189,7 +226,7 @@ def __init__( if int(num) <= up_to_debug: self.passes.append(getattr(self, p)) - self.do_swap = do_swap + self.kw_to_statements = kw_to_statements or {} self.statements, self.final_exprs = self._pipeline() def remapping_for_arrayification(self, template="m_glob[{0}]"): @@ -203,20 +240,20 @@ def remapping_for_arrayification(self, template="m_glob[{0}]"): i = i + 1 return remapping - def _mk_Neu(self, terms, lhs): + def _mk_Comp(self, terms, lhs): pfx = str(next(self._tmp_var)) if lhs is None else str(lhs) accum = Symbol(pfx + "a", real=True) carry = Symbol(pfx + "c", real=True) tempv = Symbol(pfx + "t", real=True) - na = _NeumaierAdd(terms, accum, carry, tempv) + na = self._CompAdd(terms, accum, carry, tempv) self._all_accum[accum] = na self._all_carry[carry] = na self._all_tempv[tempv] = na return na - @staticmethod - def _is_Neu(x): - return isinstance(x, _NeumaierAdd) + @classmethod + def _is_Comp(cls, x): + return isinstance(x, cls._CompAdd) def _single_pass(self, statements, pass_): new_stmts = [] @@ -265,13 +302,13 @@ def _pass_10_create_nodes(self, lhs, rhs, *, statements, debug=False): score = self._analysis.get(lhs, 0) + reduce( add, [self._analysis.get(k, 1) for k in _add.args] ) - if score >= self.limit or any(self._is_Neu(arg) for arg in _add.args): - na = self._mk_Neu(_add.args, lhs) + if score >= self.limit or any(self._is_Comp(arg) for arg in _add.args): + na = self._mk_Comp(_add.args, lhs) if _add is rhs and lhs is not None: key = lhs else: - key = next(self._neu_var) + key = next(self._csum_var) self.created[key] = na new_rhs = new_rhs.xreplace({_add: key}) break @@ -281,17 +318,17 @@ def _pass_10_create_nodes(self, lhs, rhs, *, statements, debug=False): assert False def _pass_50_to_stmnts(self, lhs, rhs, *, statements): - for neu in map(self.created.get, postorder_traversal(rhs)): - if neu is None: + for csum in map(self.created.get, postorder_traversal(rhs)): + if csum is None: continue - self._pass_50_to_stmnts(lhs, neu.terms, statements=statements) - if neu not in self.expanded: + self._pass_50_to_stmnts(lhs, csum.terms, statements=statements) + if csum not in self.expanded: statements.extend( - neu.to_statements( + csum.to_statements( self.created, self.expanded, - do_swap=self.do_swap, transients=self._trs_var, + **self.kw_to_statements ) ) return rhs @@ -333,4 +370,27 @@ def _pass_95_group(self, lhs, rhs, *, statements): return new_rhs def _pass_90_fin(self, lhs, rhs, *, statements): - return rhs.replace(lambda x: self._is_Neu(x), lambda x: x.finalize()) + return rhs.replace(lambda x: self._is_Comp(x), lambda x: x.finalize()) + + +class NeumaierTransformer(_CompensationTransformer): + _CompAdd = _NeumaierAdd + + def __init__(self, *args, do_swap=False, **kwargs): + #kw_to_statements = kwargs.pop(kw_to_statements, {}) + # ... + super().__init__(*args, kw_to_statements=dict(do_swap=do_swap), **kwargs) + +class TwoSumTransformer(_CompensationTransformer): + _CompAdd = _TwoSumAdd + +class FastTwoSumTransformer(_CompensationTransformer): + _CompAdd = _FastTwoSumAdd + + +compensated_transformers = { + 'nocomp': NullTransformer, + 'kbn': NeumaierTransformer, + '2sum': TwoSumTransformer, + 'fast2sum': FastTwoSumTransformer, +} diff --git a/pyodesys/native/symcse/groupwise.py b/pyodesys/native/symcse/groupwise.py index 6b1ac4af..0487afaa 100644 --- a/pyodesys/native/symcse/groupwise.py +++ b/pyodesys/native/symcse/groupwise.py @@ -11,7 +11,7 @@ from sympy.codegen.rewriting import create_expand_pow_optimization from .core import NullTransformer -from .util import CPrinter, Backend, ccode +from .util import CPrinter, SymbolicBackend, ccode expand_up_to_3 = create_expand_pow_optimization(3) @@ -64,7 +64,7 @@ def __init__( self._keys, _values = zip(*groups.items()) self._spans = np.cumsum([0] + list(map(len, _values))) if backend is None: - backend = Backend() + backend = SymbolicBackend() self.backend = backend _all_values = reduce(add, map(list, _values)) _all_exprs = list(map(pre_process, _all_values) if pre_process else _all_values) diff --git a/pyodesys/native/symcse/util.py b/pyodesys/native/symcse/util.py index 6803abe2..5f9c9e09 100644 --- a/pyodesys/native/symcse/util.py +++ b/pyodesys/native/symcse/util.py @@ -157,7 +157,7 @@ def is_only_ig(e): return repl, red -class Backend: +class SymbolicBackend: """Allow optional use of SymEngine.""" def __init__(self, use_symengine=None, kw_cse=None, assume_real=True): @@ -170,7 +170,7 @@ def __init__(self, use_symengine=None, kw_cse=None, assume_real=True): """ if use_symengine is None: - _req_backend = os.environ.get("SYMCSE_BACKEND", "").lower() + _req_backend = os.environ.get("SYMCSE_SYMBOLIC_BACKEND", "").lower() if _req_backend == "symengine": use_symengine = True elif _req_backend == "sympy": @@ -178,7 +178,7 @@ def __init__(self, use_symengine=None, kw_cse=None, assume_real=True): elif _req_backend == "": use_symengine = False # se is not None else: - raise ValueError("Unknown SYMCXSE_BACKEND: %s" % _req_backend) + raise ValueError("Unknown SYMCSE_SYMBOLIC_BACKEND: %s" % _req_backend) if use_symengine and se is None: raise ValueError("symengine missing (pip install symengine)") self.use_symengine = use_symengine @@ -265,7 +265,7 @@ def lambdify(self, args, exprs): return sympy.lambdify(args, exprs) -class BackendWithDisabledCSE(Backend): +class SymbolicBackendWithDisabledCSE(SymbolicBackend): def cse(self, exprs, **kwargs): return [], exprs From d8406229ed41fbe28773041a71c35f4ab0ba216f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 14 Nov 2022 21:49:48 +0100 Subject: [PATCH 062/102] update sxx --- examples/_predefined_subexpression.ipynb | 2 +- examples/_predefined_subexpression2.ipynb | 2 +- examples/_scalar_ode.ipynb | 65 ++++++++++++------- .../native/sources/summation_cxx/impl.hpp | 9 +++ 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/examples/_predefined_subexpression.ipynb b/examples/_predefined_subexpression.ipynb index a0fb6c45..7476f528 100644 --- a/examples/_predefined_subexpression.ipynb +++ b/examples/_predefined_subexpression.ipynb @@ -427,7 +427,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.10.4" } }, "nbformat": 4, diff --git a/examples/_predefined_subexpression2.ipynb b/examples/_predefined_subexpression2.ipynb index 4e79c61a..83e553fb 100644 --- a/examples/_predefined_subexpression2.ipynb +++ b/examples/_predefined_subexpression2.ipynb @@ -165,7 +165,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.10.4" } }, "nbformat": 4, diff --git a/examples/_scalar_ode.ipynb b/examples/_scalar_ode.ipynb index faaf1bf1..1fa819ee 100644 --- a/examples/_scalar_ode.ipynb +++ b/examples/_scalar_ode.ipynb @@ -31,8 +31,9 @@ "metadata": {}, "outputs": [], "source": [ - "def vary(sys, kws):\n", - " res = [sys.integrate((1, 1e20), [1.0], atol=1e-50, nsteps=64000, **kw) for kw in kws]\n", + "def vary(sys, kws, *, t_end=1e20):\n", + " res = [sys.integrate((1, t_end), [1.0], atol=1e-50, nsteps=64000#, record_order=True\n", + " , **kw) for kw in kws]\n", " return res" ] }, @@ -44,10 +45,11 @@ "outputs": [], "source": [ "kws = [\n", - " dict(integrator='cvode', method='bdf', rtol=1e-6),\n", " dict(integrator='cvode', method='adams', rtol=1e-10),\n", + " dict(integrator='cvode', method='bdf', rtol=1e-10, first_step=1e-10),\n", + " dict(integrator='gsl', method='msbdf', rtol=1e-10),\n", " dict(integrator='gsl', method='bsimp', rtol=1e-10),\n", - " dict(integrator='gsl', method='msbdf', rtol=1e-10)\n", + " dict(integrator='odeint', method='bs', rtol=1e-10)\n", "]\n", "\n", "res1 = vary(sys1, kws)" @@ -60,39 +62,66 @@ "metadata": {}, "outputs": [], "source": [ - "def plot_variation(res, kws):\n", - " fig, axes = plt.subplots(1, len(res), figsize=(16,6), dpi=150)\n", + "def plot_variation(res, kws, *, start_idx=0, fig_kw=None, plot_order=False):\n", + " if fig_kw is None:\n", + " fig_kw = dict(figsize=(16,6), dpi=150, sharey=True)\n", + " \n", + " if plot_order:\n", + " from mpl_toolkits.axes_grid1 import host_subplot\n", + " import mpl_toolkits.axisartist as aa\n", + " \n", + " fig = plt.figure(**fig_kw)\n", + " axes = [host_subplot(1, len(res), i+1, figure=fig, axes_class=aa.Axes) for i in range(len(res))]\n", + " else:\n", + " fig, axes = plt.subplots(1, len(res), **fig_kw)\n", + " \n", " for ax, r, kw in zip(axes, res, kws):\n", " #r.plot(ax=ax)\n", " rx = 1/r.xout\n", - " ax.plot(r.xout, np.abs(r.yout.squeeze() - rx)/rx, label='observed')\n", + " relerr = np.abs(r.yout.squeeze() - rx)/rx\n", + " ax.plot(r.xout[start_idx:], np.log10(relerr[start_idx:])\n", + " , label=f\"{kw['integrator']}, {kw['method']}\"\n", + " , linewidth=0.5#, marker='x'\n", + " )\n", " ax.set_xscale('log')\n", " ax.set_yscale('linear')\n", - " ax.set_ylabel('|relative error|')\n", + " ax.set_ylabel('log10 |relative error|')\n", "\n", " random_walk = np.sqrt(1.0+np.array(range(r.xout.size))) # non-systematic errors (no bias)\n", " #ax.plot(r.xout, random_walk*kw['rtol'], label='prognosis')\n", "\n", " ax.legend()\n", + " ax.set_ylim([-16, -2])\n", "\n", " ax2 = ax.twinx()\n", " ax2.plot(r.xout[:-1], np.diff(r.xout), color='k', linewidth=0.5\n", " #, ls='None', marker='.', markersize=0.5, alpha=0.3\n", " )\n", " ax2.set_yscale('log')\n", - " ax2.set_ylim([1e-12, 1e20])\n", + " ax2.set_ylim([1e-12, r.xout[-1]])\n", " ax2.set_ylabel('step size')\n", "\n", - " ax.set_title(f\"{kw['integrator']}\\n{kw['method']}\\n{r.info['n_steps']=}\\n{r.info['time_cpu']=}\\n \")\n", + " if plot_order:\n", + " ax3 = ax.twinx() \n", + " ax3.axis[\"right\"] = ax3.new_fixed_axis(loc=\"right\", offset=(60, 0))\n", + " p3, = ax3.plot(r.xout, r.info['orders'], linewidth=0.5, color='tab:orange')\n", + " ax3.axis[\"right\"].label.set_color(p3.get_color())\n", "\n", - " fig.tight_layout()" + " ax2.axis[\"right\"].toggle(all=True)\n", + " ax3.axis[\"right\"].toggle(all=True)\n", + " \n", + " ax.set_title(f\"{r.info['n_steps']=}\\n{r.info['time_cpu']=:.3f} s\\n \")\n", + "\n", + " fig.tight_layout(pad=1.08, rect=(-.05, -.05, 1.05, 1.05))" ] }, { "cell_type": "code", "execution_count": null, "id": "33d96a20", - "metadata": {}, + "metadata": { + "scrolled": false + }, "outputs": [], "source": [ "plot_variation(res1, kws)" @@ -122,15 +151,7 @@ { "cell_type": "code", "execution_count": null, - "id": "65b2d5cd", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cd167f60", + "id": "9dc8c5b1", "metadata": {}, "outputs": [], "source": [] @@ -152,7 +173,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.10.4" } }, "nbformat": 4, diff --git a/pyodesys/native/sources/summation_cxx/impl.hpp b/pyodesys/native/sources/summation_cxx/impl.hpp index d46664d8..063c1ea9 100644 --- a/pyodesys/native/sources/summation_cxx/impl.hpp +++ b/pyodesys/native/sources/summation_cxx/impl.hpp @@ -141,7 +141,16 @@ struct Operators { { return accumulator_type(-ACCUM(const), -CARRY(const)); } +#define SXX_COMP(OPER_) \ + bool operator OPER_(const T& arg) const \ + { \ + const Derived& self = *(static_cast(this)); \ + return self.template to() OPER_ arg; \ + } + SXX_COMP(<) + SXX_COMP(>) }; +#undef SXX_COMP #define SXX_COMMUTATIVE_OP(OP) \ template \ typename Derived::accumulator_type operator OP( \ From a7c6d2389ca76204f2aa88e7461cc4bd08b3ea7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 15 Nov 2022 22:11:08 +0100 Subject: [PATCH 063/102] fix test --- pyodesys/native/tests/_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyodesys/native/tests/_tests.py b/pyodesys/native/tests/_tests.py index 244386dc..1ca5372e 100644 --- a/pyodesys/native/tests/_tests.py +++ b/pyodesys/native/tests/_tests.py @@ -13,7 +13,7 @@ ) from pyodesys.tests.bateman import bateman_full # analytic, never mind the details from pyodesys.tests.test_symbolic import decay_rhs, decay_dydt_factory, _get_decay3 -from pyodesys.native.symcse.util import BackendWithDisabledCSE +from pyodesys.native.symcse.util import SymbolicBackendWithDisabledCSE sp = import_('sympy') @@ -101,7 +101,7 @@ class TransformedNativeSys(TransformedSys, NativeSys): def _test_Decay_nonnegative(NativeSys, use_cse, compensated): odesys = NativeSys.from_other( _get_decay3(lower_bounds=[0]*3), - native_code_kw=dict(groupwise_kw=None if use_cse else {'backend': BackendWithDisabledCSE()}, compensated_summation=compensated), + native_code_kw=dict(groupwise_kw=None if use_cse else {'backend': SymbolicBackendWithDisabledCSE()}, compensated_summation=compensated), ) y0, k = [3., 2., 1.], [3.5, 2.5, 0] xout, yout, info = odesys.integrate([1e-10, 1], y0, k, integrator='native') From 1ec6b44cc35a15455c00ad884fdb6c3d9ce31c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 24 Apr 2024 15:33:39 +0200 Subject: [PATCH 064/102] run tests for sdist install --- .ci/run_ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/run_ci.sh b/.ci/run_ci.sh index 8533c959..66befbda 100755 --- a/.ci/run_ci.sh +++ b/.ci/run_ci.sh @@ -27,7 +27,7 @@ CFLAGS=$SUND_CFLAGS LDFLAGS=$SUND_LDFLAGS python3 -m pip install pycvodes python3 setup.py sdist PKG_VERSION=$(python3 setup.py --version) -(cd dist/; python3 -m pip install $PKG_NAME-$PKG_VERSION.tar.gz) +(cd dist/; python3 -m pip install $PKG_NAME-$PKG_VERSION.tar.gz; python3 -m pytest --pyargs $PKG_NAME) python3 -m pip install -e .[all] export PYODESYS_CVODE_FLAGS=$SUND_CFLAGS export PYODESYS_CVODE_LDFLAGS=$SUND_LDFLAGS From 79e6b08e345e99a8e6ce38748a6106bc0dcb98a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 24 Apr 2024 15:38:24 +0200 Subject: [PATCH 065/102] select all for tests --- .ci/run_ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/run_ci.sh b/.ci/run_ci.sh index 66befbda..5c8d6e53 100755 --- a/.ci/run_ci.sh +++ b/.ci/run_ci.sh @@ -27,7 +27,7 @@ CFLAGS=$SUND_CFLAGS LDFLAGS=$SUND_LDFLAGS python3 -m pip install pycvodes python3 setup.py sdist PKG_VERSION=$(python3 setup.py --version) -(cd dist/; python3 -m pip install $PKG_NAME-$PKG_VERSION.tar.gz; python3 -m pytest --pyargs $PKG_NAME) +(cd dist/; python3 -m pip install "$PKG_NAME-$PKG_VERSION.tar.gz[all]"; python3 -m pytest --pyargs $PKG_NAME) python3 -m pip install -e .[all] export PYODESYS_CVODE_FLAGS=$SUND_CFLAGS export PYODESYS_CVODE_LDFLAGS=$SUND_LDFLAGS From e4e3f16aa2253b263b03f68e6b96e363fbce0597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 24 Apr 2024 15:48:16 +0200 Subject: [PATCH 066/102] move flags for compilation --- .ci/run_ci.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.ci/run_ci.sh b/.ci/run_ci.sh index 5c8d6e53..1841a880 100755 --- a/.ci/run_ci.sh +++ b/.ci/run_ci.sh @@ -27,10 +27,11 @@ CFLAGS=$SUND_CFLAGS LDFLAGS=$SUND_LDFLAGS python3 -m pip install pycvodes python3 setup.py sdist PKG_VERSION=$(python3 setup.py --version) -(cd dist/; python3 -m pip install "$PKG_NAME-$PKG_VERSION.tar.gz[all]"; python3 -m pytest --pyargs $PKG_NAME) -python3 -m pip install -e .[all] export PYODESYS_CVODE_FLAGS=$SUND_CFLAGS export PYODESYS_CVODE_LDFLAGS=$SUND_LDFLAGS +(cd dist/; python3 -m pip install "$PKG_NAME-$PKG_VERSION.tar.gz[all]"; python3 -m pytest --pyargs $PKG_NAME) +python3 -m pip uninstall $PKG_NAME +python3 -m pip install -e .[all] python3 -m pytest -xv -k test_integrate_chained_robertson pyodesys/tests/test_robertson.py export PYTHONHASHSEED=$(python3 -c "import random; print(random.randint(1,2**32-1))") PYTHON="python3 -R" ./scripts/run_tests.sh --cov $PKG_NAME --cov-report html From 936ea693f80b27204d1d484fd2520229f52054b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 24 Apr 2024 15:54:49 +0200 Subject: [PATCH 067/102] pass yes-flag to pip-uninstall --- .ci/run_ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/run_ci.sh b/.ci/run_ci.sh index 1841a880..332e2b84 100755 --- a/.ci/run_ci.sh +++ b/.ci/run_ci.sh @@ -30,7 +30,7 @@ PKG_VERSION=$(python3 setup.py --version) export PYODESYS_CVODE_FLAGS=$SUND_CFLAGS export PYODESYS_CVODE_LDFLAGS=$SUND_LDFLAGS (cd dist/; python3 -m pip install "$PKG_NAME-$PKG_VERSION.tar.gz[all]"; python3 -m pytest --pyargs $PKG_NAME) -python3 -m pip uninstall $PKG_NAME +python3 -m pip uninstall --yes $PKG_NAME python3 -m pip install -e .[all] python3 -m pytest -xv -k test_integrate_chained_robertson pyodesys/tests/test_robertson.py export PYTHONHASHSEED=$(python3 -c "import random; print(random.randint(1,2**32-1))") From 119d7c5080c3b24a6108df0bfd1321abf89a512e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 24 Apr 2024 18:03:13 +0200 Subject: [PATCH 068/102] move nogil --- pyodesys/native/sources/odesys_anyode.pxd | 4 ++-- pyodesys/native/sources/odesys_anyode_iterative.pxd | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyodesys/native/sources/odesys_anyode.pxd b/pyodesys/native/sources/odesys_anyode.pxd index cefe653d..1957d771 100644 --- a/pyodesys/native/sources/odesys_anyode.pxd +++ b/pyodesys/native/sources/odesys_anyode.pxd @@ -11,8 +11,8 @@ from anyode cimport Info cdef extern from "odesys_anyode.hpp" namespace "odesys_anyode": cdef cppclass OdeSys[Real_t, Index_t]: OdeSys(const Real_t * const, vector[Real_t], Real_t, Real_t, - bool, Real_t, vector[Real_t]) nogil except + + bool, Real_t, vector[Real_t]) except + nogil Index_t get_ny() nogil Real_t get_dx0(Real_t, const Real_t * const) nogil unsigned nfev, njev, njvev - Info current_info + Info current_info diff --git a/pyodesys/native/sources/odesys_anyode_iterative.pxd b/pyodesys/native/sources/odesys_anyode_iterative.pxd index c869dea3..7b2189b5 100644 --- a/pyodesys/native/sources/odesys_anyode_iterative.pxd +++ b/pyodesys/native/sources/odesys_anyode_iterative.pxd @@ -11,8 +11,8 @@ from anyode cimport Info cdef extern from "odesys_anyode_iterative.hpp" namespace "odesys_anyode": cdef cppclass OdeSys[Real_t, Index_t]: OdeSys(const Real_t * const, vector[Real_t], Real_t, Real_t, - bool, Real_t, vector[Real_t]) nogil except + - Info current_info + bool, Real_t, vector[Real_t]) except + nogil + Info current_info bool autonomous_exprs bool record_rhs_xvals bool record_jac_xvals From 0691aed209b3a9454ce98ae81d95801219bc76f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 24 Apr 2024 18:04:39 +0200 Subject: [PATCH 069/102] bump commits --- external/anyode | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/anyode b/external/anyode index c06c0dd1..ac350a93 160000 --- a/external/anyode +++ b/external/anyode @@ -1 +1 @@ -Subproject commit c06c0dd1f50b6e7bca8db4d0e9d017d026b05936 +Subproject commit ac350a937b0fe74a4da4f9b6f983c982b1cce0d2 From e5e6c0275fa572c6cd2258f820a4eacaeacc1440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 13 Jan 2025 13:34:12 +0100 Subject: [PATCH 070/102] fix cython syntax: except+ --- external/anyode | 2 +- pyodesys/native/sources/odesys_anyode.pxd | 2 +- pyodesys/native/sources/odesys_anyode_iterative.pxd | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/external/anyode b/external/anyode index c06c0dd1..898d791b 160000 --- a/external/anyode +++ b/external/anyode @@ -1 +1 @@ -Subproject commit c06c0dd1f50b6e7bca8db4d0e9d017d026b05936 +Subproject commit 898d791b786dc2819b1d107808eb8ac889315263 diff --git a/pyodesys/native/sources/odesys_anyode.pxd b/pyodesys/native/sources/odesys_anyode.pxd index 09f9e27b..c579415b 100644 --- a/pyodesys/native/sources/odesys_anyode.pxd +++ b/pyodesys/native/sources/odesys_anyode.pxd @@ -11,7 +11,7 @@ from anyode cimport Info cdef extern from "odesys_anyode.hpp" namespace "odesys_anyode": cdef cppclass OdeSys[Real_t, Index_t]: OdeSys(const Real_t * const, vector[Real_t], Real_t, Real_t, - bool, Real_t, vector[Real_t]) nogil except + + bool, Real_t, vector[Real_t]) nogil except+ Index_t get_ny() nogil Real_t get_dx0(Real_t, const Real_t * const) nogil unsigned nfev, njev, njvev diff --git a/pyodesys/native/sources/odesys_anyode_iterative.pxd b/pyodesys/native/sources/odesys_anyode_iterative.pxd index c334f2ae..857b3cb9 100644 --- a/pyodesys/native/sources/odesys_anyode_iterative.pxd +++ b/pyodesys/native/sources/odesys_anyode_iterative.pxd @@ -11,16 +11,16 @@ from anyode cimport Info, Status cdef extern from "odesys_anyode_iterative.hpp" namespace "odesys_anyode": cdef cppclass OdeSys[Real_t, Index_t]: OdeSys(const Real_t * const, vector[Real_t], Real_t, Real_t, - bool, Real_t, vector[Real_t]) nogil except + + bool, Real_t, vector[Real_t]) nogil except+ Info current_info bool autonomous_exprs bool record_rhs_xvals bool record_jac_xvals bool record_order bool record_fpe - Status rhs(Real_t t, const Real_t * const y, Real_t * const f) except + + Status rhs(Real_t t, const Real_t * const y, Real_t * const f) except+ Status dense_jac_cmaj(Real_t t, const Real_t * const y, const Real_t * const fy, Real_t * const jac, - long int ldim) except + + long int ldim) except+ From dfb97f00d218cd953e0d297cd64bd8392af62b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 14 Jan 2025 16:42:07 +0100 Subject: [PATCH 071/102] nogil except + --- external/anyode | 2 +- pyodesys/native/sources/odesys_anyode.pxd | 2 +- pyodesys/native/sources/odesys_anyode_iterative.pxd | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/external/anyode b/external/anyode index ac350a93..898d791b 160000 --- a/external/anyode +++ b/external/anyode @@ -1 +1 @@ -Subproject commit ac350a937b0fe74a4da4f9b6f983c982b1cce0d2 +Subproject commit 898d791b786dc2819b1d107808eb8ac889315263 diff --git a/pyodesys/native/sources/odesys_anyode.pxd b/pyodesys/native/sources/odesys_anyode.pxd index 1957d771..09f9e27b 100644 --- a/pyodesys/native/sources/odesys_anyode.pxd +++ b/pyodesys/native/sources/odesys_anyode.pxd @@ -11,7 +11,7 @@ from anyode cimport Info cdef extern from "odesys_anyode.hpp" namespace "odesys_anyode": cdef cppclass OdeSys[Real_t, Index_t]: OdeSys(const Real_t * const, vector[Real_t], Real_t, Real_t, - bool, Real_t, vector[Real_t]) except + nogil + bool, Real_t, vector[Real_t]) nogil except + Index_t get_ny() nogil Real_t get_dx0(Real_t, const Real_t * const) nogil unsigned nfev, njev, njvev diff --git a/pyodesys/native/sources/odesys_anyode_iterative.pxd b/pyodesys/native/sources/odesys_anyode_iterative.pxd index 7b2189b5..088410ac 100644 --- a/pyodesys/native/sources/odesys_anyode_iterative.pxd +++ b/pyodesys/native/sources/odesys_anyode_iterative.pxd @@ -11,7 +11,7 @@ from anyode cimport Info cdef extern from "odesys_anyode_iterative.hpp" namespace "odesys_anyode": cdef cppclass OdeSys[Real_t, Index_t]: OdeSys(const Real_t * const, vector[Real_t], Real_t, Real_t, - bool, Real_t, vector[Real_t]) except + nogil + bool, Real_t, vector[Real_t]) nogil except + Info current_info bool autonomous_exprs bool record_rhs_xvals From 477a554a78607f1f4b5af5e2bc2064443e86fd79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 14 Jan 2025 18:14:31 +0100 Subject: [PATCH 072/102] bjodahimg20dot -> triceratops-3:24 --- .ci/run_ci.sh | 15 +++++++++++---- .woodpecker.yaml | 44 +++++++++++++++++++++----------------------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/.ci/run_ci.sh b/.ci/run_ci.sh index 332e2b84..3b6cf790 100755 --- a/.ci/run_ci.sh +++ b/.ci/run_ci.sh @@ -9,7 +9,12 @@ if [ ! -e "$SUNDBASE/include/sundials/sundials_config.h" ]; then >&2 echo "Not a valid prefix for sundials: $SUNDBASE" exit 1 fi - +if [ -e /etc/profile.d/boost.sh ]; then + source /etc/profile.d/boost.sh + export CPATH=$BOOST_ROOT/include +fi +source $(compgen -G "/opt-3/cpython-v3.*-apt-deb/bin/activate") +python3 -m pip install --cache-dir $CI_WORKSPACE/cache-ci/pip_cache --upgrade-strategy=eager --upgrade cython "git+https://github.com/bjodah/pycompilation@use-importlib-rather-than-imp#egg=pycompilation" # REPO_TEMP_DIR="$(mktemp -d)" # trap 'rm -rf -- "$REPO_TEMP_DIR"' EXIT # cp -ra . "$REPO_TEMP_DIR/." @@ -17,13 +22,15 @@ fi mkdir -p $HOME/.config/pip/ echo -e "[global]\nno-cache-dir = false\ndownload-cache = $CI_WORKSPACE/cache-ci/pip_cache" >$HOME/.config/pip/pip.conf -python3 -m pip install symcxx pysym # unofficial backends, symengine is tested in the conda build +python3 -m pip install mako cython +python3 -m pip install --no-build-isolation "git+https://github.com/bjodah/symcxx#egg=symcxx" "git+https://github.com/bjodah/pysym#egg=pysym" # unofficial backends, symengine is tested in the conda build # (cd ./tmp/pycvodes; SUND_CFLAGS="-isystem $SUNDBASE/include $CFLAGS" SUND_LDFLAGS="-Wl,--disable-new-dtags -Wl,-rpath,$SUNDBASE/lib -L$SUNDBASE/lib $LDFLAGS" -CFLAGS=$SUND_CFLAGS LDFLAGS=$SUND_LDFLAGS python3 -m pip install pycvodes - +CFLAGS=$SUND_CFLAGS CXXFLAGS=$SUND_CFLAGS LDFLAGS=$SUND_LDFLAGS python3 -m pip install --no-build-isolation pycvodes +python3 -m pip install --no-build-isolation "git+https://github.com/bjodah/pyodeint#egg=pyodeint" +python3 -m pip install --no-build-isolation "git+https://github.com/bjodah/pygslodeiv2#egg=pygslodeiv2" python3 setup.py sdist PKG_VERSION=$(python3 setup.py --version) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 6a96b609..921ac7da 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -4,7 +4,7 @@ when: steps: - name: restore-cache - image: bjodah/bjodahimg20dot:21.8.a + image: cont-reg.bjodah.se:443/bjodah/triceratops-3:24 commands: - curl ftp://${CI_REPO_NAME}:$${ARTIFACTS_PASS}@$${FTP_SERVER}/cache/cache-ci.tar | tar -C / x || mkdir -p $CI_WORKSPACE/cache-ci/pip_cache secrets: [ ARTIFACTS_PASS, FTP_SERVER ] @@ -12,65 +12,63 @@ steps: - event: push repo: bjodah/pyodesys - - name: test-sund-5.7.0-rel + - name: test-sund-6.7.0-rel depends_on: restore-cache - image: bjodah/bjodahimg20dot:21.8.a + image: cont-reg.bjodah.se:443/bjodah/triceratops-3:24 environment: - - CC=gcc-11 - - CXX=g++-11 + - CC=gcc-14 + - CXX=g++-14 - OMP_NUM_THREADS=1 - ANYODE_NUM_THREADS=2 - - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.77.0/include + - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt-3/boost-1.87.0/include - LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - - SUNDBASE=/opt/sundials-5.7.0-release + - SUNDBASE=/opt-3/sundials-6.7.0-release commands: - - cp -ra $CI_WORKSPACE /tmp/test-sund-5.7.0-rel - - cd /tmp/test-sund-5.7.0-rel - - python3 -m pip install --cache-dir $CI_WORKSPACE/cache-ci/pip_cache --upgrade-strategy=eager --upgrade cython https://github.com/bjodah/pycompilation/archive/master.tar.gz + - cp -ra $CI_WORKSPACE /tmp/test-sund-6.7.0-rel + - cd /tmp/test-sund-6.7.0-rel - bash -c "ulimit -v 2048000; .ci/run_ci.sh pyodesys $SUNDBASE" - ./scripts/prepare_deploy.sh - bash -c '[[ $(python3 setup.py --version) =~ ^[0-9]+.* ]]' - - cp -ra /tmp/test-sund-5.7.0-rel/deploy $CI_WORKSPACE/ + - cp -ra /tmp/test-sund-6.7.0-rel/deploy $CI_WORKSPACE/ - - name: test-sund-5.7.0-extended + - name: test-sund-6.7.0-extended depends_on: restore-cache - image: bjodah/bjodahimg20dot:21.8.a + image: cont-reg.bjodah.se:443/bjodah/triceratops-3:24 environment: - - CC=gcc-11 - - CXX=g++-11 + - CC=gcc-14 + - CXX=g++-14 - OMP_NUM_THREADS=1 - ANYODE_NUM_THREADS=2 - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.77.0/include - LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - - SUNDBASE=/opt/sundials-5.7.0-extended + - SUNDBASE=/opt-3/sundials-6.7.0-extended - PYCVODES_NO_LAPACK=1 - PYCVODES_NO_KLU=1 commands: - - cp -ra $CI_WORKSPACE/ /tmp/test-sund-5.7.0-extended - - cd /tmp/test-sund-5.7.0-extended - - python3 -m pip install --cache-dir $CI_WORKSPACE/cache-ci/pip_cache --upgrade-strategy=eager --upgrade cython + - cp -ra $CI_WORKSPACE/ /tmp/test-sund-6.7.0-extended + - cd /tmp/test-sund-6.7.0-extended - bash -c "ulimit -v 2048000; .ci/run_ci.sh pyodesys $SUNDBASE" - ./scripts/grep-for-merge-blocking-token.sh - ./scripts/grep-for-binary-data.sh - name: rebuild-cache - image: bjodah/bjodahimg20dot:21.8.a + image: cont-reg.bjodah.se:443/bjodah/triceratops-3:24 commands: - find $CI_WORKSPACE/cache-ci/ -type f -mtime +90 -exec rm {} \; - tar cf cache-ci.tar $CI_WORKSPACE/cache-ci/ - curl -T cache-ci.tar ftp://${CI_REPO_NAME}:$${ARTIFACTS_PASS}@$${FTP_SERVER}/cache/ secrets: [ ARTIFACTS_PASS, FTP_SERVER ] depends_on: - - test-sund-5.7.0-rel + - test-sund-6.7.0-rel - name: deploy-public-html - image: bjodah/bjodahimg20dot:21.8.a + image: cont-reg.bjodah.se:443/bjodah/triceratops-3:24 commands: - tar -C deploy/public_html -czf ${CI_REPO_NAME}-${CI_COMMIT_BRANCH}.tar.gz . - curl -T ${CI_REPO_NAME}-${CI_COMMIT_BRANCH}.tar.gz ftp://${CI_REPO_NAME}:$${ARTIFACTS_PASS}@$${FTP_SERVER}/public_html/ secrets: [ ARTIFACTS_PASS, FTP_SERVER ] depends_on: - - test-sund-5.7.0-rel + - test-sund-6.7.0-rel From 8e0458fdec81978d254ee3aeeb9cb8416d5e353d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 15 Jan 2025 15:04:31 +0100 Subject: [PATCH 073/102] ci: update paths and env --- .woodpecker.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 921ac7da..2fd14722 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -18,6 +18,7 @@ steps: environment: - CC=gcc-14 - CXX=g++-14 + - CXXFLAGS=-std=c++20 - OMP_NUM_THREADS=1 - ANYODE_NUM_THREADS=2 - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt-3/boost-1.87.0/include @@ -38,9 +39,10 @@ steps: environment: - CC=gcc-14 - CXX=g++-14 + - CXXFLAGS=-std=c++20 - OMP_NUM_THREADS=1 - ANYODE_NUM_THREADS=2 - - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.77.0/include + - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.87.0/include - LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - SUNDBASE=/opt-3/sundials-6.7.0-extended From 29efeb68907a5b4267fb23dc6494caae1177221d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 15 Jan 2025 15:15:51 +0100 Subject: [PATCH 074/102] exit on first failed test --- .ci/run_ci.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/run_ci.sh b/.ci/run_ci.sh index 3b6cf790..e8956f76 100755 --- a/.ci/run_ci.sh +++ b/.ci/run_ci.sh @@ -28,7 +28,7 @@ python3 -m pip install --no-build-isolation "git+https://github.com/bjodah/symcx # (cd ./tmp/pycvodes; SUND_CFLAGS="-isystem $SUNDBASE/include $CFLAGS" SUND_LDFLAGS="-Wl,--disable-new-dtags -Wl,-rpath,$SUNDBASE/lib -L$SUNDBASE/lib $LDFLAGS" -CFLAGS=$SUND_CFLAGS CXXFLAGS=$SUND_CFLAGS LDFLAGS=$SUND_LDFLAGS python3 -m pip install --no-build-isolation pycvodes +CFLAGS="$SUND_CFLAGS $CXXFLAGS" CXXFLAGS="$SUND_CFLAGS $CXXFLAGS" LDFLAGS=$SUND_LDFLAGS python3 -m pip install --no-build-isolation pycvodes python3 -m pip install --no-build-isolation "git+https://github.com/bjodah/pyodeint#egg=pyodeint" python3 -m pip install --no-build-isolation "git+https://github.com/bjodah/pygslodeiv2#egg=pygslodeiv2" @@ -36,7 +36,7 @@ python3 setup.py sdist PKG_VERSION=$(python3 setup.py --version) export PYODESYS_CVODE_FLAGS=$SUND_CFLAGS export PYODESYS_CVODE_LDFLAGS=$SUND_LDFLAGS -(cd dist/; python3 -m pip install "$PKG_NAME-$PKG_VERSION.tar.gz[all]"; python3 -m pytest --pyargs $PKG_NAME) +(cd dist/; python3 -m pip install "$PKG_NAME-$PKG_VERSION.tar.gz[all]"; python3 -m pytest -v -x --pyargs $PKG_NAME) python3 -m pip uninstall --yes $PKG_NAME python3 -m pip install -e .[all] python3 -m pytest -xv -k test_integrate_chained_robertson pyodesys/tests/test_robertson.py From 462df0f77af0f86a37dd4d04a6c0c0b88976000c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 15 Jan 2025 15:22:53 +0100 Subject: [PATCH 075/102] -std=c++11 -> -std=c++20 --- pyodesys/native/_base.py | 2 +- pyodesys/native/sources/_cvode_wrapper.pyx | 2 +- pyodesys/native/sources/_gsl_wrapper.pyx | 2 +- pyodesys/native/sources/_odeint_wrapper.pyx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 93281f86..5fbf8e13 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -39,7 +39,7 @@ _compile_kwargs = { 'options': ['warn', 'pic', 'fast', 'openmp'], - 'std': 'c++11', + 'std': 'c++20', 'include_dirs': [np.get_include(), pkg_resources.resource_filename(__name__, 'sources')], 'libraries': [], 'cplus': True, diff --git a/pyodesys/native/sources/_cvode_wrapper.pyx b/pyodesys/native/sources/_cvode_wrapper.pyx index 3f9b195f..e261c812 100644 --- a/pyodesys/native/sources/_cvode_wrapper.pyx +++ b/pyodesys/native/sources/_cvode_wrapper.pyx @@ -1,7 +1,7 @@ # -*- mode: cython -*- # -*- coding: utf-8 -*- # distutils: language = c++ -# distutils: extra_compile_args = -std=c++11 -fopenmp +# distutils: extra_compile_args = -std=c++20 -fopenmp # distutils: extra_link_args = -fopenmp # cython: language_level=3str diff --git a/pyodesys/native/sources/_gsl_wrapper.pyx b/pyodesys/native/sources/_gsl_wrapper.pyx index 04aedd2e..c9e2d6f8 100644 --- a/pyodesys/native/sources/_gsl_wrapper.pyx +++ b/pyodesys/native/sources/_gsl_wrapper.pyx @@ -1,7 +1,7 @@ # -*- mode: cython -*- # -*- coding: utf-8 -*- # distutils: language = c++ -# distutils: extra_compile_args = -std=c++11 -fopenmp +# distutils: extra_compile_args = -std=c++20 -fopenmp # distutils: extra_link_args = -fopenmp diff --git a/pyodesys/native/sources/_odeint_wrapper.pyx b/pyodesys/native/sources/_odeint_wrapper.pyx index 188dfb52..34cb6fad 100644 --- a/pyodesys/native/sources/_odeint_wrapper.pyx +++ b/pyodesys/native/sources/_odeint_wrapper.pyx @@ -1,7 +1,7 @@ # -*- mode: cython -*- # -*- coding: utf-8 -*- # distutils: language = c++ -# distutils: extra_compile_args = -std=c++11 -fopenmp +# distutils: extra_compile_args = -std=c++20 -fopenmp # distutils: extra_link_args = -fopenmp From b70e529323edd5fbe0df58d90aa37b46fb3433e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 15 Jan 2025 16:03:00 +0100 Subject: [PATCH 076/102] .so -> EXT_SUFFIX in sysconfig --- pyodesys/native/_base.py | 5 +++-- setup.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index d40b0541..c95088e6 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -10,6 +10,7 @@ import os import shutil import sys +import sysconfig import tempfile import sympy @@ -69,7 +70,7 @@ def get_compile_kwargs(kwargs): return kw -_ext_suffix = '.so' # sysconfig.get_config_var('EXT_SUFFIX') +_ext_suffix = sysconfig.get_config_var('EXT_SUFFIX') _obj_suffix = '.o' # os.path.splitext(_ext_suffix)[0] + '.o' # '.obj' @@ -149,7 +150,7 @@ def __init__(self, odesys, *args, groupwise_kw=None, assigner_kws=None, ''.join(filter(lambda x: str.isalnum(x) or x in '_-', str(odesys.description).replace(' ', '_'))) ) self.obj_files = self.obj_files + ('%s%s' % (self.wrapper_name, _obj_suffix),) - self.so_file = '%s%s' % (self.wrapper_name, '.so') + self.so_file = '%s%s' % (self.wrapper_name, _ext_suffix) _wrapper_src = pkg_resources.resource_filename( __name__, 'sources/%s.pyx' % self.wrapper_name) if cachedir is None: diff --git a/setup.py b/setup.py index 8d433478..7e88644d 100755 --- a/setup.py +++ b/setup.py @@ -60,6 +60,7 @@ def _path_under_setup(*args): submodules = [ 'pyodesys.native', + 'pyodesys.native.symcse', ] tests = [ From 29b19abcaa99f50163fd8013b4abc4ba73b72da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Fri, 17 Jan 2025 14:42:40 +0100 Subject: [PATCH 077/102] relax .names attribute on Result --- pyodesys/results.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyodesys/results.py b/pyodesys/results.py index 0159598b..56826941 100644 --- a/pyodesys/results.py +++ b/pyodesys/results.py @@ -18,7 +18,8 @@ def __init__(self, xout, yout, params, info, odesys): self.params = params self.info = info self.odesys = odesys - self.names = self.odesys.names + # names not always present: + self.names = getattr(self.odesys, 'names', None) def copy(self): return Result(self.xout.copy(), self.yout.copy(), self.params.copy(), From 8847771b6fb28339d2fefcda08830197fb710ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Fri, 7 Feb 2025 18:13:23 +0100 Subject: [PATCH 078/102] error on nan or inf --- .../_native_override_chemical_kinetics.ipynb | 4 +- pyodesys/native/_base.py | 6 +- .../native/sources/odesys_anyode_template.cpp | 59 +++++++++++++++---- pyodesys/results.py | 7 ++- 4 files changed, 56 insertions(+), 20 deletions(-) diff --git a/examples/_native_override_chemical_kinetics.ipynb b/examples/_native_override_chemical_kinetics.ipynb index 34f86ec6..b2b20755 100644 --- a/examples/_native_override_chemical_kinetics.ipynb +++ b/examples/_native_override_chemical_kinetics.ipynb @@ -114,13 +114,11 @@ " const indextype ny = get_ny();\n", " std::vector f(ny);\n", " realtype tot=0.0;\n", - " rhs(x, y, &f[0]);\n", + " auto flag_rhs = rhs(x, y, &f[0]); if (flag_rhs != AnyODE::Status::success) { return AnyODE::Status::unrecoverable_error; }\n", " for (indextype i=0; inrev++;\n", - " return AnyODE::Status::success;\n", "\"\"\"\n", "}\n", "native_extend={\n", diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index c95088e6..00a44fa5 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -117,7 +117,11 @@ class _NativeCodeBase(Cpp_Code): obj_files = ('odesys_anyode.o',) _save_temp = False - namespace_default = {'p_anon': None} + namespace_default = { + 'p_anon': None, + 'p_error_on_inf': True, + 'p_error_on_nan': True, + } namespace = { 'p_includes': {'"odesys_anyode.hpp"'}, 'p_support_recoverable_error': False, diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp b/pyodesys/native/sources/odesys_anyode_template.cpp index 84a3514f..e7a770a5 100644 --- a/pyodesys/native/sources/odesys_anyode_template.cpp +++ b/pyodesys/native/sources/odesys_anyode_template.cpp @@ -5,6 +5,18 @@ This is file is a mako template for a C++ source file defining the ODE system. <%! import sympy + +%> +<% +def _inf_or_nan(s): + isinf_isnan = {'isinf(%s)': p_error_on_inf, 'isnan(%s)': p_error_on_nan} + return ' || '.join([func % s for func, pred in isinf_isnan.items()]) + +def _err_code(): + return 'AnyODE::Status::recoverable_error' if p_support_recoverable_error else 'AnyODE::Status::unrecoverable_error' + +assert p_error_on_nan in (False, True) +assert p_error_on_inf in (False, True) %> // User provided system description: ${p_odesys.description} // Names of dependent variables: ${p_odesys.names} @@ -157,21 +169,28 @@ namespace odesys_anyode { AnyODE::Status OdeSys::rhs(realtype x, const realtype * const ANYODE_RESTRICT y, realtype * const ANYODE_RESTRICT out) { - %if p_compensated_summation: + %if getattr(p_odesys, "_nonnegative", False) and p_support_recoverable_error: + for (int i=0; i<${p_odesys.ny}; ++i) if (y[i] < 0) return AnyODE::Status::recoverable_error; + %endif + %if p_error_on_inf or p_error_on_nan: + for (int i=0; i<${p_odesys.ny}; ++i) { if (${_inf_or_nan('y[i]')}) { return ${_err_code()}; } } + + %endif + %if p_compensated_summation: summation_cxx::RangedAccumulatorNeumaier f(${p_odesys.ny}); f.init(out); - %else: + %else: realtype * const f = out; - %endif - %if isinstance(p_rhs, str): + %endif + %if isinstance(p_rhs, str): ${p_rhs} - %else: + %else: ${"AnyODE::ignore(x);" if p_odesys.autonomous_exprs else ""} ${p_rhs["cses"]} ${p_rhs["assign"].all(assign_to=lambda i: sympy.Symbol("f[%d]" % i))} - %if p_compensated_summation: + %if p_compensated_summation: f.commit(); - %endif + %endif this->nfev++; %if p_support_recoverable_error: @@ -212,8 +231,8 @@ namespace odesys_anyode { } %endif %endif - %if getattr(p_odesys, "_nonnegative", False) and p_support_recoverable_error: - for (int i=0; i<${p_odesys.ny}; ++i) if (y[i] < 0) return AnyODE::Status::recoverable_error; + %if p_error_on_inf or p_error_on_nan: + for (int i=0; i<${p_odesys.ny}; ++i) { if (${_inf_or_nan('out[i]')}) { return ${_err_code()}; } } %endif return AnyODE::Status::success; %endif @@ -270,12 +289,13 @@ namespace odesys_anyode { ${"AnyODE::ignore(y);" if (not any([yi in p_odesys.get_jac().free_symbols for yi in p_odesys.dep]) and not any([yi in p_odesys.get_dfdx().free_symbols for yi in p_odesys.dep])) else ""} + %if p_error_on_inf or p_error_on_nan: + for (int i=0; i<${p_odesys.ny}; ++i) { if (${_inf_or_nan('y[i]')}) { return ${_err_code()}; } } + %endif ${p_jac_dense["cses"]} - %for i_major in range(p_odesys.ny): %for i_minor in range(p_odesys.ny): - <% if order == "cmaj": i = i_minor*p_odesys.ny + i_major @@ -292,6 +312,10 @@ namespace odesys_anyode { ${p_jac_dense["assign"](idx, assign_to=lambda _: "dfdt[%d]" % (idx - p_odesys.ny**2))} %endfor } + %if p_error_on_inf or p_error_on_nan: + for (int i = 0; i < ${p_odesys.ny}; ++i) { if (${_inf_or_nan('y[i]')}) { return ${_err_code()}; } } + %endif + this->njev++; return AnyODE::Status::success; %endif @@ -382,17 +406,26 @@ namespace odesys_anyode { } AnyODE::Status OdeSys::roots(realtype x, const realtype * const y, realtype * const out) { + + %if p_error_on_inf or p_error_on_nan: + if (${_inf_or_nan('x')}) { return ${_err_code()}; } + for (int i=0; i<${p_odesys.ny}; ++i) { if (${_inf_or_nan('y[i]')}) { return ${_err_code()}; } } + %endif + %if p_roots is None: AnyODE::ignore(x); AnyODE::ignore(y); AnyODE::ignore(out); - return AnyODE::Status::success; %elif isinstance(p_roots, str): ${p_roots} %else: ${"" if any(p_odesys.indep in expr.free_symbols for expr in p_odesys.roots) else "AnyODE::ignore(x);"} ${p_roots["cses"]} ${p_roots["assign"].all()} + %endif + + %if p_error_on_inf or p_error_on_nan: + for (int i=0; inrev++; return AnyODE::Status::success; - %endif } } diff --git a/pyodesys/results.py b/pyodesys/results.py index 56826941..c9b717f6 100644 --- a/pyodesys/results.py +++ b/pyodesys/results.py @@ -288,11 +288,12 @@ def calc_invariant_violations(self, xyp=None): val = invar(*(xyp or self._internals())) return val - val[0, :] - def plot_invariant_violations(self, **kwargs): + def plot_invariant_violations(self, *, apply_abs: bool=True, **kwargs): viol = self.calc_invariant_violations() - abs_viol = np.abs(viol) + if apply_abs: + viol = np.abs(viol) invar_names = self.odesys.all_invariant_names() - return self._plot(plot_result, x=self._internal('xout'), y=abs_viol, names=invar_names, + return self._plot(plot_result, x=self._internal('xout'), y=viol, names=invar_names, latex_names=kwargs.pop('latex_names', invar_names), indices=None, **kwargs) def extend_by_integration(self, xend, params=None, odesys=None, autonomous=None, npoints=1, **kwargs): From ddd445b91527a4f4d772ee98e60edf5f418265ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Fri, 7 Feb 2025 18:16:45 +0100 Subject: [PATCH 079/102] template.cpp.mako --- pyodesys/native/sources/odesys_anyode_template.cpp.mako | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 pyodesys/native/sources/odesys_anyode_template.cpp.mako diff --git a/pyodesys/native/sources/odesys_anyode_template.cpp.mako b/pyodesys/native/sources/odesys_anyode_template.cpp.mako new file mode 100644 index 00000000..e69de29b From f96e25e47e76690ddce200a8acbeced76665dd95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Thu, 3 Apr 2025 12:05:03 +0200 Subject: [PATCH 080/102] bupmy anyode --- external/anyode | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/anyode b/external/anyode index 898d791b..045e9646 160000 --- a/external/anyode +++ b/external/anyode @@ -1 +1 @@ -Subproject commit 898d791b786dc2819b1d107808eb8ac889315263 +Subproject commit 045e9646170a688e7b950cef6ed4b5a58defbf78 From 502fc708ef94f6f76f2a6ed8e569da43ca6899a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Fri, 25 Jul 2025 07:40:24 +0200 Subject: [PATCH 081/102] bump anyode --- external/anyode | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/anyode b/external/anyode index 045e9646..1e8d24f0 160000 --- a/external/anyode +++ b/external/anyode @@ -1 +1 @@ -Subproject commit 045e9646170a688e7b950cef6ed4b5a58defbf78 +Subproject commit 1e8d24f00662cb1732cdfabd886aea1667117411 From e5119522752d5eb37806dd7c502b14baec9cd9a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 5 May 2026 13:53:17 +0200 Subject: [PATCH 082/102] bump CI config --- .woodpecker.yaml | 50 ++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 2fd14722..9857f8ee 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -4,7 +4,7 @@ when: steps: - name: restore-cache - image: cont-reg.bjodah.se:443/bjodah/triceratops-3:24 + image: cont-reg.bjodah.se:443/bjodah/triceratops-3:41 commands: - curl ftp://${CI_REPO_NAME}:$${ARTIFACTS_PASS}@$${FTP_SERVER}/cache/cache-ci.tar | tar -C / x || mkdir -p $CI_WORKSPACE/cache-ci/pip_cache secrets: [ ARTIFACTS_PASS, FTP_SERVER ] @@ -14,17 +14,17 @@ steps: - name: test-sund-6.7.0-rel depends_on: restore-cache - image: cont-reg.bjodah.se:443/bjodah/triceratops-3:24 + image: cont-reg.bjodah.se:443/bjodah/triceratops-3:41 environment: - - CC=gcc-14 - - CXX=g++-14 - - CXXFLAGS=-std=c++20 - - OMP_NUM_THREADS=1 - - ANYODE_NUM_THREADS=2 - - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt-3/boost-1.87.0/include - - LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - - SUNDBASE=/opt-3/sundials-6.7.0-release + CC: gcc-14 + CXX: g++-14 + CXXFLAGS: -std: c++20 + OMP_NUM_THREADS: 1 + ANYODE_NUM_THREADS: 2 + CPLUS_INCLUDE_PATH: /usr/include/suitesparse:/opt-3/boost-1.87.0/include + LIBRARY_PATH: /usr/lib/x86_64-linux-gnu + LD_LIBRARY_PATH: /usr/lib/x86_64-linux-gnu + SUNDBASE: /opt-3/sundials-6.7.0-release commands: - cp -ra $CI_WORKSPACE /tmp/test-sund-6.7.0-rel - cd /tmp/test-sund-6.7.0-rel @@ -35,19 +35,19 @@ steps: - name: test-sund-6.7.0-extended depends_on: restore-cache - image: cont-reg.bjodah.se:443/bjodah/triceratops-3:24 + image: cont-reg.bjodah.se:443/bjodah/triceratops-3:41 environment: - - CC=gcc-14 - - CXX=g++-14 - - CXXFLAGS=-std=c++20 - - OMP_NUM_THREADS=1 - - ANYODE_NUM_THREADS=2 - - CPLUS_INCLUDE_PATH=/usr/include/suitesparse:/opt/boost-1.87.0/include - - LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu - - SUNDBASE=/opt-3/sundials-6.7.0-extended - - PYCVODES_NO_LAPACK=1 - - PYCVODES_NO_KLU=1 + CC: gcc-14 + CXX: g++-14 + CXXFLAGS: -std: c++20 + OMP_NUM_THREADS: 1 + ANYODE_NUM_THREADS: 2 + CPLUS_INCLUDE_PATH: /usr/include/suitesparse:/opt/boost-1.87.0/include + LIBRARY_PATH: /usr/lib/x86_64-linux-gnu + LD_LIBRARY_PATH: /usr/lib/x86_64-linux-gnu + SUNDBASE: /opt-3/sundials-6.7.0-extended + PYCVODES_NO_LAPACK: 1 + PYCVODES_NO_KLU: 1 commands: - cp -ra $CI_WORKSPACE/ /tmp/test-sund-6.7.0-extended - cd /tmp/test-sund-6.7.0-extended @@ -56,7 +56,7 @@ steps: - ./scripts/grep-for-binary-data.sh - name: rebuild-cache - image: cont-reg.bjodah.se:443/bjodah/triceratops-3:24 + image: cont-reg.bjodah.se:443/bjodah/triceratops-3:41 commands: - find $CI_WORKSPACE/cache-ci/ -type f -mtime +90 -exec rm {} \; - tar cf cache-ci.tar $CI_WORKSPACE/cache-ci/ @@ -66,7 +66,7 @@ steps: - test-sund-6.7.0-rel - name: deploy-public-html - image: cont-reg.bjodah.se:443/bjodah/triceratops-3:24 + image: cont-reg.bjodah.se:443/bjodah/triceratops-3:41 commands: - tar -C deploy/public_html -czf ${CI_REPO_NAME}-${CI_COMMIT_BRANCH}.tar.gz . - curl -T ${CI_REPO_NAME}-${CI_COMMIT_BRANCH}.tar.gz ftp://${CI_REPO_NAME}:$${ARTIFACTS_PASS}@$${FTP_SERVER}/public_html/ From 60eb3c4d0c9254f00db682cc35935fb50fbebcdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 5 May 2026 19:42:32 +0200 Subject: [PATCH 083/102] Fix typo --- .woodpecker.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 9857f8ee..e41c1db0 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -18,7 +18,7 @@ steps: environment: CC: gcc-14 CXX: g++-14 - CXXFLAGS: -std: c++20 + CXXFLAGS: -std=c++20 OMP_NUM_THREADS: 1 ANYODE_NUM_THREADS: 2 CPLUS_INCLUDE_PATH: /usr/include/suitesparse:/opt-3/boost-1.87.0/include @@ -39,7 +39,7 @@ steps: environment: CC: gcc-14 CXX: g++-14 - CXXFLAGS: -std: c++20 + CXXFLAGS: -std=c++20 OMP_NUM_THREADS: 1 ANYODE_NUM_THREADS: 2 CPLUS_INCLUDE_PATH: /usr/include/suitesparse:/opt/boost-1.87.0/include From 92e17e1152502e0846919bfc8dd62043eec6770a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Tue, 5 May 2026 19:56:42 +0200 Subject: [PATCH 084/102] update branch --- .ci/run_ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/run_ci.sh b/.ci/run_ci.sh index 53a23b54..27a6d9e3 100755 --- a/.ci/run_ci.sh +++ b/.ci/run_ci.sh @@ -14,7 +14,7 @@ if [ -e /etc/profile.d/boost.sh ]; then export CPATH=$BOOST_ROOT/include fi source $(compgen -G "/opt-3/cpython-v3.*-apt-deb/bin/activate") -python3 -m pip install --cache-dir $CI_WORKSPACE/cache-ci/pip_cache --upgrade-strategy=eager --upgrade cython "git+https://github.com/bjodah/pycompilation@use-importlib-rather-than-imp#egg=pycompilation" +python3 -m pip install --cache-dir $CI_WORKSPACE/cache-ci/pip_cache --upgrade-strategy=eager --upgrade cython "git+https://github.com/bjodah/pycompilation@master#egg=pycompilation" # REPO_TEMP_DIR="$(mktemp -d)" # trap 'rm -rf -- "$REPO_TEMP_DIR"' EXIT # cp -ra . "$REPO_TEMP_DIR/." From f84c647f514b05302e2897c6c53608240ab0ee8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 6 May 2026 00:03:31 +0200 Subject: [PATCH 085/102] Update boost --- .woodpecker.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index e41c1db0..5c8f3a74 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -21,7 +21,7 @@ steps: CXXFLAGS: -std=c++20 OMP_NUM_THREADS: 1 ANYODE_NUM_THREADS: 2 - CPLUS_INCLUDE_PATH: /usr/include/suitesparse:/opt-3/boost-1.87.0/include + CPLUS_INCLUDE_PATH: /usr/include/suitesparse:/opt-4/boost-1.91.0-release/include LIBRARY_PATH: /usr/lib/x86_64-linux-gnu LD_LIBRARY_PATH: /usr/lib/x86_64-linux-gnu SUNDBASE: /opt-3/sundials-6.7.0-release @@ -42,7 +42,7 @@ steps: CXXFLAGS: -std=c++20 OMP_NUM_THREADS: 1 ANYODE_NUM_THREADS: 2 - CPLUS_INCLUDE_PATH: /usr/include/suitesparse:/opt/boost-1.87.0/include + CPLUS_INCLUDE_PATH: /usr/include/suitesparse:/opt-4/boost-1.91.0-release/include LIBRARY_PATH: /usr/lib/x86_64-linux-gnu LD_LIBRARY_PATH: /usr/lib/x86_64-linux-gnu SUNDBASE: /opt-3/sundials-6.7.0-extended From 31f3538542500e880b90bdddccd55f3bbd3c7c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 6 May 2026 00:05:42 +0200 Subject: [PATCH 086/102] Bump CI image --- .woodpecker.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 5c8f3a74..bc35476b 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -4,7 +4,7 @@ when: steps: - name: restore-cache - image: cont-reg.bjodah.se:443/bjodah/triceratops-3:41 + image: cont-reg.bjodah.se:443/bjodah/triceratops-4:33 commands: - curl ftp://${CI_REPO_NAME}:$${ARTIFACTS_PASS}@$${FTP_SERVER}/cache/cache-ci.tar | tar -C / x || mkdir -p $CI_WORKSPACE/cache-ci/pip_cache secrets: [ ARTIFACTS_PASS, FTP_SERVER ] @@ -14,7 +14,7 @@ steps: - name: test-sund-6.7.0-rel depends_on: restore-cache - image: cont-reg.bjodah.se:443/bjodah/triceratops-3:41 + image: cont-reg.bjodah.se:443/bjodah/triceratops-4:33 environment: CC: gcc-14 CXX: g++-14 @@ -35,7 +35,7 @@ steps: - name: test-sund-6.7.0-extended depends_on: restore-cache - image: cont-reg.bjodah.se:443/bjodah/triceratops-3:41 + image: cont-reg.bjodah.se:443/bjodah/triceratops-4:33 environment: CC: gcc-14 CXX: g++-14 @@ -56,7 +56,7 @@ steps: - ./scripts/grep-for-binary-data.sh - name: rebuild-cache - image: cont-reg.bjodah.se:443/bjodah/triceratops-3:41 + image: cont-reg.bjodah.se:443/bjodah/triceratops-4:33 commands: - find $CI_WORKSPACE/cache-ci/ -type f -mtime +90 -exec rm {} \; - tar cf cache-ci.tar $CI_WORKSPACE/cache-ci/ @@ -66,7 +66,7 @@ steps: - test-sund-6.7.0-rel - name: deploy-public-html - image: cont-reg.bjodah.se:443/bjodah/triceratops-3:41 + image: cont-reg.bjodah.se:443/bjodah/triceratops-4:33 commands: - tar -C deploy/public_html -czf ${CI_REPO_NAME}-${CI_COMMIT_BRANCH}.tar.gz . - curl -T ${CI_REPO_NAME}-${CI_COMMIT_BRANCH}.tar.gz ftp://${CI_REPO_NAME}:$${ARTIFACTS_PASS}@$${FTP_SERVER}/public_html/ From 6ad68500f8d5fd59d9ede285d6b60e47161f1d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Wed, 6 May 2026 00:12:00 +0200 Subject: [PATCH 087/102] Bump AnyODE --- external/anyode | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/anyode b/external/anyode index 1e8d24f0..156635a1 160000 --- a/external/anyode +++ b/external/anyode @@ -1 +1 @@ -Subproject commit 1e8d24f00662cb1732cdfabd886aea1667117411 +Subproject commit 156635a11ba3a1f33027284e0eb2d0a96ca5e50a From 67612bd6ffd192c61ea03153dee4696100518385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Fri, 8 May 2026 16:40:21 +0200 Subject: [PATCH 088/102] do without pkg_resources --- pyodesys/util.py | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/pyodesys/util.py b/pyodesys/util.py index f5e795a6..d4d94276 100644 --- a/pyodesys/util.py +++ b/pyodesys/util.py @@ -4,11 +4,9 @@ from functools import reduce import inspect import math -import operator +from operator import lt, le, eq, ne, ge, gt import sys -from pkg_resources import parse_requirements, parse_version - import numpy as np import pytest @@ -190,8 +188,14 @@ def __call__(self, x, y, params=(), backend=None): return result +_relop = dict(zip("<= == != >= > <".split(), (le, eq, ne, ge, gt, lt))) + + +def _parse_version(vs: str, /): + return tuple(map(int, vs.split('.'))) + class requires(object): - """ Conditional skipping (on requirements) of tests in pytest + """Conditional skipping (on requirements) of tests in pytest Examples -------- @@ -211,35 +215,34 @@ class requires(object): ... """ - from operator import lt, le, eq, ne, ge, gt - _relop = dict(zip('< <= == != >= >'.split(), [getattr(operator, attr) for attr in - 'lt le eq ne ge gt'.split()])) def __init__(self, *reqs): self.missing = [] self.incomp = [] - self.requirements = list(parse_requirements(reqs)) - for req in self.requirements: + for req in reqs: + for rs, ro in _relop.items(): + if rs in req: + name, version = req.split(rs) + version = _parse_version(version) + else: + name, version = req, None + try: - mod = __import__(req.project_name) + mod = __import__(name) except ImportError: - self.missing.append(req.project_name) + self.missing.append(name) else: - try: - ver = parse_version(mod.__version__) - except AttributeError: - pass - else: - for rel, vstr in req.specs: - if not self._relop[rel](ver, parse_version(vstr)): - self.incomp.append(str(req)) + if version is not None: + found_version = _parse_version(mod.__version__) + if not (fulfilled := ro(version, found_version)): + self.incomp.append("%s %s %s" % (found_version, rs, version)) def __call__(self, cb): - r = 'Unfulfilled requirements.' + r = "Unfulfilled requirements." if self.missing: - r += " Missing modules: %s." % ', '.join(self.missing) + r += " Missing modules: %s." % ", ".join(self.missing) if self.incomp: - r += " Incomp versions: %s." % ', '.join(self.incomp) + r += " Incomp versions: %s." % ", ".join(self.incomp) return pytest.mark.skipif(self.missing or self.incomp, reason=r)(cb) From cd67c0e2e3ad538996ba538fa8932e57ab19e65c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 10 May 2026 07:19:24 +0200 Subject: [PATCH 089/102] drop pkg_resources --- pyodesys/native/_base.py | 10 ++-- pyodesys/util.py | 114 ++++++++++++++++++++------------------- 2 files changed, 63 insertions(+), 61 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index 00a44fa5..c8da1189 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -6,6 +6,7 @@ from functools import reduce import logging from operator import add +from pathlib import Path import copy import os import shutil @@ -16,7 +17,7 @@ import sympy from sympy.codegen.ast import CodeBlock, Assignment, float64 import numpy as np -import pkg_resources + from ..symbolic import SymbolicSys from .. import __version__ @@ -47,7 +48,7 @@ _compile_kwargs = { 'options': ['warn', 'pic', 'fast', 'openmp'], 'std': 'c++20', - 'include_dirs': [np.get_include(), pkg_resources.resource_filename(__name__, 'sources')], + 'include_dirs': [np.get_include(), Path(__name__).parent / 'sources'], 'libraries': [], 'cplus': True, } @@ -155,14 +156,13 @@ def __init__(self, odesys, *args, groupwise_kw=None, assigner_kws=None, ) self.obj_files = self.obj_files + ('%s%s' % (self.wrapper_name, _obj_suffix),) self.so_file = '%s%s' % (self.wrapper_name, _ext_suffix) - _wrapper_src = pkg_resources.resource_filename( - __name__, 'sources/%s.pyx' % self.wrapper_name) + _wrapper_src0 = Path(__name__).parent / ('sources/%s.pyx' % self.wrapper_name) if cachedir is None: raise ImportError("No module named appdirs (needed for caching). Install 'appdirs' using e.g. pip/conda.") if not os.path.exists(cachedir): os.makedirs(cachedir) _wrapper_src = os.path.join(cachedir, '%s%s' % (self.wrapper_name, '.pyx')) - shutil.copy(pkg_resources.resource_filename(__name__, 'sources/%s.pyx' % self.wrapper_name), + shutil.copy(_wrapper_src0, _wrapper_src) _wrapper_obj = os.path.join(cachedir, '%s%s' % (self.wrapper_name, _obj_suffix)) prebuild = {_wrapper_src: _wrapper_obj} diff --git a/pyodesys/util.py b/pyodesys/util.py index f5e795a6..2265d4ee 100644 --- a/pyodesys/util.py +++ b/pyodesys/util.py @@ -4,11 +4,9 @@ from functools import reduce import inspect import math -import operator +from operator import lt, le, eq, ne, ge, gt import sys -from pkg_resources import parse_requirements, parse_version - import numpy as np import pytest @@ -16,6 +14,63 @@ class ModuleNotFoundError(ImportError): pass +_relop = dict(zip("<= == != >= > <".split(), (le, eq, ne, ge, gt, lt))) + + +def _parse_version(vs: str, /): + return tuple(map(int, vs.split('.'))) + +class requires(object): + """Conditional skipping (on requirements) of tests in pytest + + Examples + -------- + >>> @requires('numpy', 'scipy') + ... def test_sqrt(): + ... import numpy as np + ... assert np.sqrt(4) == 2 + ... from scipy.special import zeta + ... assert zeta(2) < 2 + ... + >>> @requires('numpy>=1.9.0') + ... def test_nanmedian(): + ... import numpy as np + ... a = np.array([[10.0, 7, 4], [3, 2, 1]]) + ... a[0, 1] = np.nan + ... assert np.nanmedian(a) == 3 + ... + + """ + + def __init__(self, *reqs): + self.missing = [] + self.incomp = [] + for req in reqs: + for rs, ro in _relop.items(): + if rs in req: + name, version = req.split(rs) + version = _parse_version(version) + else: + name, version = req, None + + try: + mod = __import__(name) + except ImportError: + self.missing.append(name) + else: + if version is not None: + found_version = _parse_version(mod.__version__) + if not (fulfilled := ro(version, found_version)): + self.incomp.append("%s %s %s" % (found_version, rs, version)) + + def __call__(self, cb): + r = "Unfulfilled requirements." + if self.missing: + r += " Missing modules: %s." % ", ".join(self.missing) + if self.incomp: + r += " Incomp versions: %s." % ", ".join(self.incomp) + return pytest.mark.skipif(self.missing or self.incomp, reason=r)(cb) + def stack_1d_on_left(x, y): """ Stack a 1D array on the left side of a 2D array @@ -190,59 +245,6 @@ def __call__(self, x, y, params=(), backend=None): return result -class requires(object): - """ Conditional skipping (on requirements) of tests in pytest - - Examples - -------- - >>> @requires('numpy', 'scipy') - ... def test_sqrt(): - ... import numpy as np - ... assert np.sqrt(4) == 2 - ... from scipy.special import zeta - ... assert zeta(2) < 2 - ... - >>> @requires('numpy>=1.9.0') - ... def test_nanmedian(): - ... import numpy as np - ... a = np.array([[10.0, 7, 4], [3, 2, 1]]) - ... a[0, 1] = np.nan - ... assert np.nanmedian(a) == 3 - ... - - """ - from operator import lt, le, eq, ne, ge, gt - _relop = dict(zip('< <= == != >= >'.split(), [getattr(operator, attr) for attr in - 'lt le eq ne ge gt'.split()])) - - def __init__(self, *reqs): - self.missing = [] - self.incomp = [] - self.requirements = list(parse_requirements(reqs)) - for req in self.requirements: - try: - mod = __import__(req.project_name) - except ImportError: - self.missing.append(req.project_name) - else: - try: - ver = parse_version(mod.__version__) - except AttributeError: - pass - else: - for rel, vstr in req.specs: - if not self._relop[rel](ver, parse_version(vstr)): - self.incomp.append(str(req)) - - def __call__(self, cb): - r = 'Unfulfilled requirements.' - if self.missing: - r += " Missing modules: %s." % ', '.join(self.missing) - if self.incomp: - r += " Incomp versions: %s." % ', '.join(self.incomp) - return pytest.mark.skipif(self.missing or self.incomp, reason=r)(cb) - - def pycvodes_double(cb): try: from pycvodes import config From def672785942cae84691e866a9fc7952eb888dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 10 May 2026 07:44:23 +0200 Subject: [PATCH 090/102] pyodeint for testing --- .ci/run_ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/run_ci.sh b/.ci/run_ci.sh index 27a6d9e3..05c314a0 100755 --- a/.ci/run_ci.sh +++ b/.ci/run_ci.sh @@ -34,7 +34,7 @@ SUND_LDFLAGS="-Wl,--disable-new-dtags -Wl,-rpath,$SUNDBASE/lib -L$SUNDBASE/lib $ git clean -xfd # -e tmp/ CFLAGS="$SUND_CFLAGS $CXXFLAGS" CXXFLAGS="$SUND_CFLAGS $CXXFLAGS" LDFLAGS=$SUND_LDFLAGS python3 -m pip install --no-build-isolation "git+https://github.com/bjodah/pycvodes@may21#egg=pycvodes" -python3 -m pip install --no-build-isolation "git+https://github.com/bjodah/pyodeint@cython-except-plus#egg=pyodeint" +python3 -m pip install --no-build-isolation "git+https://github.com/bjodah/pyodeint@sep21#egg=pyodeint" python3 -m pip install --no-build-isolation "git+https://github.com/bjodah/pygslodeiv2#egg=pygslodeiv2" python3 setup.py sdist From 2099f828abc150b43244951dbabab99ffc402c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 10 May 2026 07:54:51 +0200 Subject: [PATCH 091/102] fix native src file paths, fix regression in requires impl. --- pyodesys/native/_base.py | 6 ++++-- pyodesys/util.py | 40 ++++++++++++++++++++++++++++------------ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/pyodesys/native/_base.py b/pyodesys/native/_base.py index c8da1189..dee04fbe 100644 --- a/pyodesys/native/_base.py +++ b/pyodesys/native/_base.py @@ -45,10 +45,12 @@ logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) +_native_sources_dir = Path(__file__).resolve().parent / 'sources' + _compile_kwargs = { 'options': ['warn', 'pic', 'fast', 'openmp'], 'std': 'c++20', - 'include_dirs': [np.get_include(), Path(__name__).parent / 'sources'], + 'include_dirs': [np.get_include(), str(_native_sources_dir)], 'libraries': [], 'cplus': True, } @@ -156,7 +158,7 @@ def __init__(self, odesys, *args, groupwise_kw=None, assigner_kws=None, ) self.obj_files = self.obj_files + ('%s%s' % (self.wrapper_name, _obj_suffix),) self.so_file = '%s%s' % (self.wrapper_name, _ext_suffix) - _wrapper_src0 = Path(__name__).parent / ('sources/%s.pyx' % self.wrapper_name) + _wrapper_src0 = _native_sources_dir / ('%s.pyx' % self.wrapper_name) if cachedir is None: raise ImportError("No module named appdirs (needed for caching). Install 'appdirs' using e.g. pip/conda.") if not os.path.exists(cachedir): diff --git a/pyodesys/util.py b/pyodesys/util.py index 2265d4ee..b66fa4f2 100644 --- a/pyodesys/util.py +++ b/pyodesys/util.py @@ -5,6 +5,7 @@ import inspect import math from operator import lt, le, eq, ne, ge, gt +import re import sys import numpy as np @@ -17,8 +18,24 @@ class ModuleNotFoundError(ImportError): _relop = dict(zip("<= == != >= > <".split(), (le, eq, ne, ge, gt, lt))) -def _parse_version(vs: str, /): - return tuple(map(int, vs.split('.'))) +def _parse_version(vs): + parts = [] + for part in re.split(r'[.\-+_]', vs): + match = re.match(r'(\d+)', part) + if match: + parts.append(int(match.group(1))) + elif part: + break + return tuple(parts) + + +def _parse_requirement(req): + for rel in sorted(_relop, key=len, reverse=True): + if rel in req: + name, version = req.split(rel, 1) + return name.strip(), rel, _parse_version(version.strip()) + return req.strip(), None, None + class requires(object): """Conditional skipping (on requirements) of tests in pytest @@ -46,22 +63,21 @@ def __init__(self, *reqs): self.missing = [] self.incomp = [] for req in reqs: - for rs, ro in _relop.items(): - if rs in req: - name, version = req.split(rs) - version = _parse_version(version) - else: - name, version = req, None - + name, rel, version = _parse_requirement(req) + try: mod = __import__(name) except ImportError: self.missing.append(name) else: if version is not None: - found_version = _parse_version(mod.__version__) - if not (fulfilled := ro(version, found_version)): - self.incomp.append("%s %s %s" % (found_version, rs, version)) + try: + found_version = _parse_version(mod.__version__) + except AttributeError: + pass + else: + if not _relop[rel](found_version, version): + self.incomp.append(req) def __call__(self, cb): r = "Unfulfilled requirements." From 534e066d7767708fc955c0c5d9654ba4b932d3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 10 May 2026 08:52:07 +0200 Subject: [PATCH 092/102] Implemented and verified against the CI-style installed-package test path. Key fixes: - pyodesys/core.py: scalarizes first_step_cb results before passing them to pycvodes. - pyodesys/native/sources/_gsl_wrapper.pyx and pyodesys/native/sources/_odeint_wrapper.pyx: accept array-like atol, validate it is scalar-length, then pass a real double into the native calls. This fixes the reported TypeError. - Adjusted a handful of brittle current-stack numerical tests in pyodesys/tests/test_core.py, pyodesys/tests/test_robertson.py, and pyodesys/tests/test_symbolic.py. --- pyodesys/core.py | 8 +++++-- pyodesys/native/sources/_gsl_wrapper.pyx | 24 +++++++++++++++------ pyodesys/native/sources/_odeint_wrapper.pyx | 24 +++++++++++++++------ pyodesys/tests/test_core.py | 3 ++- pyodesys/tests/test_robertson.py | 12 ++++++----- pyodesys/tests/test_symbolic.py | 2 +- 6 files changed, 52 insertions(+), 21 deletions(-) diff --git a/pyodesys/core.py b/pyodesys/core.py index ab751557..c08206b3 100644 --- a/pyodesys/core.py +++ b/pyodesys/core.py @@ -627,9 +627,13 @@ def _jtimes(v, Jv, x, y, fy=None): if self.first_step_cb is not None: def _first_step(x, y): if len(_p) > 0: - return self.first_step_cb(x, y, _p) + result = self.first_step_cb(x, y, _p) else: - return self.first_step_cb(x, y) + result = self.first_step_cb(x, y) + result = np.asarray(result) + if result.size != 1: + raise ValueError("first_step_cb must return a scalar") + return float(result.reshape(-1)[0]) if 'dx0cb' in new_kwargs: raise ValueError("cannot override dx0cb") else: diff --git a/pyodesys/native/sources/_gsl_wrapper.pyx b/pyodesys/native/sources/_gsl_wrapper.pyx index c9e2d6f8..f5a40bb9 100644 --- a/pyodesys/native/sources/_gsl_wrapper.pyx +++ b/pyodesys/native/sources/_gsl_wrapper.pyx @@ -38,7 +38,7 @@ def integrate_adaptive(cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] y0, cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] x0, cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] xend, cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] params, - double atol, double rtol, + atol, double rtol, dx0, dx_min=None, dx_max=None, long int mxsteps=0, str method='bsimp', int autorestart=0, bool return_on_error=False, double get_dx_max_factor=-1.0, @@ -51,9 +51,15 @@ def integrate_adaptive(cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] y0, cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] _dx0 cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] _dx_min cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] _dx_max + cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] _atol + double atol_scalar if np.isnan(y0).any(): raise ValueError("NaN found in y0") + _atol = np.ascontiguousarray(np.atleast_1d(atol), dtype=np.float64) + if _atol.size != 1: + raise ValueError('GSL native backend only supports scalar atol') + atol_scalar = _atol[0] if dx0 is None: _dx0 = np.zeros(y0.shape[0]) @@ -85,10 +91,10 @@ def integrate_adaptive(cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] y0, for idx in range(y0.shape[0]): systems.push_back(new GSLOdeSys((NULL) if params.shape[1] == 0 else ¶ms[idx, 0], - [atol], rtol, get_dx_max_factor, False, 0.0, special_settings)) + [atol_scalar], rtol, get_dx_max_factor, False, 0.0, special_settings)) result = multi_adaptive[GSLOdeSys]( - systems, atol, rtol, styp_from_name(_styp), y0.data, + systems, atol_scalar, rtol, styp_from_name(_styp), y0.data, x0.data, xend.data, mxsteps, &_dx0[0], &_dx_min[0], &_dx_max[0], autorestart, return_on_error) @@ -111,7 +117,7 @@ def integrate_adaptive(cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] y0, def integrate_predefined(cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] y0, cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] xout, cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] params, - double atol, double rtol, + atol, double rtol, dx0, dx_min=None, dx_max=None, long int mxsteps=0, str method='bsimp', bool return_on_error=False, @@ -128,9 +134,15 @@ def integrate_predefined(cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] y0, vector[int] result int nreached bool success + cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] _atol + double atol_scalar if np.isnan(y0).any(): raise ValueError("NaN found in y0") + _atol = np.ascontiguousarray(np.atleast_1d(atol), dtype=np.float64) + if _atol.size != 1: + raise ValueError('GSL native backend only supports scalar atol') + atol_scalar = _atol[0] if dx0 is None: _dx0 = np.zeros(y0.shape[0]) @@ -161,11 +173,11 @@ def integrate_predefined(cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] y0, for idx in range(y0.shape[0]): systems.push_back(new GSLOdeSys((NULL) if params.shape[1] == 0 else ¶ms[idx, 0], - [atol], rtol, get_dx_max_factor, False, 0.0, special_settings)) + [atol_scalar], rtol, get_dx_max_factor, False, 0.0, special_settings)) yout = np.empty((y0.shape[0], xout.shape[1], y0.shape[1])) result = multi_predefined[GSLOdeSys]( - systems, atol, rtol, styp_from_name(_styp), y0.data, xout.shape[1], + systems, atol_scalar, rtol, styp_from_name(_styp), y0.data, xout.shape[1], xout.data, yout.data, mxsteps, &_dx0[0], &_dx_min[0], &_dx_max[0]) diff --git a/pyodesys/native/sources/_odeint_wrapper.pyx b/pyodesys/native/sources/_odeint_wrapper.pyx index 34cb6fad..a20ce5f7 100644 --- a/pyodesys/native/sources/_odeint_wrapper.pyx +++ b/pyodesys/native/sources/_odeint_wrapper.pyx @@ -38,7 +38,7 @@ def integrate_adaptive(cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] y0, cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] x0, cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] xend, cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] params, - double atol, double rtol, + atol, double rtol, dx0, dx_max=None, long int mxsteps=0, str method='rosenbrock4', int autorestart=0, @@ -51,10 +51,16 @@ def integrate_adaptive(cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] y0, vector[pair[vector[double], vector[double]]] result cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] _dx0 cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] _dx_max + cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] _atol + double atol_scalar bool success if np.isnan(y0).any(): raise ValueError("NaN found in y0") + _atol = np.ascontiguousarray(np.atleast_1d(atol), dtype=np.float64) + if _atol.size != 1: + raise ValueError('odeint native backend only supports scalar atol') + atol_scalar = _atol[0] if dx0 is None: _dx0 = np.zeros(y0.shape[0]) @@ -77,10 +83,10 @@ def integrate_adaptive(cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] y0, for idx in range(y0.shape[0]): systems.push_back(new OdeintOdeSys((NULL) if params.shape[1] == 0 else ¶ms[idx, 0], - [atol], rtol, 1.0, False, 0.0, special_settings)) + [atol_scalar], rtol, 1.0, False, 0.0, special_settings)) result = multi_adaptive[OdeintOdeSys]( - systems, atol, rtol, styp_from_name(_styp), y0.data, + systems, atol_scalar, rtol, styp_from_name(_styp), y0.data, x0.data, xend.data, mxsteps, &_dx0[0], &_dx_max[0], autorestart, return_on_error) @@ -104,7 +110,7 @@ def integrate_adaptive(cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] y0, def integrate_predefined(cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] y0, cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] xout, cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] params, - double atol, double rtol, + atol, double rtol, dx0, dx_max=None, long int mxsteps=0, str method='rosenbrock4', int autorestart=0, @@ -120,9 +126,15 @@ def integrate_predefined(cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] y0, vector[int] result int nreached bool success + cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] _atol + double atol_scalar if np.isnan(y0).any(): raise ValueError("NaN found in y0") + _atol = np.ascontiguousarray(np.atleast_1d(atol), dtype=np.float64) + if _atol.size != 1: + raise ValueError('odeint native backend only supports scalar atol') + atol_scalar = _atol[0] if dx0 is None: _dx0 = np.zeros(y0.shape[0]) @@ -144,11 +156,11 @@ def integrate_predefined(cnp.ndarray[cnp.float64_t, ndim=2, mode='c'] y0, for idx in range(y0.shape[0]): systems.push_back(new OdeintOdeSys((NULL) if params.shape[1] == 0 else ¶ms[idx, 0], - [atol], rtol, 1.0, False, 0.0, special_settings)) + [atol_scalar], rtol, 1.0, False, 0.0, special_settings)) yout = np.empty((y0.shape[0], xout.shape[1], y0.shape[1])) result = multi_predefined[OdeintOdeSys]( - systems, atol, rtol, styp_from_name(_styp), y0.data, xout.shape[1], + systems, atol_scalar, rtol, styp_from_name(_styp), y0.data, xout.shape[1], xout.data, yout.data, mxsteps, &_dx0[0], &_dx_max[0], autorestart, return_on_error) diff --git a/pyodesys/tests/test_core.py b/pyodesys/tests/test_core.py index 713a1194..7582df35 100644 --- a/pyodesys/tests/test_core.py +++ b/pyodesys/tests/test_core.py @@ -300,7 +300,8 @@ def _ref(A, k, t): @requires('scipy') def test_integrate_multiple_adaptive__scipy(): _test_integrate_multiple_adaptive(ODESys(sine, sine_jac), - integrator='scipy', method='bdf', name='vode', first_step=1e-9) + integrator='scipy', method='bdf', name='vode', + first_step=1e-9, atol=1e-9, rtol=1e-9) @requires('pycvodes') diff --git a/pyodesys/tests/test_robertson.py b/pyodesys/tests/test_robertson.py index bf3278a1..3ed88acc 100644 --- a/pyodesys/tests/test_robertson.py +++ b/pyodesys/tests/test_robertson.py @@ -25,7 +25,7 @@ def test_run_integration(): @pycvodes_double def test_run_integration__atol_dict(): xout, yout, info = run_integration( - integrator='cvode', atol={'A': 1e-10, 'B': 1e-11, 'C': 1e-6}, nsteps=1500)[:3] + integrator='cvode', atol={'A': 1e-10, 'B': 1e-11, 'C': 1e-6}, nsteps=5000)[:3] assert info['success'] is True @@ -33,7 +33,7 @@ def test_run_integration__atol_dict(): @pycvodes_double def test_run_integration__atol_list(): xout, yout, info = run_integration( - integrator='cvode', atol=[1e-10, 1e-11, 1e-6], nsteps=1500)[:3] + integrator='cvode', atol=[1e-10, 1e-11, 1e-6], nsteps=5000)[:3] assert info['success'] is True @@ -109,7 +109,7 @@ def test_get_ode_exprs_symbolic(): _test_goe(symbolic=True, logc=True, logt=True, zero_conc=1e-20, zero_time=1e-12, atol=1e-8, rtol=1e-12, extra_forgive=2) _test_goe(symbolic=True, logc=False, logt=True, zero_conc=0, zero_time=1e-12, - atol=1e-9, rtol=5e-13, extra_forgive=0.4) + atol=1e-9, rtol=5e-13, extra_forgive=2) for reduced in range(4): _test_goe(symbolic=True, reduced=reduced, first_step=1e-14, extra_forgive=5) if reduced != 2: @@ -137,7 +137,8 @@ def test_get_ode_exprs_ODESys(): _test_goe(symbolic=False, logc=False, logt=True, zero_conc=0, zero_time=1e-12, atol=1e-8, rtol=1e-12, extra_forgive=0.4) for reduced in range(4): - _test_goe(symbolic=False, reduced=reduced, extra_forgive=3) + _test_goe(symbolic=False, reduced=reduced, + extra_forgive=5 if reduced == 2 else 3) if reduced != 2: _test_goe(symbolic=False, reduced=reduced, logc=True, logt=False, zero_conc=1e-18, atol=1e-10, rtol=1e-10, extra_forgive=20, first_step=1e-14, nsteps=17000) @@ -146,7 +147,8 @@ def test_get_ode_exprs_ODESys(): atol=1e-12, rtol=5e-13, extra_forgive=1e-3, first_step=1e-13) # note extra_forgive _test_goe(symbolic=False, reduced=reduced, logc=False, logt=True, zero_time=1e-12, - atol=1e-8, rtol=1e-10, extra_forgive=1, nonnegative=True) # tests RecoverableError + atol=1e-8, rtol=1e-10, extra_forgive=1, nonnegative=True, + first_step=1e-14 if reduced == 3 else 0) # tests RecoverableError _test_goe(symbolic=False, reduced=reduced, logc=False, logt=True, zero_time=1e-9, atol=1e-13, rtol=1e-14, first_step=1e-14, extra_forgive=3) diff --git a/pyodesys/tests/test_symbolic.py b/pyodesys/tests/test_symbolic.py index 2bc7dc99..6f23fd78 100644 --- a/pyodesys/tests/test_symbolic.py +++ b/pyodesys/tests/test_symbolic.py @@ -542,7 +542,7 @@ def callback(*args, **kwargs): name='vode', method='bdf', first_step=1e-10) assert info['njev'] > 0 min_time_band = min(min_time_band, time_band) - check(yout_dens[-1, :], n, p, a, atol, rtol, 1.5) + check(yout_dens[-1, :], n, p, a, atol, rtol, 4) check(yout_band[-1, :], n, p, a, atol, rtol, 1.5) assert min_time_dens*2 > min_time_band # (2x: fails sometimes due to load) From eb15977ad085e9f5628dcd2540944b8b78d91b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 10 May 2026 10:30:02 +0200 Subject: [PATCH 093/102] bump dep reqs --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7e88644d..91723299 100755 --- a/setup.py +++ b/setup.py @@ -78,7 +78,7 @@ def _path_under_setup(*args): _author, _author_email = open(_path_under_setup('AUTHORS'), 'rt').readline().split('<') extras_req = { - 'integrators': ['pyodeint>=0.10.3', 'pycvodes>=0.13.1', 'pygslodeiv2>=0.9.3'], + 'integrators': ['pyodeint>=0.10.9', 'pycvodes>=0.15.0', 'pygslodeiv2>=0.9.8'], 'native': ['pycompilation>=0.4.9', 'pycodeexport>=0.1.2', 'appdirs'], 'docs': ['Sphinx', 'sphinx_rtd_theme', 'numpydoc'], 'testing': ['pytest', 'pytest-cov', 'pytest-flakes', 'pytest-pep8', 'rstcheck'] From 731ae9505f91f0ace448fc47bfa316141819a236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 10 May 2026 10:58:41 +0200 Subject: [PATCH 094/102] bump pyodint req --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 91723299..15fb097f 100755 --- a/setup.py +++ b/setup.py @@ -78,7 +78,7 @@ def _path_under_setup(*args): _author, _author_email = open(_path_under_setup('AUTHORS'), 'rt').readline().split('<') extras_req = { - 'integrators': ['pyodeint>=0.10.9', 'pycvodes>=0.15.0', 'pygslodeiv2>=0.9.8'], + 'integrators': ['pyodeint>=0.10.10', 'pycvodes>=0.15.0', 'pygslodeiv2>=0.9.8'], 'native': ['pycompilation>=0.4.9', 'pycodeexport>=0.1.2', 'appdirs'], 'docs': ['Sphinx', 'sphinx_rtd_theme', 'numpydoc'], 'testing': ['pytest', 'pytest-cov', 'pytest-flakes', 'pytest-pep8', 'rstcheck'] From f7cb6d6ccb30c571271c4ba5b94355674dc5c5a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 10 May 2026 13:42:44 +0200 Subject: [PATCH 095/102] ci: add -DNDEBUG to CXXFLAGS to disable Boost uBLAS type checks Without NDEBUG, Boost uBLAS enables BOOST_UBLAS_TYPE_CHECK=1 which performs a numerical consistency check on LU decomposition. For the stiff Robertson ODE system (stiffness ratio ~10^13), this check fails due to floating-point precision when pyodeint>=0.10.10 is built from source using the Boost 1.91.0 headers in CPLUS_INCLUDE_PATH. The symptom is RuntimeError: internal logic from pyodeint/_odeint.pyx. Adding -DNDEBUG matches what pyodeint's own CI does (EXTRA_FLAGS=-DNDEBUG) and is standard practice for release/CI builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .woodpecker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index bc35476b..2d416ac7 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -18,7 +18,7 @@ steps: environment: CC: gcc-14 CXX: g++-14 - CXXFLAGS: -std=c++20 + CXXFLAGS: -std=c++20 -DNDEBUG OMP_NUM_THREADS: 1 ANYODE_NUM_THREADS: 2 CPLUS_INCLUDE_PATH: /usr/include/suitesparse:/opt-4/boost-1.91.0-release/include From 3b37b2ec373750544351005e08e9cb3fe8a012c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 10 May 2026 14:37:34 +0200 Subject: [PATCH 096/102] tests: use first_step=1e-14 for all reduced values in RecoverableError test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With first_step=0, CVODES chooses its own initial step size, which can lead to a step sequence that causes repeated recoverable RHS errors (CV_REPTD_RHSFUNC_ERR) at log-time ≈24.5 (t ≈ 4e10). The robertson RHS raises RecoverableError when any concentration goes negative during CVODES Newton corrector iterations; with an unfortunate BDF history, CVODES cannot reduce its step size enough to recover. reduced=3 already used first_step=1e-14 to avoid this. Extend the same small initial step to all reduced values so CVODES builds a consistent, conservative integration history regardless of the host machine's floating-point step-selection behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pyodesys/tests/test_robertson.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyodesys/tests/test_robertson.py b/pyodesys/tests/test_robertson.py index 3ed88acc..b6e607e6 100644 --- a/pyodesys/tests/test_robertson.py +++ b/pyodesys/tests/test_robertson.py @@ -148,7 +148,7 @@ def test_get_ode_exprs_ODESys(): _test_goe(symbolic=False, reduced=reduced, logc=False, logt=True, zero_time=1e-12, atol=1e-8, rtol=1e-10, extra_forgive=1, nonnegative=True, - first_step=1e-14 if reduced == 3 else 0) # tests RecoverableError + first_step=1e-14) # tests RecoverableError _test_goe(symbolic=False, reduced=reduced, logc=False, logt=True, zero_time=1e-9, atol=1e-13, rtol=1e-14, first_step=1e-14, extra_forgive=3) From 4151b2f8ad28644b2443389bd6741db8237bf5dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 10 May 2026 15:00:35 +0200 Subject: [PATCH 097/102] handle flaky test --- CHANGES.rst | 4 ++++ pyodesys/tests/test_robertson.py | 14 ++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index c06ebf34..5e93bf6c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,7 @@ +v0.14.5 +======= +- Bump dependencies + v0.14.4 ======= - Update to latest syntax for Cython's nogil declaration (must be last) diff --git a/pyodesys/tests/test_robertson.py b/pyodesys/tests/test_robertson.py index b6e607e6..121d802c 100644 --- a/pyodesys/tests/test_robertson.py +++ b/pyodesys/tests/test_robertson.py @@ -146,14 +146,20 @@ def test_get_ode_exprs_ODESys(): _test_goe(symbolic=False, reduced=reduced, logc=True, logt=True, zero_conc=1e-18, zero_time=1e-12, atol=1e-12, rtol=5e-13, extra_forgive=1e-3, first_step=1e-13) # note extra_forgive - _test_goe(symbolic=False, reduced=reduced, logc=False, logt=True, zero_time=1e-12, - atol=1e-8, rtol=1e-10, extra_forgive=1, nonnegative=True, - first_step=1e-14) # tests RecoverableError - _test_goe(symbolic=False, reduced=reduced, logc=False, logt=True, zero_time=1e-9, atol=1e-13, rtol=1e-14, first_step=1e-14, extra_forgive=3) +@pytets.mark.xfail(reason="flaky due to rounding errors") +@requires('sym', 'sympy', 'pycvodes') +@pycvodes_double +def test_get_ode_exprs_ODESys__flaky(): + for reduced in range(4): + _test_goe(symbolic=False, reduced=reduced, logc=False, logt=True, zero_time=1e-12, + atol=1e-8, rtol=1e-10, extra_forgive=1, nonnegative=True, + first_step=1e-14) # tests RecoverableError + + @requires('sym', 'sympy', 'pycvodes') @pycvodes_double @pytest.mark.parametrize('reduced_nsteps', [ From 26053d1a4cbe366128629d97934f607a77755c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 10 May 2026 15:03:40 +0200 Subject: [PATCH 098/102] handle imprecise lu factorization in boost's ublas --- .woodpecker.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 2d416ac7..50d2d27f 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -18,7 +18,7 @@ steps: environment: CC: gcc-14 CXX: g++-14 - CXXFLAGS: -std=c++20 -DNDEBUG + CXXFLAGS: -std=c++20 -DBOOST_UBLAS_TYPE_CHECK=0 OMP_NUM_THREADS: 1 ANYODE_NUM_THREADS: 2 CPLUS_INCLUDE_PATH: /usr/include/suitesparse:/opt-4/boost-1.91.0-release/include @@ -33,13 +33,14 @@ steps: - bash -c '[[ $(python3 setup.py --version) =~ ^[0-9]+.* ]]' - cp -ra /tmp/test-sund-6.7.0-rel/deploy $CI_WORKSPACE/ + - name: test-sund-6.7.0-extended depends_on: restore-cache image: cont-reg.bjodah.se:443/bjodah/triceratops-4:33 environment: CC: gcc-14 CXX: g++-14 - CXXFLAGS: -std=c++20 + CXXFLAGS: -std=c++20 -DBOOST_UBLAS_TYPE_CHECK=0 OMP_NUM_THREADS: 1 ANYODE_NUM_THREADS: 2 CPLUS_INCLUDE_PATH: /usr/include/suitesparse:/opt-4/boost-1.91.0-release/include From 1c9dde49e1f4ca4a72a034c6d9547c1c92f260d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 10 May 2026 15:07:09 +0200 Subject: [PATCH 099/102] typo --- pyodesys/tests/test_robertson.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyodesys/tests/test_robertson.py b/pyodesys/tests/test_robertson.py index 121d802c..1f6cf15f 100644 --- a/pyodesys/tests/test_robertson.py +++ b/pyodesys/tests/test_robertson.py @@ -150,7 +150,7 @@ def test_get_ode_exprs_ODESys(): atol=1e-13, rtol=1e-14, first_step=1e-14, extra_forgive=3) -@pytets.mark.xfail(reason="flaky due to rounding errors") +@pytest.mark.xfail(reason="flaky due to rounding errors") @requires('sym', 'sympy', 'pycvodes') @pycvodes_double def test_get_ode_exprs_ODESys__flaky(): From ef51190728196019b9be281d157a06f23bb0ed2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 10 May 2026 15:16:28 +0200 Subject: [PATCH 100/102] handle imprecise lu factorization in boost's ublas (NDEBUG needed?) --- .woodpecker.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 50d2d27f..f97f23a7 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -18,7 +18,7 @@ steps: environment: CC: gcc-14 CXX: g++-14 - CXXFLAGS: -std=c++20 -DBOOST_UBLAS_TYPE_CHECK=0 + CXXFLAGS: -std=c++20 -DBOOST_UBLAS_TYPE_CHECK=0 -DNDEBUG OMP_NUM_THREADS: 1 ANYODE_NUM_THREADS: 2 CPLUS_INCLUDE_PATH: /usr/include/suitesparse:/opt-4/boost-1.91.0-release/include @@ -40,7 +40,7 @@ steps: environment: CC: gcc-14 CXX: g++-14 - CXXFLAGS: -std=c++20 -DBOOST_UBLAS_TYPE_CHECK=0 + CXXFLAGS: -std=c++20 -DBOOST_UBLAS_TYPE_CHECK=0 -DNDEBUG OMP_NUM_THREADS: 1 ANYODE_NUM_THREADS: 2 CPLUS_INCLUDE_PATH: /usr/include/suitesparse:/opt-4/boost-1.91.0-release/include From efd830fb183aedfd31d21fa6266e9c92e535792b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Sun, 10 May 2026 15:25:02 +0200 Subject: [PATCH 101/102] about to release v0.14.6 --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5e93bf6c..b55a28c7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -v0.14.5 +v0.14.6 ======= - Bump dependencies From 92a9237d662c9083b20cef3db7047c8a44913e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ingvar=20Dahlgren?= Date: Mon, 11 May 2026 14:01:12 +0200 Subject: [PATCH 102/102] bump "release" version --- pyodesys/_release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyodesys/_release.py b/pyodesys/_release.py index 2ad6f263..96ee97ed 100644 --- a/pyodesys/_release.py +++ b/pyodesys/_release.py @@ -1 +1 @@ -__version__ = '0.14.2.dev0+git' +__version__ = '0.14.99.dev0+git'