How to define a Priors Parameter that change over time

I built a dual SIR model where my infectivity parameter varied sinusoidally over time so that I could simulate years of data rather than just one period. But I’m having trouble with the code.

Here is the code:

    def dual_SIR_sunode(self, t, y, p):
        # define β1/2(t) = B * (1 + b * sin(2 * pi * t / 365))
        beta1_t = p.B1 * (1 + p.b1 * pm.math.sin(2 * np.pi * t / 52))
        beta2_t = p.B2 * (1 + p.b2 * pm.math.sin(2 * np.pi * t / 52))

        #  I1_prime and I2_prime
        I1_prime = y.IS + y.II + y.IR
        I2_prime = y.SI + y.II + y.RI

        return {
            'SS': p.sigma1 * y.RS + p.sigma2 * y.SR - beta1_t * y.SS * I1_prime / y.n - beta2_t * y.SS * I2_prime / y.n,
            'IS': beta1_t * y.SS * I1_prime / y.n + p.sigma2 * y.IR - p.gamma1 * y.IS - beta2_t * (1 - p.phi12) * y.IS * I2_prime / y.n,
            'RS': p.sigma2 * y.RR + p.gamma1 * y.IS - p.sigma1 * y.RS - beta2_t * y.SS * I2_prime / y.n,

            'SI': p.sigma1 * y.RI + beta2_t * y.SS * I2_prime / y.n - beta1_t * (1 - p.phi21) * y.SI * I1_prime / y.n - p.gamma2 * y.SI ,
            'II': beta1_t * (1 - p.phi21) * y.SI * I1_prime / y.n + beta2_t * (1 - p.phi12) * y.IS * I2_prime / y.n - p.gamma1 * y.II - p.gamma2 * y.II,
            'RI': p.gamma1 * y.II + beta2_t * y.RS * I2_prime / y.n - p.sigma1 * y.RI - p.gamma2 * y.RI,

            'SR': p.sigma1 * y.RR + p.gamma2 * y.SI - beta1_t * y.SR * I1_prime / y.n - p.sigma2 * y.SR ,
            'IR': beta1_t * y.SR * I1_prime / y.n + p.gamma2 * y.II - p.gamma1 * y.IR - p.sigma2 * y.IR ,
            'RR': p.gamma1 * y.IR + p.gamma2 * y.RI - p.sigma1 * y.RR - p.sigma2 * y.RR 
        }
    def run_SEIRS_model(self):
        with pm.Model() as self.model:
            # Priors for unknown model parameters
            B1 = pm.Normal('B1', mu=0.5, sigma=0.1)
            b1 = pm.Normal('b1', mu=0.1, sigma=0.02)

            B2 = pm.Normal('B2', mu=0.5, sigma=0.1)
            b2 = pm.Normal('b2', mu=0.1, sigma=0.02)
            
            phi12 = pm.Beta('phi12', alpha=2, beta=5)
            phi21 = pm.Beta('phi21', alpha=2, beta=5)

            sigma1 = pm.Normal('sigma1', mu=0.1, sigma=0.05)
            sigma2 = pm.Normal('sigma2', mu=0.1, sigma=0.05)
            gamma1 = pm.Normal('gamma1', mu=0.1, sigma=0.02)
            gamma2 = pm.Normal('gamma2', mu=0.1, sigma=0.02)


            res, _, problem, solver, _, _ = sunode.wrappers.as_pytensor.solve_ivp(
            y0={
            # The initial conditions of the ode. Each variable
            # needs to specify a theano or numpy variable and a shape.
            # This dict can be nested.
                'SS': (self.init_y['SS'], ()),
                'IS': (self.init_y['IS'], ()),
                'RS': (self.init_y['RS'], ()),
                'SI': (self.init_y['SI'], ()),
                'II': (self.init_y['II'], ()),
                'RI': (self.init_y['RI'], ()),
                'SR': (self.init_y['SR'], ()),
                'IR': (self.init_y['IR'], ()),
                'RR': (self.init_y['RR'], ()),
                'n': (self.init_y['n'], ())
            },
            params={
            # Each parameter of the ode. sunode will only compute derivatives
            # with respect to theano variables. The shape needs to be specified
            # as well. It it infered automatically for numpy variables.
            # This dict can be nested.
                'B1': (B1, ()),
                'b1': (b1, ()),
                'B2': (B2, ()),
                'b2': (b2, ()),
                
                'phi12': (phi12, ()),
                'phi21': (phi21, ()),

                'sigma1': (sigma1, ()),
                'sigma2': (sigma2, ()),
                'gamma1': (gamma1, ()),
                'gamma2': (gamma2, ()),

                '_dummy': (np.array(1.), ()),
            },
            # A functions that computes the right-hand-side of the ode using
            # sympy variables.
            rhs=self.dual_SIR_sunode,
            # The time points where we want to access the solution
            tvals=self.times[1:],
            t0=self.times[0]
            )

            # The solution of the ode. This is a dictionary with the same structure
            # as the initial conditions.
            I1_total = res['IS'] + res['II'] + res['IR']
            I2_total = res['SI'] + res['II'] + res['RI']

        # Define the likelihood function
        I1_obs = pm.Normal('I1_obs', mu=I1_total, sigma=0.1, observed=self.Dieases1_num[1:])
        I2_obs = pm.Normal('I2_obs', mu=I2_total, sigma=0.1, observed=self.Dieases2_num[1:])

        # Sample the model
        with self.model:
            trace = pm.sample(**self.samples_params)
        return trace

However error happens on # define β1/2(t) = B * (1 + b * sin(2 * pi * t / 365)): NotImplementedError: Cannot convert 0.120830486676531time to a tensor variable.

I know this won’t work, but I can’t think of any other way to pass in the t parameter as a numerical value. I tried defind it outside the dual_SIR_sunode function then I can’t get the time parameter, like this:

Maybe someone has experience in this regard? Thanks to all who provided suggestions!

Use sympy instead of numpy

        beta1_t = p.B1 * (1 + p.b1 * sympy.sin(2 * np.pi * (t) / 52))
        beta2_t = p.B2 * (1 + p.b2 * sympy.sin(2 * np.pi * (t) / 52))

:rofl:

This is not a solution, and if it works, it works entirely by accident. The computational backend for pymc is pytensor, and all math functions should use their pytensor equivalents (e.g. pytensor.tensor.sin).

It’s not an accident. In fact I found out the t is a symbol type:

And when I tried pytensor.tensor.sin:

@aseyboldt

Sorry for the late reply…
Sympy is correct here, the right hand side function is defined via sympy in sunode (A pytensor backend would be nice, but that wasn’t an option back when I wrote sunode).