Usage Guide
Intervals
Intervals represent bounded temporal domains. The simplest form is a NakedInterval, which wraps a (start, stop) tuple:
int = NakedInterval((0.0, 10.0))
bounds(int) # (0.0, 10.0)
measure(int) # 10.0
midpoint(int) # 5.0You can also construct one from two arguments:
NakedInterval(0.0, 10.0)Marked Intervals
MarkedInterval attaches arbitrary metadata to an interval:
trial = MarkedInterval((0.0, 5.0), "stimulus_A")
get_mark(trial) # "stimulus_A"
bounds(trial) # (0.0, 5.0)
measure(trial) # 5.0Marks can be any type — strings, numbers, symbols, or custom structs.
Relative Intervals
RelativeInterval expresses an interval in coordinates relative to a reference interval. This is useful for defining peri-event windows (e.g., "100 ms before to 300 ms after stimulus onset"):
reference = NakedInterval((5.0, 8.0))
rel = RelativeInterval(reference, true, NakedInterval((-1.0, 3.0)))
bounds(rel) # (4.0, 8.0) — anchored to the left edge of referenceThe anchored_left flag controls whether offsets are relative to the start or end of the reference.
Interval Sets
IntervalSet groups contiguous intervals into a single object. The intervals must touch (no gaps) and be ordered:
iset = IntervalSet(NakedInterval((0.0, 5.0)), MarkedInterval((5.0, 10.0), :stim))
bounds(iset) # (0.0, 10.0)
measure(iset) # 10.0
get_mark(iset) # :stim — returns the mark of the first marked intervalInterval Operations
Complement — find the portions of a domain not covered by an interval or set of intervals:
domain = NakedInterval((0.0, 10.0))
stim = NakedInterval((3.0, 7.0))
baseline = complement(domain, stim)
# [NakedInterval((0.0, 3.0)), NakedInterval((7.0, 10.0))]complement also accepts a vector of intervals:
stims = [NakedInterval((2.0, 4.0)), NakedInterval((6.0, 8.0))]
gaps = complement(domain, stims)
# [NakedInterval((0.0, 2.0)), NakedInterval((4.0, 6.0)), NakedInterval((8.0, 10.0))]Chunking — break an interval into fixed-size pieces:
chunks = chunk(NakedInterval((0.0, 10.0)), 3.0)
# [NakedInterval((0.0, 3.0)), NakedInterval((3.0, 6.0)),
# NakedInterval((6.0, 9.0)), NakedInterval((9.0, 10.0))]Pass exact=true to drop any remainder that is shorter than chunk_len:
chunks = chunk(NakedInterval((0.0, 10.0)), 3.0, true)
# [NakedInterval((0.0, 3.0)), NakedInterval((3.0, 6.0)), NakedInterval((6.0, 9.0))]Chunking also works on a vector of intervals, concatenating the results:
chunk([NakedInterval((0.0, 5.0)), NakedInterval((10.0, 15.0))], 2.5)Shrinking — contract an interval inward from both ends:
shrink(NakedInterval((0.0, 10.0)), 2.0)
# NakedInterval((1.0, 9.0))When applied to a vector, intervals too short to survive the shrink are dropped.
Shifting — translate an interval by an offset:
shift_interval(NakedInterval((0.0, 5.0)), 10.0)
# NakedInterval((10.0, 15.0))shift_interval preserves marks on MarkedInterval and also works as a curried function:
shifter = shift_interval(10.0)
shifter(NakedInterval((0.0, 5.0))) # NakedInterval((10.0, 15.0))Overlap depth — compute how many intervals overlap at each position:
intervals = [NakedInterval((0.0, 5.0)),
NakedInterval((3.0, 8.0)),
NakedInterval((6.0, 10.0))]
levels = interval_levels(intervals)interval_levels returns a vector of MarkedInterval{Float64,Int} where the mark is the overlap count. Assumes input is sorted by start time.
Intersection and subintervals — find overlapping regions:
interval_intersect(NakedInterval((0.0, 5.0)), NakedInterval((3.0, 8.0)))
# NakedInterval((3.0, 5.0))
# Pairwise intersections between two sorted, non-overlapping vectors
interval_intersections([NakedInterval((0.0, 5.0)), NakedInterval((7.0, 10.0))],
[NakedInterval((3.0, 8.0))])
# [NakedInterval((3.0, 5.0)), NakedInterval((7.0, 8.0))]subinterval validates that one interval is contained in another:
subinterval(NakedInterval((0.0, 10.0)), NakedInterval((2.0, 8.0)))
# NakedInterval((2.0, 8.0)) — validated as a subintervalRelative coordinates — express an interval relative to a reference:
relative_interval(NakedInterval((5.0, 8.0)), NakedInterval((4.0, 10.0)))Other utilities:
check_overlap(NakedInterval((0.0, 5.0)), NakedInterval((3.0, 8.0))) # true
is_subinterval(NakedInterval((2.0, 4.0)), NakedInterval((0.0, 10.0))) # true
5.0 in NakedInterval((0.0, 10.0)) # truePoints
NakedPoints
NakedPoints stores a sorted vector of timestamps on a defined interval:
spikes = NakedPoints([0.5, 1.3, 2.1, 4.7, 5.2, 8.0, 9.1], NakedInterval((0.0, 10.0)))The constructor validates that points are sorted and fall within the interval. If not sorted, it will sort them:
# Unsorted input — automatically sorted
spikes = NakedPoints([9.1, 0.5, 4.7, 2.1, 1.3, 8.0, 5.2], NakedInterval((0.0, 10.0)))You can also construct from a tuple or two numbers for the interval bounds:
NakedPoints([1.0, 2.0, 3.0], (0.0, 5.0))
NakedPoints([1.0, 2.0, 3.0], 0.0, 5.0)Or let the interval be inferred from the data:
NakedPoints([1.0, 2.0, 3.0]) # interval = (1.0, 3.0)Basic queries:
count(spikes) # 7
duration(spikes) # 10.0
rate(spikes) # 0.7 (count / duration)
bounds(spikes) # (0.0, 10.0)
interval(spikes) # NakedInterval((0.0, 10.0))
# Count and rate within a sub-range
count(spikes, 2.0, 6.0) # 3
rate(spikes, 2.0, 6.0) # 0.75Extracting values:
point_values(spikes) # the underlying vector
point_values(spikes, 2.0, 6.0) # view of values in [2.0, 6.0]
nakedvalues(spikes) # same as point_values for NakedPointsTranslation — shift all points and their interval by a constant offset:
shifted = translate(spikes, 100.0)
bounds(shifted) # (100.0, 110.0)VariablePoints
VariablePoints pairs each timestamp with a mark (metadata value). Construct from a NakedPoints and a marks vector:
np = NakedPoints([1.0, 2.0, 3.0], NakedInterval((0.0, 5.0)))
vp = VariablePoints(np, [:a, :b, :c])Individual elements are MarkedPoint objects:
vp[1] # MarkedPoint(1.0, :a)Extracting values and marks:
point_values(vp) # (times_vector, marks_vector)
point_values(vp, 1.0, 2.5) # (view of times, view of marks) in range
get_mark(vp) # the full marks vectorAll Points operations (count, rate, duration, bounds, translate) work on VariablePoints.
SubPoints
SubPoints is a lazy, zero-copy view of a Points collection restricted to a sub-interval:
spikes = NakedPoints([0.5, 1.3, 2.1, 4.7, 5.2, 8.0, 9.1], NakedInterval((0.0, 10.0)))
trial = SubPoints(spikes, NakedInterval((2.0, 6.0)))
count(trial) # 3 — only points in [2.0, 6.0]
rate(trial) # 0.75
duration(trial) # 4.0The constructor validates that the sub-interval is contained within the parent:
SubPoints(spikes, NakedInterval((2.0, 6.0))) # OK
SubPoints(spikes, (2.0, 6.0)) # tuple form also works
SubPoints(spikes, 2.0, 6.0) # two-argument formNesting SubPoints flattens automatically — the inner view references the original data, not the intermediate view.
maybe_subpoints returns a SubPoints if the interval intersects, or nothing if it doesn't:
maybe_subpoints(spikes, NakedInterval((2.0, 6.0))) # SubPoints(...)
maybe_subpoints(spikes, NakedInterval((20.0, 30.0))) # nothingMark Operations
Marks can be stacked and unstacked using tuple nesting:
p = NakedPoint(1.0)
mp = push_mark(p, :neuron_A) # MarkedPoint(1.0, :neuron_A)
mp2 = push_mark(mp, 42) # MarkedPoint(1.0, (42, :neuron_A))
inner, outer_mark = pop_mark(mp2) # (MarkedPoint(1.0, (:neuron_A,)), 42)pop_marks strips the outermost mark from all points in a VariablePoints.
Joining Points
join_points merges multiple point collections. The result's interval is the union of the inputs:
a = NakedPoints([1.0, 3.0], NakedInterval((0.0, 5.0)))
b = NakedPoints([2.0, 7.0], NakedInterval((0.0, 10.0)))
joined = join_points(a, b)
# 4 points on interval (0.0, 10.0), sortedWorks for both naked and marked points, and accepts any number of arguments:
join_points(a, b, c) # merges three collectionsIntersecting Point Collections
points_intersects finds the intersection of two vectors of Points — restricting each collection to only the time ranges where both have coverage:
pts1 = [NakedPoints([1.0, 2.0], NakedInterval((0.0, 5.0)))]
pts2 = [NakedPoints([3.0, 6.0], NakedInterval((2.0, 8.0)))]
sub1, sub2 = points_intersects(pts1, pts2)interval_intersections_subpoints is the lower-level function that intersects a vector of Points with a vector of Interval objects, returning SubPoints views.
Downsampling
pp_downsamp merges points that are closer together than a given resolution, useful for reducing dense event data for visualization:
spikes = NakedPoints([1.0, 1.1, 1.2, 5.0, 5.05, 9.0], NakedInterval((0.0, 10.0)))
ds = pp_downsamp(spikes, 0.0, 10.0, 0.5)Points within resolution of each other are merged using a merge function. The default pt_merge averages timestamps (and marks, if present). pt_extent_merge preserves the extent (min, max) of merged points as an additional mark.
Each merged point gets a count pushed onto its mark stack, recording how many original points were combined.
Aggregate Rate
rate can also be computed over a vector of Points:
rate([trial_spikes_1, trial_spikes_2, trial_spikes_3])
# total spike count across all trials / total duration