Skip to content

Interface for Multivariate processes#92

Open
JoseKling wants to merge 6 commits into
mainfrom
MultivariateHawkes
Open

Interface for Multivariate processes#92
JoseKling wants to merge 6 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 99.40476% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/bounded_point_process.jl 80.00% 1 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
end
return l
end
DensityInterface.logdensityof

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.

?

Comment thread src/univariate/hawkes/hawkes_process.jl Outdated
HawkesProcess(μ, α, ω) = HawkesProcess(μ, α, ω, NoMarks())

∑_{i=1:n} pᵢᵢ = ∑_{i=1:n} (1 - ∑_{j < i} Dᵢⱼ) = N - D.
function ground_intensity(hp::HawkesProcess, h::History, t)

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 will throw on a Univariate hawkes no? as in univariate_process.jl the order is t, 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.

if yes we should also add a test for a univariate hawkes intensity

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.

The test is already there, but I switched the order in the tests too : P

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 on lines +30 to +33
function time_change(h::History, pp::AbstractUnivariateProcess)
Λ(t) = integrated_ground_intensity(pp, h, min_time(h), t)
return time_change(h, Λ)
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.

redundant with abstract_point_process.jl:126?

Comment thread src/univariate/hawkes/simulation.jl Outdated
Comment on lines +6 to +8
h_temp = History(sim, tmin, tmax)
marks = [sample_mark(hp.mark_dist, t, h_temp) for t in event_times(h_temp)]
return History(; times=sim, tmin=tmin, tmax=tmax, marks=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.

why build history twice?

"""
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)

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
```
The default method uses a loop over events combined with `integrated_ground_intensity`, but it should be reimplemented for specific processes if faster computation is possible.
"""
function DensityInterface.logdensityof(pp::AbstractUnivariateProcess, h::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.

With the new type hierarchy, BoundedPointProcess now has no logdensityofmethod i.e., this would fail:

using PointProcesses

h = History([1.0, 2.0, 3.0], 0.0, 10.0)
logdensityof(BoundedPointProcess(PoissonProcess(1.0), 0.0, 10.0), h)

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.

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?

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
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

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

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