From e9fb84e417c0d8e6725c5842e026d6813fa6ee35 Mon Sep 17 00:00:00 2001
From: Jonas Isensee <jonas.isensee@ds.mpg.de>
Date: Tue, 11 Jul 2023 11:56:37 +0200
Subject: [PATCH 01/23] improved import (speed)

---
 src/export/generated.jl | 51 +++++++++++++++++++++++------------------
 1 file changed, 29 insertions(+), 22 deletions(-)

diff --git a/src/export/generated.jl b/src/export/generated.jl
index ad64069..6462425 100644
--- a/src/export/generated.jl
+++ b/src/export/generated.jl
@@ -262,7 +262,8 @@ module ExportUtils
 
     # defaults for centerpos: transform  into 2D array for export
     exportcollect(::Type{<:AbstractParticle}, ::FieldExport{:centerpos}) = :(InPartS.collectvectors(vector))
-    importcollect(::Type{<:AbstractParticle}, ::FieldExport{:centerpos}) = :([InPartS.SVector{size(blob)[2], Float64}(blob[i,:]) for i ∈ 1:size(blob)[1]]) #TODO: faster‽
+    importcollect(::Type{<:AbstractParticle{2}}, ::FieldExport{:centerpos}) = :(@views InPartS.SVector{2, Float64}.(blob[:, 1], blob[:, 2]))
+    importcollect(::Type{<:AbstractParticle{3}}, ::FieldExport{:centerpos}) = :(@views InPartS.SVector{3, Float64}.(blob[:, 1], blob[:, 2], blob[:, 3]))
 
     # default names (trivial for FieldExport, irrelevant for AdditionalExport)
     exportname(::Type, ::FieldExport{S}) where S = string(S)
@@ -790,7 +791,6 @@ the export system, consult the InPartS manual.
 _deserialize_function(T::Type) = quote
     function ExportUtils.deserialize(group, type::Type{<:$T}; sim::Union{Simulation, Nothing} = nothing)
         $(_deserialize_block(T))
-        return items
     end
 end
 
@@ -822,41 +822,48 @@ function _deserialize_block(T::Type, ::ScalarExport)
 
     end
     constructor = Expr(:call, :(ExportUtils.importconstruct), datakwargs, :type)
-    return :(items = $constructor)
+    return :($constructor)
 end
 
 function _deserialize_block(T::Type, ::VectorExport)
-    datakwargs = Expr(:parameters)
     inames = Expr(:parameters)
-
+    inamelist = Symbol[]
+    assignments = []
     for e ∈ allexports(T)
         iname = ExportUtils.importname(T, e)
         source = :(group[$(ExportUtils.exportname(T, e))])
         collect = ExportUtils.importcollect(T, e)
         transform = ExportUtils.importtransform(T, e)
 
-        push!(datakwargs.args, Expr(
-            :kw,
-            iname,
-            quote
-                blob = $source
-                datavec = $collect
-                values = map(data->$transform, datavec)
-            end
-        ))
-
-        push!(inames.args, Expr(:kw, iname, :(data.$iname[i])))
+        push!(inames.args, Expr(:kw, iname, :($iname[i])))
+        push!(inamelist, iname)
+        push!(assignments,
+            Expr(Symbol("="), iname,
+            if transform == :data
+                quote
+                    blob = $source
+                    $collect
+                end
+            else
+                quote
+                    blob = $source
+                    datavec = $collect
+                    map(data->$transform, datavec)
+                end
+            end))
     end
 
-    datatuple = Expr(:tuple, datakwargs)
     return quote
         # all the importing happens here!
-        data = $datatuple
-        nitems = length(data[1])
-        items = Vector{type}(undef, nitems)
-        for i ∈ 1:nitems
-            items[i] = ExportUtils.importconstruct($inames, type)
+        $(assignments...)
+        items = Vector{type}(undef, length($(inamelist[1])))
+        lambda = function(items, $(inamelist...))
+            @inbounds for i ∈ eachindex(items)
+                items[i] = ExportUtils.importconstruct($inames, eltype(items))
+            end
         end
+        lambda(items, $(inamelist...))
+        return items
     end
 end
 
-- 
GitLab


From 87e848c7c32c7f42608a423e5ff585084b8a42d6 Mon Sep 17 00:00:00 2001
From: Jonas Isensee <jonas.isensee@ds.mpg.de>
Date: Tue, 11 Jul 2023 11:56:55 +0200
Subject: [PATCH 02/23] empty internalforces for force extraction

---
 src/particles.jl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/particles.jl b/src/particles.jl
index 1de15cb..e1bc496 100644
--- a/src/particles.jl
+++ b/src/particles.jl
@@ -68,7 +68,7 @@ Can be used to compute forces that do not depend on the state of other particles
     Forces should be stored within the particle struct. Any values returned by this
     function are discarded.
 """
-function internalforces!(p::AbstractParticle, s::Simulation) end
+@forcedef function internalforces!(p::AbstractParticle, s::Simulation) end
 
 
 
-- 
GitLab


From 69905778a4226206ddeb7741513c8fa7f36d950b Mon Sep 17 00:00:00 2001
From: Jonas Isensee <jonas.isensee@ds.mpg.de>
Date: Mon, 24 Jul 2023 10:02:04 +0000
Subject: [PATCH 03/23] Fix ParamType(SpheroidDiskCell)

---
 src/InPartS.jl | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/InPartS.jl b/src/InPartS.jl
index 908dc4a..0518a43 100644
--- a/src/InPartS.jl
+++ b/src/InPartS.jl
@@ -105,9 +105,12 @@ Base.ndims(::AbstractParticleContainer{N}) where {N} = N
 Returns the parameter type for particles of type `PT`.
 """
 function ParamType(T::Type{<:AbstractParticle})
+    T isa UnionAll && return ParamType(T.body)
     i = findfirst(==(:params), fieldnames(T))
-    isnothing(i) && throw(TypeError("Type $T has no field params."))
-    return T.types[i]
+    isnothing(i) && error("Type $T has no field params.")
+    P = T.types[i]
+    P isa DataType || error("Failed to determine parameter type for type $T.")
+    return P
 end
 
 ## Includes
-- 
GitLab


From 2c811cb81e5629592470c2403177a58aedec4d27 Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Mon, 24 Jul 2023 10:16:09 +0000
Subject: [PATCH 04/23] export some accessors

---
 src/particlecontainers/simpleparticlecontainer.jl | 3 +--
 src/simulation.jl                                 | 9 +++++++--
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/particlecontainers/simpleparticlecontainer.jl b/src/particlecontainers/simpleparticlecontainer.jl
index 0d65d00..d582f6e 100644
--- a/src/particlecontainers/simpleparticlecontainer.jl
+++ b/src/particlecontainers/simpleparticlecontainer.jl
@@ -1,5 +1,4 @@
-export SimpleParticleContainer,
-    hasobstacles, particles, obstacles
+export SimpleParticleContainer
 
 hasobstacles(::Type{<:AbstractParticleContainer}) = false
 
diff --git a/src/simulation.jl b/src/simulation.jl
index c7ed78c..1a1000c 100644
--- a/src/simulation.jl
+++ b/src/simulation.jl
@@ -1,4 +1,5 @@
-export Simulation
+export Simulation, current_time, current_step, particles, obstacles, particletype, hasobstacles
+
 
 mutable struct Simulation{
    PTC<:AbstractParticleContainer,
@@ -170,8 +171,12 @@ Returns the particle type of the simulation
 """
 particletype(sim::Simulation) = particletype(sim.particles)
 
-
 """
+    hasobstacles(sim)
+Returns true if the simulation can contain obstacles and [`obstacles`](@sim) can be safely called
+"""
+hasobstacles(sim) = hasobstacles(sim.particles)
+
    reset_clock!(sim::Simulation)
 Set simulation time to zero
 """
-- 
GitLab


From 79371516a4c851f1df05b9b2a9060f408b3b152e Mon Sep 17 00:00:00 2001
From: Jonas Isensee <jonas.isensee@ds.mpg.de>
Date: Tue, 25 Jul 2023 14:58:24 +0000
Subject: [PATCH 05/23] Tovectors

---
 src/export/generated.jl | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/src/export/generated.jl b/src/export/generated.jl
index 6462425..5f80652 100644
--- a/src/export/generated.jl
+++ b/src/export/generated.jl
@@ -262,6 +262,7 @@ module ExportUtils
 
     # defaults for centerpos: transform  into 2D array for export
     exportcollect(::Type{<:AbstractParticle}, ::FieldExport{:centerpos}) = :(InPartS.collectvectors(vector))
+    importcollect(::Type{<:AbstractParticle}, ::FieldExport{:centerpos}) = :(InPartS.tosvectors(blob))
     importcollect(::Type{<:AbstractParticle{2}}, ::FieldExport{:centerpos}) = :(@views InPartS.SVector{2, Float64}.(blob[:, 1], blob[:, 2]))
     importcollect(::Type{<:AbstractParticle{3}}, ::FieldExport{:centerpos}) = :(@views InPartS.SVector{3, Float64}.(blob[:, 1], blob[:, 2], blob[:, 3]))
 
@@ -925,3 +926,14 @@ function collectvectors(vs::AbstractVector{SVector{D, T}}) where {D, T}
 
     return out
 end
+
+function tosvectors(blob)
+    N = size(blob, 2)
+    if N == 2
+        return @views SVector{2, Float64}.(blob[:, 1], blob[:, 2])
+    elseif N == 3
+        return @views SVector{3, Float64}.(blob[:, 1], blob[:, 2], blob[:, 3])
+    else
+        return @views SVector{N,Float64}.((blob[:,i] for i=1:N)...)
+    end
+end
-- 
GitLab


From 6ed87f0cad42cb262327ac6334e321edfaa0723f Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Wed, 26 Jul 2023 10:51:32 +0000
Subject: [PATCH 06/23] whoops

---
 src/simulation.jl | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/simulation.jl b/src/simulation.jl
index 1a1000c..f7f1e87 100644
--- a/src/simulation.jl
+++ b/src/simulation.jl
@@ -177,6 +177,7 @@ Returns true if the simulation can contain obstacles and [`obstacles`](@sim) can
 """
 hasobstacles(sim) = hasobstacles(sim.particles)
 
+"""
    reset_clock!(sim::Simulation)
 Set simulation time to zero
 """
-- 
GitLab


From ffafd58034c655ad2fbb299b4eb66195a92485c5 Mon Sep 17 00:00:00 2001
From: Jonas Isensee <jonas.isensee@ds.mpg.de>
Date: Wed, 26 Jul 2023 11:14:22 +0000
Subject: [PATCH 07/23] add a new forcecontribution function

---
 src/deprecations.jl           | 30 ++++++++++++++++++++++++++++++
 src/extras/forceextraction.jl | 25 +++++++++++--------------
 2 files changed, 41 insertions(+), 14 deletions(-)

diff --git a/src/deprecations.jl b/src/deprecations.jl
index cc5b800..64a077b 100644
--- a/src/deprecations.jl
+++ b/src/deprecations.jl
@@ -5,3 +5,33 @@
 # there is a kwarg-only deprecation in the SimpleAdaptiveStepper constructor that cannot be moved here
 
 @deprecate latest_full_snap(f) lastfullsnap(f)
+
+
+# HACK: this is not nice
+"""
+    forcetuple(r::SVector{2, T}, f::SVector{2, T}, δ::SVector{2, T}) where {T}
+    forcetuple(r::SVector{3, T}, f::SVector{3, T}, δ::SVector{3, T}) where {T}
+Returns a named tuple containing the individual components of `r`, `f` and `δ`.
+
+In the 2D case, the names of the tuple entries are `rx`, `ry`, `fx`, `fy` and `δx`, `δy`;
+in 3D, the additional entries are called `rz`, `fz` and `δz` respectively
+
+DEPRECATED! Use [`forcecontribution`](@ref) instead.
+"""
+function forcetuple(r::SVector{2, T}, f::SVector{2, T}, δ::SVector{2, T}) where {T}
+    Base.depwarn("forcetuple was used for a semantically suboptimal force extraction. Use forcecontribution instead", :forcetuple)
+    return (
+        rx = r[1], ry = r[2],
+        fx = f[1], fy = f[2],
+        δx = δ[1], δy = δ[2]
+    )
+end
+
+function forcetuple(r::SVector{3, T}, f::SVector{3, T}, δ::SVector{3, T}) where {T}
+    Base.depwarn("forcetuple was used for a semantically suboptimal force extraction. Use forcecontribution instead", :forcetuple)
+    return (
+        rx = r[1], ry = r[2], rz = r[3],
+        fx = f[1], fy = f[2], fz = f[3],
+        δx = δ[1], δy = δ[2], δz = δ[3]
+    )
+end
diff --git a/src/extras/forceextraction.jl b/src/extras/forceextraction.jl
index 48c3e54..9c7a223 100644
--- a/src/extras/forceextraction.jl
+++ b/src/extras/forceextraction.jl
@@ -1,6 +1,6 @@
 
 using MacroTools
-export @force, @forcedef, @forcefun
+export @force, @forcedef, @forcefun, forcecontribution
 
 
 """
@@ -101,30 +101,27 @@ macro forcedef(ex)
     end
 end
 
-
-
-# HACK: this is not nice
 """
-    forcetuple(r::SVector{2, T}, f::SVector{2, T}, δ::SVector{2, T}) where {T}
-    forcetuple(r::SVector{3, T}, f::SVector{3, T}, δ::SVector{3, T}) where {T}
-Returns a named tuple containing the individual components of `r`, `f` and `δ`.
+    forcecontribution(c::SVector{2, T}, f::SVector{2, T}, δ::SVector{2, T}) where {T}
+    forcecontribution(c::SVector{3, T}, f::SVector{3, T}, δ::SVector{3, T}) where {T}
+Returns a named tuple containing the individual components of `c`, `f` and `δ`.
 
-In the 2D case, the names of the tuple entries are `rx`, `ry`, `fx`, `fy` and `δx`, `δy`;
-in 3D, the additional entries are called `rz`, `fz` and `δz` respectively
+In the 2D case, the names of the tuple entries are `cx`, `cy`, `fx`, `fy` and `δx`, `δy`;
+in 3D, the additional entries are called `cz`, `fz` and `δz` respectively
 
-Useful for dipole accumulation using [`@forcedef`](@ref).
+Useful for force accumulation using [`@forcedef`](@ref).
 """
-function forcetuple(r::SVector{2, T}, f::SVector{2, T}, δ::SVector{2, T}) where {T}
+function forcecontribution(c::SVector{2, T}, f::SVector{2, T}, δ::SVector{2, T}) where {T}
     return (
-        rx = r[1], ry = r[2],
+        cx = c[1], cy = c[2],
         fx = f[1], fy = f[2],
         δx = δ[1], δy = δ[2]
     )
 end
 
-function forcetuple(r::SVector{3, T}, f::SVector{3, T}, δ::SVector{3, T}) where {T}
+function forcecontribution(c::SVector{3, T}, f::SVector{3, T}, δ::SVector{3, T}) where {T}
     return (
-        rx = r[1], ry = r[2], rz = r[3],
+        cx = c[1], cy = c[2], cz = c[3],
         fx = f[1], fy = f[2], fz = f[3],
         δx = δ[1], δy = δ[2], δz = δ[3]
     )
-- 
GitLab


From a83e3537ef2e1fe3825a3dc87abdb2eac2b9c72b Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Thu, 27 Jul 2023 13:33:45 +0200
Subject: [PATCH 08/23] kill pirates

---
 src/export/generic.jl     |  4 +--
 src/export/jld2wrapper.jl | 61 +++++++++++++++++++++++++++++----------
 2 files changed, 48 insertions(+), 17 deletions(-)

diff --git a/src/export/generic.jl b/src/export/generic.jl
index b7db702..dbad0df 100644
--- a/src/export/generic.jl
+++ b/src/export/generic.jl
@@ -15,7 +15,7 @@ function backend(filename::String)
         error("File appears to be a $modulename file but library is not loaded.")
     end
     if modulename == :JLD2
-        return JLD2.JLDFile
+        return JLD2Wrapper
     elseif modulename == :HDF5
         return H5Wrapper
     else
@@ -927,7 +927,7 @@ function write_sim(filename, sim::Simulation; dumprng = true)
         Base.require(Main, modulename)
         return Base.invokelatest(write_sim, filename, sim; dumprng)
     elseif modulename == :JLD2
-        FT = JLD2.JLDFile
+        FT = JLD2Wrapper
     elseif modulename == :HDF5
         FT = H5Wrapper
     else
diff --git a/src/export/jld2wrapper.jl b/src/export/jld2wrapper.jl
index 3d640d9..a92f1a3 100644
--- a/src/export/jld2wrapper.jl
+++ b/src/export/jld2wrapper.jl
@@ -1,24 +1,41 @@
+export JLD2Wrapper
 using .JLD2
 
+
+const JLD2Obj = Union{JLD2.JLDFile, JLD2.Group}
+
+struct JLD2Wrapper{T<:JLD2Obj}
+    _handle::T
+end
+
+function Base.getindex(j2w::JLD2Wrapper, key)
+    item = j2w._handle[key]
+    return item isa JLD2Obj ? JLD2Wrapper(item) : item
+end
+
+Base.get(j2w::JLD2Wrapper, key, default) = get(j2w._handle, key, default)
+
+Base.setindex!(j2w::JLD2Wrapper, val, key) = setindex!(j2w._handle, val, key)
+# For backwards compatibility with HDF5 write all SArrays as Arrays
+Base.setindex!(j2w::JLD2Wrapper, value::SArray, key::String) = setindex!(j2w, collect(value), key)
+# use writedict for recursive dicts
+Base.setindex!(j2w::JLD2Wrapper, value::Dict{String, Any}, key::String) = InPartS.writedict(j2w, value; name = key)
+
+
 # InPartS IO Backend API
-dfopen(type::Type{JLD2.JLDFile}, filename::String, mode::String = "r") = JLD2.jldopen(filename, mode)
+InPartS.dfopen(::Type{<:JLD2Wrapper}, filename::String, mode::String = "r") = JLD2Wrapper(JLD2.jldopen(filename, mode))
 
-dfclose(f::JLD2.JLDFile) = close(f)
-function dfrename(::Type{JLD2.JLDFile}, oldname, newname; force = false)
+InPartS.dfclose(j2w::JLD2Wrapper) = close(j2w._handle)
+
+function InPartS.dfrename(::Type{JLD2Wrapper}, oldname, newname; force = false)
     cp(oldname, newname; force = force)
     rm(oldname)
 end
 
-gcreate(g::Union{JLD2.JLDFile, JLD2.Group}, name::String; kwargs...) = JLD2.Group(g, name; kwargs...)
-
-isgroup(g::Union{JLD2.JLDFile, JLD2.Group}, key) = (g[key] isa JLD2.Group)
-Base.length(g::Union{JLD2.JLDFile, JLD2.Group}) = length(keys(g))
+InPartS.gcreate(g::JLD2Wrapper, name::String; kwargs...) = JLD2.Group(g._handle, name; kwargs...)
 
-# For backwards compatibility with HDF5 write all SArrays as Arrays
-Base.setindex!(g::JLD2.Group, value::SArray, key::String) = setindex!(g, collect(value), key)
-Base.setindex!(g::JLD2.JLDFile, value::SArray, key::String) = setindex!(g, collect(value), key)
-Base.setindex!(g::JLD2.Group, value::Dict{String, Any}, key::String) = InPartS.writedict(g, value; name = key)
-Base.setindex!(g::JLD2.JLDFile, value::Dict{String, Any}, key::String) = InPartS.writedict(g, value; name = key)
+InPartS.isgroup(g::JLD2Wrapper, key) = (g[key] isa JLD2Wrapper{<:JLD2Group})
+Base.length(g::JLD2Wrapper) = length(keys(g._handle))
 
 
 
@@ -27,7 +44,8 @@ Base.setindex!(g::JLD2.JLDFile, value::Dict{String, Any}, key::String) = InPartS
 # to preserve nestedness
 # is faster than generic version in generic.jl because reading via `g[k]`
 # only happens once
-function readdict(g::Union{JLD2.JLDFile, JLD2.Group}; kwargs...)
+function InPartS.readdict(j2w::JLD2Wrapper; kwargs...)
+    g = j2w._handle
     d = Dict{String, Any}()
     for k in keys(g)
         v = g[k]
@@ -39,5 +57,18 @@ function readdict(g::Union{JLD2.JLDFile, JLD2.Group}; kwargs...)
     end
     return d
 end
-    
-readdict(d::Dict) = d
+
+InPartS.readdict(d::Dict) = d
+
+
+
+# legacy stuff
+InPartS.dfopen(::Type{<:JLD2.JLDFile}, args...; kwargs...) = dfopen(JLD2Wrapper, args...; kwargs...)
+InPartS.dfclose(df::JLD2.JLDFile) = dfclose(JLD2Wrapper(df))
+InPartS.dfrename(df::Type{<:JLD2.JLDFile}, args...; kwargs...) = InPartS.dfrename(JLD2Wrapper, args...; kwargs...)
+ # fully automatic wrapping
+for fun ∈ [:readdict, :readdomain, :readobstacles, :readparams, :readrng, :readsim, :readsnap, :readstatic, :readtype, :numsnaps, :lastfullsnap]
+    @eval InPartS.$(fun)(d::JLD2Obj, args...; kwargs...) = InPartS.$(fun)(JLD2Wrapper(d), args...; kwargs...)
+end
+
+InPartS.readsnap!(sim::Simulation, df::JLD2.JLDFile, args...; kwargs...) = InPartS.readsnap!(sim::Simulation, JLD2Wrapper(df), args...; kwargs...)
-- 
GitLab


From fcbfd204e702bb7293f6c0f11777968153f2b7d6 Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Thu, 27 Jul 2023 13:46:28 +0200
Subject: [PATCH 09/23] maybe resolve ambiguity

---
 src/export/generic.jl | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/export/generic.jl b/src/export/generic.jl
index dbad0df..4a3cded 100644
--- a/src/export/generic.jl
+++ b/src/export/generic.jl
@@ -30,9 +30,9 @@ Open the data file at `filename`.
 
 Mode specifications follow [`Base.open`](@ref).
 """
-dfopen(type::Type, filename::String, mode::String) = dfopen(type, filename; modetoflags(mode)...)
-dfopen(@nospecialize(type::Type), filename::String; kwargs...) = error("Unknown export backend: $type")
-dfopen(filename::String, mode::String="r") = dfopen(backend(filename), filename, mode)
+dfopen(type::Type, filename, mode) = dfopen(type, filename; modetoflags(mode)...)
+dfopen(@nospecialize(type::Type), filename; kwargs...) = error("Unknown export backend: $type")
+dfopen(filename::String, mode="r") = dfopen(backend(filename), filename, mode)
 
 function dfopen(f::Function, args...; kwargs...)
     file = dfopen(args...; kwargs...)
-- 
GitLab


From 30fb3e0fc1b5c7f6f5509d03abf4f8114a0576d2 Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Thu, 27 Jul 2023 13:50:26 +0200
Subject: [PATCH 10/23] missing functions

---
 src/export/jld2wrapper.jl | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/export/jld2wrapper.jl b/src/export/jld2wrapper.jl
index a92f1a3..aca45e4 100644
--- a/src/export/jld2wrapper.jl
+++ b/src/export/jld2wrapper.jl
@@ -21,6 +21,8 @@ Base.setindex!(j2w::JLD2Wrapper, value::SArray, key::String) = setindex!(j2w, co
 # use writedict for recursive dicts
 Base.setindex!(j2w::JLD2Wrapper, value::Dict{String, Any}, key::String) = InPartS.writedict(j2w, value; name = key)
 
+Base.keys(j2w::JLD2Wrapper) = keys(j2w._handle)
+Base.haskey(j2w::JLD2Wrapper, k) = haskey(j2w._handle, k)
 
 # InPartS IO Backend API
 InPartS.dfopen(::Type{<:JLD2Wrapper}, filename::String, mode::String = "r") = JLD2Wrapper(JLD2.jldopen(filename, mode))
-- 
GitLab


From f4f80e8f82aa7e8f4f06b38476001327a0f5e9e7 Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Thu, 27 Jul 2023 15:26:42 +0200
Subject: [PATCH 11/23] more fixes

---
 src/export/jld2wrapper.jl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/export/jld2wrapper.jl b/src/export/jld2wrapper.jl
index aca45e4..4f3f183 100644
--- a/src/export/jld2wrapper.jl
+++ b/src/export/jld2wrapper.jl
@@ -13,7 +13,7 @@ function Base.getindex(j2w::JLD2Wrapper, key)
     return item isa JLD2Obj ? JLD2Wrapper(item) : item
 end
 
-Base.get(j2w::JLD2Wrapper, key, default) = get(j2w._handle, key, default)
+Base.get(j2w::JLD2Wrapper, key, default) = haskey(j2w, key) ? j2w[key] : default
 
 Base.setindex!(j2w::JLD2Wrapper, val, key) = setindex!(j2w._handle, val, key)
 # For backwards compatibility with HDF5 write all SArrays as Arrays
-- 
GitLab


From 08a428f308f61144087867365d4437f49426c54e Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Thu, 27 Jul 2023 16:08:04 +0200
Subject: [PATCH 12/23] more fixes

---
 src/export/jld2wrapper.jl | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/export/jld2wrapper.jl b/src/export/jld2wrapper.jl
index 4f3f183..00ec0ae 100644
--- a/src/export/jld2wrapper.jl
+++ b/src/export/jld2wrapper.jl
@@ -26,15 +26,15 @@ Base.haskey(j2w::JLD2Wrapper, k) = haskey(j2w._handle, k)
 
 # InPartS IO Backend API
 InPartS.dfopen(::Type{<:JLD2Wrapper}, filename::String, mode::String = "r") = JLD2Wrapper(JLD2.jldopen(filename, mode))
-
 InPartS.dfclose(j2w::JLD2Wrapper) = close(j2w._handle)
+Base.close(j2w::JLD2Wrapper{<:JLD2.JLDFile}) = close(j2w._handle) # for lazy people
 
 function InPartS.dfrename(::Type{JLD2Wrapper}, oldname, newname; force = false)
     cp(oldname, newname; force = force)
     rm(oldname)
 end
 
-InPartS.gcreate(g::JLD2Wrapper, name::String; kwargs...) = JLD2.Group(g._handle, name; kwargs...)
+InPartS.gcreate(g::JLD2Wrapper, name::String; kwargs...) = JLD2Wrapper(JLD2.Group(g._handle, name; kwargs...))
 
 InPartS.isgroup(g::JLD2Wrapper, key) = (g[key] isa JLD2Wrapper{<:JLD2Group})
 Base.length(g::JLD2Wrapper) = length(keys(g._handle))
-- 
GitLab


From d7686640344b25c0ea62dd256ed9604be1594858 Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Thu, 27 Jul 2023 16:51:53 +0200
Subject: [PATCH 13/23] default snap for readsim

---
 src/export/generic.jl | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/export/generic.jl b/src/export/generic.jl
index 4a3cded..1ed2337 100644
--- a/src/export/generic.jl
+++ b/src/export/generic.jl
@@ -496,13 +496,14 @@ end
 ## Convenience function for reading things
 
 """
-    readsim(filename; snap, [warn = IOWARN[]], [kwargs...])
-    readsim(f; snap, [warn = IOWARN[]], [kwargs...])
-Read a simulation from file and load the specified snapshot.
+readsim(f; snap = InPartS.lastfullsnap(f), [warn = IOWARN[]], [kwargs...])
+readsim(filename; snap, [warn = IOWARN[]], [kwargs...])
+Read a simulation from file and load the specified snapshot. Defaults to the last full snapshot
+(see [`lastfullsnap`](@ref)).
 
 Additional keyword arguments are passed through to [`readstatic!`](@ref) and [`readsnap`](@ref).
 """
-function readsim(f; snap, warn = IOWARN[], kwargs...)
+function readsim(f; snap = lastfullsnap(f), warn = IOWARN[], kwargs...)
     sim = readstatic(f; warn, kwargs...)
     try
         readsnap!(sim, f, snap; warn, kwargs...)
-- 
GitLab


From d43f303c877a4ba79f1e8211a0ad925978aed84e Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Thu, 27 Jul 2023 16:52:18 +0200
Subject: [PATCH 14/23] =?UTF-8?q?backends=20=E2=86=92=20extensions?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Project.toml                                |  5 +++++
 src/export/h5wrapper.jl => ext/HDF5Ext.jl   | 21 +++++++++++++--------
 src/export/jld2wrapper.jl => ext/JLD2Ext.jl | 13 +++++++++----
 src/InPartS.jl                              | 20 ++++++++++++--------
 src/export/generic.jl                       | 12 +++++-------
 5 files changed, 44 insertions(+), 27 deletions(-)
 rename src/export/h5wrapper.jl => ext/HDF5Ext.jl (78%)
 rename src/export/jld2wrapper.jl => ext/JLD2Ext.jl (90%)

diff --git a/Project.toml b/Project.toml
index 7055263..b101ed1 100644
--- a/Project.toml
+++ b/Project.toml
@@ -16,6 +16,11 @@ Requires = "ae029012-a4dd-5104-9daa-d747884805df"
 StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
 Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
 
+[weakdeps]
+JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
+
+[extensions]
+JLD2Ext = "JLD2"
 
 [compat]
 julia = "1.7"
diff --git a/src/export/h5wrapper.jl b/ext/HDF5Ext.jl
similarity index 78%
rename from src/export/h5wrapper.jl
rename to ext/HDF5Ext.jl
index f5fffd6..716bfcf 100644
--- a/src/export/h5wrapper.jl
+++ b/ext/HDF5Ext.jl
@@ -1,12 +1,15 @@
-export H5Wrapper
+module HDF5Ext
 
-using .HDF5
+using InPartS, HDF5
 
 
 struct H5Wrapper{T<:Union{HDF5.File, HDF5.Group}}
     handle::T
 end
 
+InPartS._backend(::Val{:HDF5}) = H5Wrapper
+
+
 """
     H5Wrapper(filename; [kwargs...]) → H5Wrapper{HDF5.File}
     H5Wrapper(filename, mode::String) → H5Wrapper{HDF5.File}
@@ -48,21 +51,23 @@ Base.setindex!(h5w::H5Wrapper, value::SArray, key::String) = setindex!(h5w.handl
 Base.get(h5w::H5Wrapper, key::String, default) = exists(h5w.handle, key) ? h5w[key] : default
 
 # InPartS IO Backend API
-dfopen(::Type{H5Wrapper}, name::String; flags...) = H5Wrapper(name; flags...)
-dfclose(h5w::H5Wrapper{HDF5.File}) = close(h5w.handle)
-function dfrename(::Type{H5Wrapper}, oldname, newname; force = false)
+InPartS.dfopen(::Type{H5Wrapper}, name::String; flags...) = H5Wrapper(name; flags...)
+InPartS.dfclose(h5w::H5Wrapper{HDF5.File}) = close(h5w.handle)
+function InPartS.dfrename(::Type{H5Wrapper}, oldname, newname; force = false)
     cp(oldname, newname; force = force)
     rm(oldname)
 end
 
-gcreate(h5w::H5Wrapper, name::String; kwargs...) = H5Wrapper(HDF5.create_group(h5w.handle, name))
+InPartS.gcreate(h5w::H5Wrapper, name::String; kwargs...) = H5Wrapper(HDF5.create_group(h5w.handle, name))
 Base.keys(h5w::H5Wrapper) = keys(h5w.handle)
 Base.haskey(h5w::H5Wrapper, k::AbstractString) = haskey(h5w.handle, k)
 
-isgroup(h5w::H5Wrapper, key) = !(h5w.handle[key] isa HDF5.Dataset)
+InPartS.isgroup(h5w::H5Wrapper, key) = !(h5w.handle[key] isa HDF5.Dataset)
 Base.length(h5w::H5Wrapper) = length(h5w.handle)
 
 
 
 # more efficient readdict
-readdict(h5w::H5Wrapper; kwargs...) = read(h5w.handle)
+InPartS.readdict(h5w::H5Wrapper; kwargs...) = read(h5w.handle)
+
+end
\ No newline at end of file
diff --git a/src/export/jld2wrapper.jl b/ext/JLD2Ext.jl
similarity index 90%
rename from src/export/jld2wrapper.jl
rename to ext/JLD2Ext.jl
index 00ec0ae..1e8a21a 100644
--- a/src/export/jld2wrapper.jl
+++ b/ext/JLD2Ext.jl
@@ -1,5 +1,6 @@
-export JLD2Wrapper
-using .JLD2
+module JLD2Ext
+
+using InPartS, JLD2
 
 
 const JLD2Obj = Union{JLD2.JLDFile, JLD2.Group}
@@ -25,6 +26,8 @@ Base.keys(j2w::JLD2Wrapper) = keys(j2w._handle)
 Base.haskey(j2w::JLD2Wrapper, k) = haskey(j2w._handle, k)
 
 # InPartS IO Backend API
+InPartS._backend(::Val{:JLD2}) = JLD2Wrapper
+
 InPartS.dfopen(::Type{<:JLD2Wrapper}, filename::String, mode::String = "r") = JLD2Wrapper(JLD2.jldopen(filename, mode))
 InPartS.dfclose(j2w::JLD2Wrapper) = close(j2w._handle)
 Base.close(j2w::JLD2Wrapper{<:JLD2.JLDFile}) = close(j2w._handle) # for lazy people
@@ -65,8 +68,8 @@ InPartS.readdict(d::Dict) = d
 
 
 # legacy stuff
-InPartS.dfopen(::Type{<:JLD2.JLDFile}, args...; kwargs...) = dfopen(JLD2Wrapper, args...; kwargs...)
-InPartS.dfclose(df::JLD2.JLDFile) = dfclose(JLD2Wrapper(df))
+InPartS.dfopen(::Type{<:JLD2.JLDFile}, args...; kwargs...) = InPartS.dfopen(JLD2Wrapper, args...; kwargs...)
+InPartS.dfclose(df::JLD2.JLDFile) = InPartS.dfclose(JLD2Wrapper(df))
 InPartS.dfrename(df::Type{<:JLD2.JLDFile}, args...; kwargs...) = InPartS.dfrename(JLD2Wrapper, args...; kwargs...)
  # fully automatic wrapping
 for fun ∈ [:readdict, :readdomain, :readobstacles, :readparams, :readrng, :readsim, :readsnap, :readstatic, :readtype, :numsnaps, :lastfullsnap]
@@ -74,3 +77,5 @@ for fun ∈ [:readdict, :readdomain, :readobstacles, :readparams, :readrng, :rea
 end
 
 InPartS.readsnap!(sim::Simulation, df::JLD2.JLDFile, args...; kwargs...) = InPartS.readsnap!(sim::Simulation, JLD2Wrapper(df), args...; kwargs...)
+
+end
\ No newline at end of file
diff --git a/src/InPartS.jl b/src/InPartS.jl
index 0518a43..e874704 100644
--- a/src/InPartS.jl
+++ b/src/InPartS.jl
@@ -189,16 +189,20 @@ function __init__()
 
 
     # EXPORT BACKENDS
-
-    @require HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" begin
-        include("export/h5wrapper.jl")
-        @info "InPartS: HDF5 export enabled"
-    end
-    @require JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" begin
-        include("export/jld2wrapper.jl")
-        @info "InPartS: JLD2 export enabled"
+    # fallback if no extensions available
+    @static if !isdefined(Base, :get_extension)
+        @require HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" begin
+            include("../ext/HDF5Ext.jl")
+            @info "InPartS: HDF5 export enabled"
+        end
+
+        @require JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" begin
+            include("../ext/JLD2Ext.jl")
+            @info "InPartS: JLD2 export enabled"
+        end
     end
 
+
     # Use Requires infrastructure to warn if legacy version is ever loaded simultaneously
     legacyInPartS = Base.PkgId(Base.UUID("385f2a1c-27df-41b8-9918-2c5d735168af"), "InPartS")
     Requires.listenpkg(legacyInPartS) do
diff --git a/src/export/generic.jl b/src/export/generic.jl
index 1ed2337..10e0485 100644
--- a/src/export/generic.jl
+++ b/src/export/generic.jl
@@ -14,15 +14,13 @@ function backend(filename::String)
     if !isdefined(Main, modulename)
         error("File appears to be a $modulename file but library is not loaded.")
     end
-    if modulename == :JLD2
-        return JLD2Wrapper
-    elseif modulename == :HDF5
-        return H5Wrapper
-    else
-        error("This backend is not implemented")
-    end
+    # this is a hack to get around the fact that extensions cannot export copied
+    # so we can't use the JLD2Wrapper/H5Wrapper types from here
+    return _backend(Val(modulename))
 end
 
+_backend(v) = error("The backend $v is not implemented")
+
 """
     dfopen(backend, filename; kwargs...)
     dfopen(backend, filename, mode::String)
-- 
GitLab


From 24ff064acfa1d3c82f8ec0a1040ec64b50fff1e5 Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Thu, 27 Jul 2023 16:56:02 +0200
Subject: [PATCH 15/23] fix

---
 src/export/generic.jl | 13 +------------
 1 file changed, 1 insertion(+), 12 deletions(-)

diff --git a/src/export/generic.jl b/src/export/generic.jl
index 10e0485..a20ff6f 100644
--- a/src/export/generic.jl
+++ b/src/export/generic.jl
@@ -920,18 +920,7 @@ A function for whenever you just need to save the ~~world~~ simulation.
 
 """
 function write_sim(filename, sim::Simulation; dumprng = true)
-    s = FileIO.query(filename; checkfile=true)
-    modulename = typeof(s).parameters[1].parameters[1]
-    if !isdefined(InPartS, modulename)
-        Base.require(Main, modulename)
-        return Base.invokelatest(write_sim, filename, sim; dumprng)
-    elseif modulename == :JLD2
-        FT = JLD2Wrapper
-    elseif modulename == :HDF5
-        FT = H5Wrapper
-    else
-        error("This backend is not implemented")
-    end
+    FT = backend(filename)
     dfopen(FT, filename, "w") do f
         writestatic(f, sim)
         writesnap(f[SNAPGROUPNAME], sim; name = "0", dumprng)
-- 
GitLab


From ef400ca49cd65be41ccfabd60a9bfc900b25bb88 Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Thu, 27 Jul 2023 17:29:42 +0200
Subject: [PATCH 16/23] forgot to register extension

---
 Project.toml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Project.toml b/Project.toml
index b101ed1..1c477a6 100644
--- a/Project.toml
+++ b/Project.toml
@@ -18,9 +18,11 @@ Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
 
 [weakdeps]
 JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
+HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
 
 [extensions]
 JLD2Ext = "JLD2"
+HDF5Ext = "HDF5
 
 [compat]
 julia = "1.7"
-- 
GitLab


From b45d801e75284a50dc4af09ce65d40044bd2acca Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Thu, 27 Jul 2023 17:49:13 +0200
Subject: [PATCH 17/23] typo

---
 Project.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Project.toml b/Project.toml
index 1c477a6..95180cd 100644
--- a/Project.toml
+++ b/Project.toml
@@ -22,7 +22,7 @@ HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
 
 [extensions]
 JLD2Ext = "JLD2"
-HDF5Ext = "HDF5
+HDF5Ext = "HDF5"
 
 [compat]
 julia = "1.7"
-- 
GitLab


From 4d48ab683ed205ab296f74190598e01eb644ed28 Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Thu, 27 Jul 2023 18:01:14 +0200
Subject: [PATCH 18/23] ugb

---
 ext/HDF5Ext.jl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ext/HDF5Ext.jl b/ext/HDF5Ext.jl
index 716bfcf..57524a3 100644
--- a/ext/HDF5Ext.jl
+++ b/ext/HDF5Ext.jl
@@ -41,7 +41,7 @@ end
 # dictionary-like access
 function Base.getindex(h5w::H5Wrapper, key::String)
     item = h5w.handle[key]
-    return isgroup(h5w, key) ? H5Wrapper(item) : read(item)
+    return InPartS.isgroup(h5w, key) ? H5Wrapper(item) : read(item)
 end
 Base.setindex!(h5w::H5Wrapper, value, key::String) = setindex!(h5w.handle, value, key)
 Base.setindex!(h5w::H5Wrapper, value::Dict{String, Any}, key::String) = InPartS.writedict(h5w, value; name = key)
-- 
GitLab


From 9e4120142c5de5555bd1e49535329c476842c89a Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Thu, 27 Jul 2023 18:09:32 +0200
Subject: [PATCH 19/23] aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaA

---
 ext/JLD2Ext.jl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ext/JLD2Ext.jl b/ext/JLD2Ext.jl
index 1e8a21a..da0a3a0 100644
--- a/ext/JLD2Ext.jl
+++ b/ext/JLD2Ext.jl
@@ -55,7 +55,7 @@ function InPartS.readdict(j2w::JLD2Wrapper; kwargs...)
     for k in keys(g)
         v = g[k]
         if v isa JLD2.Group
-            d[k] = readdict(v)
+            d[k] = InPartS.readdict(v)
         else
             d[k] = v
         end
-- 
GitLab


From 8f143477cb1067d9c253612421f15b74d10cf904 Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Fri, 28 Jul 2023 10:11:28 +0000
Subject: [PATCH 20/23] changelog & version

---
 CHANGELOG.md | 10 ++++++++++
 Project.toml |  2 +-
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2cc88c3..6c5f1be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # Changelog
 
+## v0.4.2 
+ - *deprecation*: `forcetuple` is deprecated in favour of the new improved `forcecontribution`
+ - *bugfix*: automatic ParamType now can deal with UnionAll particle types as long as `(::ParticleType).params` is concrete
+ - various IO improvements
+    - HDF5 and JLD2 IO backends have been moved to extensions on Julia versions that support them
+    - improvements to type reconstruction, speeding up simulation loading
+    - `readsim` now loads the last full snapshot when no snapshot is explicitely specified
+ - utility functions `current_time`, `current_step`, `particletype`, `hasobstacles` are now exported
+ 
+
 ## v0.4.1
  - *bugfix*: PyPlot plotting has been repaired
  - *new feature*: new convenience syntax for reading data files with the `Snapshots` accessor
diff --git a/Project.toml b/Project.toml
index 95180cd..e1eae75 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,7 +1,7 @@
 name = "InPartS"
 uuid = "f768f48f-0d8a-415b-80aa-7de5ff9b8474"
 authors = ["Lukas Hupe <lukas.hupe@ds.mpg.de>", "Jonas Isensee <jonas.isensee@ds.mpg.de>", "Philip Bittihn <philip.bittihn@ds.mpg.de>"]
-version = "0.4.1"
+version = "0.4.2"
 
 [deps]
 FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
-- 
GitLab


From 51627056c559a06f19e84a5fbbaad504fd187275 Mon Sep 17 00:00:00 2001
From: Jonas Isensee <jonas.isensee@ds.mpg.de>
Date: Fri, 28 Jul 2023 11:09:34 +0000
Subject: [PATCH 21/23] Noninteractive Termination

---
 src/callbacks.jl | 72 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 72 insertions(+)

diff --git a/src/callbacks.jl b/src/callbacks.jl
index d7f89ab..75b182a 100644
--- a/src/callbacks.jl
+++ b/src/callbacks.jl
@@ -2,6 +2,7 @@ using Printf
 import Base.∘
 
 export EveryStepCallback, TimedCallback, PeriodicCallback, RealTimeCallback, BurstModeCallback, CallbackSet
+export SleeperCallback
 
 
 abstract type AbstractCallback end
@@ -33,6 +34,14 @@ simulation.
 function finalize(::AbstractCallback, sim; kwargs...) end
 
 
+"""
+    defrost!(cb <: AbstractCallback)
+Define for custom callback if it needs modification after a simulation was interrupted.
+The primary intended use case is to update timestamps in e.g. RealTimeCallback.
+"""
+function defrost!(cb::AbstractCallback) end
+
+
 
 """
     NoCallback() <: AbstractCallback
@@ -166,6 +175,7 @@ function (cb::RealTimeCallback)(sim)
 end
 
 finalize(cb::RealTimeCallback, sim; kwargs...) = cb.finalize(sim; kwargs...)
+defrost!(cb::RealTimeCallback) = (cb.offset = time(); return nothing);
 
 
 
@@ -302,3 +312,65 @@ function (cb::BurstModeCallback)(sim)
 end
 
 InPartS.finalize(cb::BurstModeCallback, sim; kwargs...) = cb.finalize(sim; kwargs...)
+
+
+"""
+    SleeperCallback(trigger_file, interval=30.0) <: AbstractCallback
+Returns a callback that checks for the existence of `trigger_file` every `interval` seconds.
+If the file exists, it is read and the following commands are supported:
+- `terminate`: terminates the simulation
+- `simtime=...`: terminates the simulation at the specified simulation time
+- `simtime+...`: terminates the simulation at the next multiple of the specified simulation time
+
+The callback copies the received commands to `trigger_file*"_parsed"` and deletes `trigger_file`.
+"""
+mutable struct SleeperCallback <: AbstractCallback
+    trigger_file::String
+    interval::Float64
+    last_executiontime::Float64
+    terminate_simtime::Float64
+    SleeperCallback(trigger_file::String, interval::Real=30.0) =
+        new(trigger_file, interval, time(), Inf)
+end
+
+function prepropagate(cb::SleeperCallback, sim)
+    triggertime = time()
+
+    if sim.t ≥ cb.terminate_simtime
+        # terminate simulation
+        println("# SleeperCallback sim-time condition was triggered at t=$(sim.t) > $(cb.terminate_simtime). Terminating simulation.")
+        return false
+    end
+    if (triggertime - cb.last_executiontime) < cb.interval
+        return true
+    end
+    cb.last_executiontime = triggertime
+    retcode = true
+    if !isfile(cb.trigger_file)
+        # trigger file doesn't exist. Create it.
+        write(cb.trigger_file, "")
+    else
+        command = readline(cb.trigger_file)
+        if command == "terminate"
+            println("# SleeperCallback: Received terminate command. Terminating simulation.")
+            retcode = false
+        elseif startswith(command, "simtime=")
+            cb.terminate_simtime = parse(Float64, split(command, "=")[2])
+            println("# SleeperCallback: Received terminate command. Terminating simulation at t=$(cb.terminate_simtime).")
+        elseif startswith(command, "simtime+")
+            extrasimtime = parse(Float64, split(command, "+")[2])
+            cb.terminate_simtime = sim.t - sim.t%extrasimtime + extrasimtime
+            println("# SleeperCallback: Received terminate command. Terminating simulation at t=$(cb.terminate_simtime).")
+        elseif !isempty(command)
+            println("# SleeperCallback: Received gibberish: \"$(command)\". Ignoring.")
+        end
+        if !isempty(command)
+            open(cb.trigger_file*"_parsed", "a") do f
+                write(f, command)
+                write(f, "\n")
+            end
+            write(cb.trigger_file, "") # empty the file
+        end
+    end
+    return retcode
+end
-- 
GitLab


From 83ffacb1a251e5622c74d134306610496232ccb5 Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Fri, 28 Jul 2023 11:10:12 +0000
Subject: [PATCH 22/23] Update file CHANGELOG.md

---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c5f1be..aa5eddc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,7 @@
     - improvements to type reconstruction, speeding up simulation loading
     - `readsim` now loads the last full snapshot when no snapshot is explicitely specified
  - utility functions `current_time`, `current_step`, `particletype`, `hasobstacles` are now exported
- 
+ - added SleeperCallback for external simulation control
 
 ## v0.4.1
  - *bugfix*: PyPlot plotting has been repaired
-- 
GitLab


From 3ddee8455fc7d01e09f0a1c24d227c7300dbac16 Mon Sep 17 00:00:00 2001
From: Lukas Hupe <lukas.hupe@ds.mpg.de>
Date: Fri, 28 Jul 2023 16:59:30 +0200
Subject: [PATCH 23/23] con structors

---
 src/export/generic.jl | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/src/export/generic.jl b/src/export/generic.jl
index a20ff6f..a58c3f4 100644
--- a/src/export/generic.jl
+++ b/src/export/generic.jl
@@ -167,6 +167,8 @@ mutable struct SaveCallback{FT, CB<:AbstractCallback, FN<:Function} <: AbstractC
         new{FT, CB, FN}(trigger, filename, 0, false, fn, warn)
 end
 
+SaveCallback(trigger, filename, fn, warn) = SaveCallback{backend(filename)}(trigger, filename, fn, warn)
+
 """
     SaveCallback([backend], filename; [interval = 1.0], [offset = 0.0], [dumprng = (s, sim) -> (s.nextsnap % 10 == 0)], [warn = IOWARN[]])
 Create a [`SaveCallback`](@ref) that periodically saves simulation snapshots to `filename`.
@@ -174,10 +176,11 @@ Create a [`SaveCallback`](@ref) that periodically saves simulation snapshots to
 Uses a [`PeriodicCallback`](@ref) with the given `interval` and `offset`.
 """
 SaveCallback(backend, filename; interval = 1.0, offset = 0.0,  dumprng = (s, sim) -> _defaultdumprng(sim.rng, s), warn = IOWARN[]) =
-    SaveCallback{backend}(PeriodicCallback(sim -> true, interval; offset = offset), filename, dumprng, warn)
+    SaveCallback(PeriodicCallback(sim -> true, interval; offset = offset), filename, dumprng, warn)
 
 SaveCallback(filename::String; kwargs...) = SaveCallback(backend(filename), filename; kwargs...)
 
+
 function (s::SaveCallback)(sim::Simulation)
     if !s.initialized
         _init(s, sim)
@@ -257,6 +260,8 @@ struct BackupCallback{FT, CB} <: AbstractCallback
     BackupCallback{FT}(trigger::CB, filename, warn) where {FT, CB} = new{FT, CB}(trigger, filename, warn)
 end
 
+BackupCallback(trigger, filename, warn) = SaveCallback{backend(filename)}(trigger, filename, warn)
+
 """
    BackupCallback(backend, filename; [interval = 300.0], [offset = 0.0], [warn = IOWARN[]])
 Creates a [`BackupCallback`](@ref) that periodically dumps the entire simulation state to `filename`.
@@ -264,7 +269,7 @@ Creates a [`BackupCallback`](@ref) that periodically dumps the entire simulation
 Uses a [`RealTimeCallback`](@ref) with the given `interval` and `offset`.
 """
 BackupCallback(backend, filename; interval = 300.0, offset = 0.0, warn = IOWARN[]) =
-    BackupCallback{backend}(RealTimeCallback(interval, sim -> true; offset = offset), filename, warn)
+    BackupCallback(RealTimeCallback(interval, sim -> true; offset = offset), filename, warn)
 BackupCallback(filename::String; kwargs...) =
     BackupCallback(backend(filename), filename; kwargs...)
 
-- 
GitLab