Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstraction for init value #64

Closed
SamuelBrand1 opened this issue Feb 23, 2024 · 10 comments · Fixed by #74
Closed

Abstraction for init value #64

SamuelBrand1 opened this issue Feb 23, 2024 · 10 comments · Fixed by #74
Assignees

Comments

@SamuelBrand1
Copy link
Collaborator

Current API

For each latent process, an init scalar value is generated along with the latent process itself. The interpretation of init is typically $\log I(0)$; that is the log-incidence rate on the time step before data.

Problem

Although this API "works"; it locks in the interpretation above and makes iteration to other models that might want more structured initialisation difficult.

Solution

Create an initializer sub-model to be combined with latent processes and observation models.

@SamuelBrand1
Copy link
Collaborator Author

Alternate solution

In epidemia, the inference priors for initialisation "live" with the epiinfs model. At the moment we use EpiModel objects to transform latent process to latent infections; which corresponds to the epiinfs epidemia model when we choose Renewal.

Therefore, it would seem to be natural to add initialisation of latent infections to this struct.

@seabbs
Copy link
Collaborator

seabbs commented Feb 23, 2024

Hmm yes I think I agree with the alternative solution here? That does introduce a problem with priors maybe (as back to a shared prior list?)

@SamuelBrand1
Copy link
Collaborator Author

Hmm yes I think I agree with the alternative solution here? That does introduce a problem with priors maybe (as back to a shared prior list?)

I think we can keep a consistent pattern here.

LatentProcess type structs have

  • a Turing model constructor function
  • Priors for latent process

ObservationModel type structs have

  • a Turing model constructor function
  • Priors for observation model

Proposed API for alternate solution would be that the

<: AbstractEpiModel structs have

  • a Turing model constructor function (for init)
  • Prior(s) for initialisation
  • EpiData object for holding information on (say) GI
  • a callable function defined for the struct which implements latent process + init -> latent infections

Obviously a bit more is happening in the <: AbstractEpiModel struct but that feels consistent and makes sense.

@SamuelBrand1
Copy link
Collaborator Author

Btw, in comparison to epidemia.

  • LatentProcess <-> epirt
  • ObservationModel <-> epiobs
  • <: AbstractEpiModel <-> epiinf

Main difference is that the subtype of <: AbstractEpiModel dispatches the target of the latent process.

@seabbs
Copy link
Collaborator

seabbs commented Feb 23, 2024

I think this sounds good. Just a note whilst we are here but I think currently that EpiData is inconsistent as it includes data on the delays for the observation process in a way that doesn't seem to make sense?

Another tangent but why do we have Abstract in the name of just one parent struct (i.e and not in LatentProcess etc.)?

@SamuelBrand1
Copy link
Collaborator Author

Another tangent but why do we have Abstract in the name of just one parent struct (i.e and not in LatentProcess etc.)?

Because we have three different concrete subtypes of AbstractEpiModel but we don't need that for LatentProcess.

Obviously, we could just have a single type EpiModel and the latent process + init -> latent infs function could be defined in that struct (rather than a Callable that dispatches on subtype). But I like the way we do it because I find it expressive (this is pretty subjective).

@SamuelBrand1
Copy link
Collaborator Author

I think this sounds good. Just a note whilst we are here but I think currently that EpiData is inconsistent as it includes data on the delays for the observation process in a way that doesn't seem to make sense?

Yes, that has been annoying me too.

@seabbs
Copy link
Collaborator

seabbs commented Feb 26, 2024

Because we have three different concrete subtypes of AbstractEpiModel but we don't need that for LatentProcess.

I guess my question is why aren't we using a struct with a dict and function pairing vs this.

Doing some reading (the manual and https://stackoverflow.com/questions/60218078/why-create-an-abstract-super-type-in-julia) it looks like abstract structs are all about inheritance and grouping of related objects.

So here we are making some kind of contract that AbstractEpiModel will always use EpiData (except my read of the docs is that we are not doing this at the moment) and we could extend this to have a default (empty function) that can then be overloaded in inheriting structs?

So my question is this seems like a useful way to do things so why don't we want the same for LatentProcess (i.e. RandomWalkLatentProcess etc.)? Or not need it for either (as we currently aren't doing any inheritance using out abstract struct?

@SamuelBrand1
Copy link
Collaborator Author

SamuelBrand1 commented Feb 26, 2024

So here we are making some kind of contract that AbstractEpiModel will always use EpiData (except my read of the docs is that we are not doing this at the moment) and we could extend this to have a default (empty function) that can then be overloaded in inheriting structs?

At the moment, we're implementing the latent process -> latent infections function (which defines the target of the inference) using a callable on the <:AbstractEpiModel struct, e.g.

function (epimodel::Renewal)(_Rt, init)
I₀ = epimodel.data.transformation(init)
Rt = epimodel.data.transformation.(_Rt)
r_approx = R_to_r(Rt[1], epimodel)
init = I₀ * [exp(-r_approx * t) for t in 0:(epimodel.data.len_gen_int - 1)]
function generate_infs(recent_incidence, Rt)
new_incidence = Rt * dot(recent_incidence, epimodel.data.gen_int)
[new_incidence; recent_incidence[1:(epimodel.data.len_gen_int - 1)]], new_incidence
end
I_t, _ = scan(generate_infs, init, Rt)
return I_t
end

Which does use EpiData information, and the concrete type. This isn't the only or even best way to implement this pattern? I'm influenced by the pattern in LogDensityProblems here.

So my question is this seems like a useful way to do things so why don't we want the same for LatentProcess (i.e. RandomWalkLatentProcess etc.)? Or not need it for either (as we currently aren't doing any inheritance using out abstract struct?

At the moment we aren't doing any inheritance for the LatentProcess; its implementation is down to the Turing model constructor passed to it (the latent_process field). To tell the truth, this was the due to me not being sure how inheritance would work since the Turing model constructor functions are themselves generated by the @model macro at compilation time for EpiAware.

As the example you found suggested, the abstract super-type pattern is particularly useful if there is a default behaviour we'd want from the latent process that we might specialise on with concrete sub-types. In this case, I can't think of one but I'm open to suggestions.

@seabbs
Copy link
Collaborator

seabbs commented Feb 26, 2024

In this case, I can't think of one but I'm open to suggestions.

What about for the args splitting?

@seabbs seabbs added this to the EpiAware 0.1.0 milestone Feb 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants