Skip to content

Interface for Multivariate processes - #92

Open
JoseKling wants to merge 7 commits into
mainfrom
MultivariateHawkes
Open

Interface for Multivariate processes#92
JoseKling wants to merge 7 commits into
mainfrom
MultivariateHawkes

Conversation

@JoseKling

Copy link
Copy Markdown
Owner

This PR has the objective of implementing multivariate Hawkes processes.
In the process, some other modifications were carried out.

  • Split implementation of univariate Hawkes process into three files and added mark distributions
  • Defined separate common interfaces for AbstractUnivariateProcess and AbstractMultivariateProcess

JoseKling added 2 commits May 28, 2026 19:44
- Added mark distribution to the process
- Added `univariate_process.jl` and `multivariate_process.jl` files
  defining the common interfaces
- Updated the multivariate processes to comply with the interface
- Split univariate Hawkes implementation into thre files
@JoseKling JoseKling mentioned this pull request Jun 2, 2026
Comment thread src/abstract_point_process.jl Outdated
Comment thread src/multivariate_process.jl
Comment thread src/univariate/hawkes/fit.jl Outdated
Comment thread src/univariate/hawkes/hawkes_process.jl Outdated
Comment thread src/univariate_process.jl Outdated
Comment thread src/univariate_process.jl Outdated
Comment thread test/hawkes.jl Outdated
Comment thread test/mark_distributions.jl Outdated
@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.90741% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/bounded_point_process.jl 44.44% 5 Missing ⚠️
src/multivariate_process.jl 88.57% 4 Missing ⚠️
src/history.jl 94.28% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@JoseKling

Copy link
Copy Markdown
Owner Author

I figured it would be a good idea to split this PR in two.
This part only fixes the univariate Hawkes process and defines the interface for multivariate processes.
The next PR will be the implementation of multivariate Hawkes processes.

@JoseKling JoseKling changed the title Multivariate hawkes Interface for Multivariate processes Jun 21, 2026
@JoseKling
JoseKling marked this pull request as ready for review June 21, 2026 16:33
@rsenne

rsenne commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Is this ready for me to review?

@JoseKling

Copy link
Copy Markdown
Owner Author

Sorry. I guess I forgot to request the review. But it should be ready, yes

@rsenne
rsenne self-requested a review June 27, 2026 20:59
Comment thread src/abstract_point_process.jl Outdated
Comment thread src/univariate/hawkes/hawkes_process.jl Outdated
descendants = T[]
next_gen = immigrants
while !isempty(next_gen)
# OPTIMIZE: Can this be improved by avoiding allocations of `curr_gen` and `next_gen`? Or does the compiler take care of that?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably. we would want to make a workspace type that we use inplace operations on

Comment thread src/univariate_process.jl Outdated
Comment thread src/univariate/hawkes/simulation.jl Outdated
"""
abstract type AbstractMultivariateProcess <: AbstractPointProcess end

Base.ndims(pp::AbstractMultivariateProcess) = length(pp.mark_dist)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes there is a mark_dist field but IndependentMultivariate does not have one?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, IndependentMultivariateProcess is a bit different, since we are not really building a new process, but only gluing a bunch of processes together.
For new multivariate processes, I would expect we use the interface for mark distributions. The idea is that the mark distributions at time t are independent conditional on the history up to t, so we can model them separetely (well, technically not really true, only for processes in which the probability of simultaneous events is 0, which are all interesting processes I am aware of)

Comment thread src/univariate/hawkes/fit.jl Outdated
@@ -0,0 +1,102 @@
"""
StatsAPI.fit(rng, ::Type{HawkesProcess{T}}, h::History; step_tol::Float64 = 1e-6, max_iter::Int = 1000) where {T<:Real}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stale docstring

end

pp_est = fit(PP, h)
pp_est = fit(PP, h)::AbstractPointProcess # JET complains if not specified

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is bein caused by the type stability issue i'm p sure

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the message I get.

┌ MonteCarloTest(S::Type{<:Statistic}, PP::Type{<:AbstractPointProcess}, h::History; kwargs...) @ PointProcesses /home/jkling/projects/PointProcesses.jl/src/HypothesisTests/PPTests/monte_carlo_test.jl:114
│ no matching method found kwcall(::NamedTuple, ::Type{MonteCarloTest}, ::Type{<:Statistic}, ::Rician, ::History) (1/4 union split): Core.kwcall(merge(NamedTuple()::@NamedTuple{}, kwargs::@kwargs{…})::NamedTuple, MonteCarloTest, S::Type{<:Statistic}, pp_est::Union{Rician, HawkesProcess, IndependentMultivariateProcess, PoissonProcess}, h::History)
└────────────────────

Apparently, the static analysis thinks that Rician is a possible return type of fit (Rician is a Distributions.Distribution). Not sure how to deal with this if not by type annotating this line.


# Estimate process and calculate statistic from data
pp_est = fit(PP, h; rng=rng)
pp_est = fit(PP, h; rng=rng)::AbstractPointProcess # JET complains if not specified

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as my other comment

Comment thread src/univariate/hawkes/hawkes_process.jl Outdated
@JoseKling
JoseKling force-pushed the MultivariateHawkes branch from c3eec46 to 774c595 Compare July 2, 2026 16:06
Added tests for custom custom processes
@JoseKling
JoseKling force-pushed the MultivariateHawkes branch from 774c595 to 4052588 Compare July 5, 2026 16:00
@JoseKling
JoseKling requested a review from rsenne July 5, 2026 16:08
end

"""
function StatsAPI.fit(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These fit methods e.g., fit(HawkesProcess, h) were delted yet still referenced in docs (see index.md, hawkes.md, and montecarlo.md)

Comment thread src/univariate/hawkes/simulation.jl Outdated
sort!(sim)
h = History(eltype(sim)[], tmin, tmax, eltype(hp.mark_dist)[], Nothing[], 1)
for event in sim
push!(h, event, sample_mark(hp, event, h))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently ignores the RNG

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
push!(h, event, sample_mark(hp, event, h))
push!(h, event, sample_mark(rng, hp, event, h))

Comment thread src/univariate_process.jl
Comment on lines +59 to +60
transformed_times = [zeros(T, nb_events(h, d)) for d in 1:ndims(h)]
for d in 1:ndims(pp)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A 3-dim process with a history whose keyword constructor computed N=length(unique(dims)) = 2, which split_into_chunks would do, would cause a BBounds error. I think changiing transformed tiems to index by 1:ndims(pp) would fix.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but should we allow for time_change to be applied with process and history with different dimensions? I think it is better for split_into_chunks to guarantee all the chunks have the same number of dimensions as the original history.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, that makes sense. In that case we probably want to thread N through:

chunk = History(times, a, b, marks, dims, h.N; check_args=false)

Comment thread src/univariate/hawkes/hawkes_process.jl Outdated
return sum(log.(hp.μ .+ (hp.α .* A))) - # Value of intensity at each event
(hp.μ * duration(h)) - # Integral of base rate
((hp.α / hp.ω) * sum(1 .- exp.(-hp.ω .* (duration(h) .- h.times)))) + # Integral of each kernel
sum(log.([densityof(hp.mark_dist, t, h, m) for (t, m) in zip(h.times, h.marks)])) # Loglikelihood of marks

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sum(log.([densityof(hp.mark_dist, t, h, m) for (t, m) in zip(h.times, h.marks)])) # Loglikelihood of marks
sum(log(densityof(hp.mark_dist, t, h, m)) for (t, m) in zip(h.times, h.marks); init=0.0) # Loglikelihood of marks

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

return PoissonProcess(λ, mark_dist)
end

function StatsAPI.fit(pptype::Type{PoissonProcess{R,D}}, args...; kwargs...) where {R,D}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this deletion means we no longer support weighted fitting. is that intentional?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I was testing to see if this was the origin of the problem with JET and forgot to add back. Anyway, good catch

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though this is not really in tune with the rest of the package anymore. Not sure if we should keep it, or try to implement something similar for the other processes, or get rid of it...

Comment thread src/univariate/hawkes/hawkes_process.jl Outdated
end
return sum(log.(hp.μ .+ (hp.α .* A))) - # Value of intensity at each event
(hp.μ * duration(h)) - # Integral of base rate
((hp.α / hp.ω) * sum(1 .- exp.(-hp.ω .* (duration(h) .- h.times)))) + # Integral of each kernel

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be h.tmax .- h.times ?

Comment on lines +46 to +49
function ground_intensity(hp::HawkesProcess, t, h::History)
activation = sum(exp.(hp.ω .* (@view h.times[1:(searchsortedfirst(h.times, t) - 1)])))
return hp.μ + (hp.α * activation / exp(hp.ω * t))
end

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need the shift invariant form

Comment thread src/history.jl Outdated
dims = fill(nothing, length(times))
N = 1
else
N = length(unique(dims))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This counts distinct dim values present, not the actual dimension count

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. In the case we are passing vectors, instead of one vector per dimension, this is the only way to deduce the number of dimensions in the history.
I agree this has some weird edge cases. We should probably think in a more structured way about the interface for the constructor. Perhaps this is trying to be too general.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some ideas:

  • Deduce with N = maximum(dims) instead of length(unique(dims))
  • Additionally accept an explicit N/ndims keyword so callers like split_into_chunks can preserve the parent's dimension count

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the split_into_chunks I planned to do exactly this.
About deducing the dimensions with max(dims), this also has edge cases in which there are no events in the dimensions with higher values. Perhaps for multivariate histories the dimension must be passed, if the number of dimensions is nos passed we assume it is univariate?

Comment thread src/univariate/hawkes/hawkes_process.jl Outdated
any((μ, α, ω) .< 0) &&
throw(DomainError((μ, α, ω), "All parameters must be non-negative."))
(α > 0 && α >= ω) &&
throw(DomainError((α, ω), "Parameter ω must be strictly smaller than α"))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error message inverted?

@rsenne

rsenne commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

found a few more bugs. i'll see if i can figure out the JET thing

- Fixed constructor for empty history
- History does not deduce number of dimensions
- Small fixes for Hawkes processes
@JoseKling
JoseKling force-pushed the MultivariateHawkes branch from 1328b5e to 5d08bc9 Compare July 18, 2026 10:33
@JoseKling
JoseKling requested a review from rsenne July 18, 2026 10:38
@JoseKling

Copy link
Copy Markdown
Owner Author

I think I got everything, except for the JET warning.
I changed History slightly. Instead of guessing the number of dimensions, if a Vector is passed to the constructor, we assume it is a univariate history. The inner constructor does not have any default parameters, so everything must be passed explicitly.
Also, I changed the constructor for the empty history to expect a type for the marks and the number of dimensions (not necessary for univariate history)

@rsenne

rsenne commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

i'll try to look at this soon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants