Skip to content

Commit 2bc3267

Browse files
kalidkeclaude
andcommitted
Add pattern_ids tracking and update SMLMData compat to 0.6
- uniform2D/uniform3D now return pattern_ids indicating pattern membership - apply_labeling preserves pattern_ids through expansion (with backward-compat overloads) - Emitters now carry pattern id through simulation pipeline - Update SMLMData compat to "0.5, 0.6" - Update documentation for new return signatures Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9ef36b9 commit 2bc3267

File tree

7 files changed

+111
-47
lines changed

7 files changed

+111
-47
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
1414
[compat]
1515
Distributions = "0.25"
1616
MicroscopePSFs = "0.5"
17-
SMLMData = "0.5.1"
17+
SMLMData = "0.5, 0.6"
1818
julia = "1.6"
1919

2020
[extras]

api_overview.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -512,21 +512,21 @@ rotate!(pattern3d, R)
512512
#### apply_labeling
513513

514514
Apply labeling strategy to binding site coordinates, expanding each site to the
515-
appropriate number of fluorophore positions.
515+
appropriate number of fluorophore positions while preserving pattern IDs.
516516

517517
```julia
518-
# Generate binding site coordinates from pattern distribution
519-
x, y = uniform2D(1.0, Nmer2D(), 10.0, 10.0) # ~100 binding sites
518+
# Generate binding site coordinates with pattern IDs from pattern distribution
519+
x, y, pattern_ids = uniform2D(1.0, Nmer2D(), 10.0, 10.0)
520520

521-
# Apply Poisson labeling (average 1.5 fluorophores per site)
522-
x_labeled, y_labeled = apply_labeling((x, y), PoissonLabeling(1.5))
521+
# Apply labeling with pattern ID tracking (returns coords tuple and new IDs)
522+
(x_labeled, y_labeled), new_ids = apply_labeling((x, y), pattern_ids, PoissonLabeling(1.5))
523523

524-
# Apply fixed labeling with efficiency
524+
# Backward-compatible: without pattern IDs (returns only coords)
525525
x_labeled, y_labeled = apply_labeling((x, y), FixedLabeling(1; efficiency=0.8))
526526

527527
# Works with 3D coordinates too
528-
x, y, z = uniform3D(1.0, Nmer3D(), 10.0, 10.0)
529-
x_labeled, y_labeled, z_labeled = apply_labeling((x, y, z), BinomialLabeling(4, 0.8))
528+
x, y, z, pattern_ids = uniform3D(1.0, Nmer3D(), 10.0, 10.0)
529+
(x_labeled, y_labeled, z_labeled), new_ids = apply_labeling((x, y, z), pattern_ids, BinomialLabeling(4, 0.8))
530530
```
531531

532532
#### n_fluorophores
@@ -543,7 +543,8 @@ n = n_fluorophores(labeling) # Returns Int (can be 0)
543543

544544
### Pattern Distribution
545545

546-
Generate random pattern distributions in a field.
546+
Generate random pattern distributions in a field. Each function returns coordinates
547+
plus pattern instance IDs for tracking which emitters belong to the same pattern.
547548

548549
```julia
549550
# Create patterns
@@ -555,11 +556,12 @@ field_x = 10.0 # μm
555556
field_y = 10.0 # μm
556557
density = 1.0 # patterns per μm²
557558

558-
# Get coordinates for 2D distribution
559-
x, y = uniform2D(density, pattern2d, field_x, field_y)
559+
# Get coordinates for 2D distribution (returns pattern IDs)
560+
x, y, pattern_ids = uniform2D(density, pattern2d, field_x, field_y)
561+
# pattern_ids[i] indicates which pattern instance point i belongs to
560562

561-
# Get coordinates for 3D distribution
562-
x, y, z = uniform3D(density, pattern3d, field_x, field_y, zrange=[-2.0, 2.0])
563+
# Get coordinates for 3D distribution (returns pattern IDs)
564+
x, y, z, pattern_ids = uniform3D(density, pattern3d, field_x, field_y, zrange=[-2.0, 2.0])
563565
```
564566

565567
## Common Workflows

docs/src/core/patterns.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,16 @@ field_y = 10.0 # μm
105105
pattern = Nmer2D(n=6, d=0.2)
106106
density = 1.5 # patterns/μm²
107107

108-
x, y = uniform2D(density, pattern, field_x, field_y)
108+
x, y, pattern_ids = uniform2D(density, pattern, field_x, field_y)
109+
# pattern_ids[i] indicates which pattern instance point i belongs to
109110

110111
# Create custom distribution of 3D patterns
111112
pattern3d = Nmer3D(n=6, d=0.2)
112-
x, y, z = uniform3D(density, pattern3d, field_x, field_y, zrange=[-2.0, 2.0])
113+
x, y, z, pattern_ids = uniform3D(density, pattern3d, field_x, field_y, zrange=[-2.0, 2.0])
113114
```
114115

116+
The `pattern_ids` array tracks which pattern instance each point belongs to. This is useful for analysis, visualization (coloring by pattern), or grouping emitters by their originating structure. The `id` field on emitters in the simulation output contains this pattern ID.
117+
115118
## Pattern Manipulation
116119

117120
### Rotation

src/core/labeling.jl

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -173,57 +173,102 @@ Apply Labeling to Coordinates
173173
==========================================================================#
174174

175175
"""
176-
apply_labeling(coords, labeling::AbstractLabeling)
176+
apply_labeling(coords, pattern_ids, labeling::AbstractLabeling)
177177
178178
Apply labeling strategy to binding site coordinates, expanding each site
179-
to the appropriate number of fluorophore positions.
179+
to the appropriate number of fluorophore positions while preserving pattern IDs.
180180
181181
# Arguments
182182
- `coords`: Tuple of coordinate vectors `(x, y)` for 2D or `(x, y, z)` for 3D
183+
- `pattern_ids::Vector{Int}`: Pattern instance ID for each binding site
183184
- `labeling::AbstractLabeling`: Labeling strategy to apply
184185
185186
# Returns
186-
- Tuple of coordinate vectors with fluorophore positions
187+
- `Tuple`: (coords, new_pattern_ids) where coords is a tuple of coordinate vectors
188+
and new_pattern_ids preserves pattern membership for each fluorophore
187189
188190
# Example
189191
```julia
190-
# Original: 100 binding sites
191-
x, y = uniform2D(1.0, Nmer2D(), 10.0, 10.0)
192+
# Original: binding sites with pattern IDs
193+
x, y, pattern_ids = uniform2D(1.0, Nmer2D(), 10.0, 10.0)
192194
193-
# After Poisson labeling: variable number of fluorophores
194-
x_labeled, y_labeled = apply_labeling((x, y), PoissonLabeling(1.5))
195+
# After Poisson labeling: variable number of fluorophores, pattern IDs preserved
196+
(x_labeled, y_labeled), new_ids = apply_labeling((x, y), pattern_ids, PoissonLabeling(1.5))
195197
```
196198
"""
197-
function apply_labeling(coords::Tuple{Vector{T}, Vector{T}}, labeling::AbstractLabeling) where T
199+
function apply_labeling(coords::Tuple{Vector{T}, Vector{T}}, pattern_ids::Vector{Int},
200+
labeling::AbstractLabeling) where T
198201
x, y = coords
199202
new_x = T[]
200203
new_y = T[]
204+
new_ids = Int[]
201205

202206
for i in eachindex(x)
203207
n = n_fluorophores(labeling)
204208
for _ in 1:n
205209
push!(new_x, x[i])
206210
push!(new_y, y[i])
211+
push!(new_ids, pattern_ids[i])
207212
end
208213
end
209214

210-
return (new_x, new_y)
215+
return (new_x, new_y), new_ids
211216
end
212217

213-
function apply_labeling(coords::Tuple{Vector{T}, Vector{T}, Vector{T}}, labeling::AbstractLabeling) where T
218+
function apply_labeling(coords::Tuple{Vector{T}, Vector{T}, Vector{T}}, pattern_ids::Vector{Int},
219+
labeling::AbstractLabeling) where T
214220
x, y, z = coords
215221
new_x = T[]
216222
new_y = T[]
217223
new_z = T[]
224+
new_ids = Int[]
218225

219226
for i in eachindex(x)
220227
n = n_fluorophores(labeling)
221228
for _ in 1:n
222229
push!(new_x, x[i])
223230
push!(new_y, y[i])
224231
push!(new_z, z[i])
232+
push!(new_ids, pattern_ids[i])
225233
end
226234
end
227235

228-
return (new_x, new_y, new_z)
236+
return (new_x, new_y, new_z), new_ids
237+
end
238+
239+
# Backward-compatible overloads without pattern_ids (returns only coords)
240+
"""
241+
apply_labeling(coords, labeling::AbstractLabeling)
242+
243+
Apply labeling strategy to binding site coordinates without pattern tracking.
244+
245+
This is a convenience method that returns only the expanded coordinates
246+
(for backward compatibility).
247+
248+
# Arguments
249+
- `coords`: Tuple of coordinate vectors `(x, y)` for 2D or `(x, y, z)` for 3D
250+
- `labeling::AbstractLabeling`: Labeling strategy to apply
251+
252+
# Returns
253+
- Tuple of coordinate vectors with fluorophore positions
254+
255+
# Example
256+
```julia
257+
# Apply labeling without pattern tracking
258+
x, y = [1.0, 2.0], [3.0, 4.0]
259+
new_x, new_y = apply_labeling((x, y), FixedLabeling(2))
260+
```
261+
"""
262+
function apply_labeling(coords::Tuple{Vector{T}, Vector{T}}, labeling::AbstractLabeling) where T
263+
# Generate sequential IDs for backward compatibility
264+
pattern_ids = collect(1:length(coords[1]))
265+
result, _ = apply_labeling(coords, pattern_ids, labeling)
266+
return result
267+
end
268+
269+
function apply_labeling(coords::Tuple{Vector{T}, Vector{T}, Vector{T}}, labeling::AbstractLabeling) where T
270+
# Generate sequential IDs for backward compatibility
271+
pattern_ids = collect(1:length(coords[1]))
272+
result, _ = apply_labeling(coords, pattern_ids, labeling)
273+
return result
229274
end

src/core/patterns.jl

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -323,13 +323,14 @@ Create coordinate arrays for randomly placed and rotated 2D patterns.
323323
- `field_y::Float64`: Field height in microns
324324
325325
# Returns
326-
- `Tuple{Vector{Float64}, Vector{Float64}}`: (x, y) coordinates in microns
326+
- `Tuple{Vector{Float64}, Vector{Float64}, Vector{Int}}`: (x, y, pattern_ids) where
327+
pattern_ids indicates which pattern instance each point belongs to
327328
328329
# Example
329330
```julia
330331
# Generate coordinates for randomly placed Nmer2D patterns
331332
nmer = Nmer2D(; n=6, d=0.2)
332-
x, y = uniform2D(1.0, nmer, 10.0, 10.0)
333+
x, y, pattern_ids = uniform2D(1.0, nmer, 10.0, 10.0)
333334
```
334335
"""
335336
function uniform2D(ρ::T1, p::Pattern2D, field_x::T2, field_y::T3) where {T1<:AbstractFloat, T2<:AbstractFloat, T3<:AbstractFloat}
@@ -348,7 +349,8 @@ function _uniform2D_impl(ρ::T, p::Pattern2D, field_x::T, field_y::T) where T <:
348349
# Initialize coordinate arrays
349350
x = Vector{T}(undef, ntotal)
350351
y = Vector{T}(undef, ntotal)
351-
352+
pattern_ids = Vector{Int}(undef, ntotal)
353+
352354
idx = 1
353355
for nn = 1:npatterns
354356
θ = 2 * pi * rand()
@@ -359,15 +361,16 @@ function _uniform2D_impl(ρ::T, p::Pattern2D, field_x::T, field_y::T) where T <:
359361
# Rotate and translate pattern points
360362
x[idx] = p.x[mm] * cos(θ) - p.y[mm] * sin(θ) + x0
361363
y[idx] = p.x[mm] * sin(θ) + p.y[mm] * cos(θ) + y0
364+
pattern_ids[idx] = nn
362365
idx += 1
363366
end
364367
end
365368

366-
return x, y
369+
return x, y, pattern_ids
367370
end
368371

369372
"""
370-
uniform3D(ρ::Float64, p::Pattern3D, field_x::Float64, field_y::Float64;
373+
uniform3D(ρ::Float64, p::Pattern3D, field_x::Float64, field_y::Float64;
371374
zrange::Vector{Float64}=[-1.0, 1.0])
372375
373376
Create coordinate arrays for randomly placed and rotated 3D patterns.
@@ -380,13 +383,14 @@ Create coordinate arrays for randomly placed and rotated 3D patterns.
380383
- `zrange::Vector{Float64}=[-1.0, 1.0]`: [min_z, max_z] range in microns
381384
382385
# Returns
383-
- `Tuple{Vector{Float64}, Vector{Float64}, Vector{Float64}}`: (x, y, z) coordinates
386+
- `Tuple{Vector{Float64}, Vector{Float64}, Vector{Float64}, Vector{Int}}`: (x, y, z, pattern_ids) where
387+
pattern_ids indicates which pattern instance each point belongs to
384388
385389
# Example
386390
```julia
387391
# Generate coordinates for randomly placed Nmer3D patterns
388392
nmer = Nmer3D(; n=6, d=0.2)
389-
x, y, z = uniform3D(1.0, nmer, 10.0, 10.0; zrange=[-2.0, 2.0])
393+
x, y, z, pattern_ids = uniform3D(1.0, nmer, 10.0, 10.0; zrange=[-2.0, 2.0])
390394
```
391395
"""
392396
function uniform3D(ρ::T1, p::Pattern3D, field_x::T2, field_y::T3;
@@ -405,7 +409,7 @@ function _uniform3D_impl(ρ::T, p::Pattern3D, field_x::T, field_y::T;
405409
if length(zrange) != 2 || zrange[1] >= zrange[2]
406410
throw(ArgumentError("zrange must be a vector of two values [min_z, max_z] where min_z < max_z"))
407411
end
408-
412+
409413
# Generate random number of patterns (note: ρ is 2D density)
410414
npatterns = rand(Poisson(field_x * field_y * ρ))
411415
ntotal = npatterns * p.n
@@ -414,7 +418,8 @@ function _uniform3D_impl(ρ::T, p::Pattern3D, field_x::T, field_y::T;
414418
x = Vector{T}(undef, ntotal)
415419
y = Vector{T}(undef, ntotal)
416420
z = Vector{T}(undef, ntotal)
417-
421+
pattern_ids = Vector{Int}(undef, ntotal)
422+
418423
idx = 1
419424
for nn = 1:npatterns
420425
# Random position
@@ -425,12 +430,12 @@ function _uniform3D_impl(ρ::T, p::Pattern3D, field_x::T, field_y::T;
425430
# Generate random 3D rotation using quaternions
426431
# This gives uniform rotation in 3D space
427432
u1, u2, u3 = rand(3)
428-
433+
429434
q0 = sqrt(1 - u1) * sin(2π * u2)
430435
q1 = sqrt(1 - u1) * cos(2π * u2)
431436
q2 = sqrt(u1) * sin(2π * u3)
432437
q3 = sqrt(u1) * cos(2π * u3)
433-
438+
434439
# Convert quaternion to rotation matrix
435440
R = [
436441
1-2*(q2^2 + q3^2) 2*(q1*q2 - q0*q3) 2*(q1*q3 + q0*q2);
@@ -444,11 +449,12 @@ function _uniform3D_impl(ρ::T, p::Pattern3D, field_x::T, field_y::T;
444449
x[idx] = pos[1] + x0
445450
y[idx] = pos[2] + y0
446451
z[idx] = pos[3] + z0
452+
pattern_ids[idx] = nn
447453
idx += 1
448454
end
449455
end
450456

451-
return x, y, z
457+
return x, y, z, pattern_ids
452458
end
453459

454460
#==========================================================================

src/core/photophysics.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,8 @@ function kinetic_model(smld::BasicSMLD, f::Molecule, nframes::Int, framerate::Re
221221
σ_xy=0.0, # x-y covariance (0 for symmetric PSF)
222222
frame=frame,
223223
dataset=dd,
224-
track_id=pos.track_id
224+
track_id=pos.track_id,
225+
id=pos.id
225226
)
226227
elseif emitter_type <: Emitter3DFit
227228
emitter = Emitter3DFit{Float64}(
@@ -233,7 +234,8 @@ function kinetic_model(smld::BasicSMLD, f::Molecule, nframes::Int, framerate::Re
233234
σ_xy=0.0, # x-y covariance (0 for symmetric PSF)
234235
frame=frame,
235236
dataset=dd,
236-
track_id=pos.track_id
237+
track_id=pos.track_id,
238+
id=pos.id
237239
)
238240
else
239241
error("Unsupported emitter type: $emitter_type")

src/static/simulation.jl

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,18 @@ function simulate(params::StaticSMLMParams;
139139
field_y = maximum(centers_y) - minimum(centers_y)
140140

141141
# Generate binding site coordinates based on pattern type
142-
coords = if pattern isa Pattern2D
143-
uniform2D(params.density, pattern, field_x, field_y)
142+
# uniform2D/uniform3D now return pattern_ids as well
143+
coords_with_ids = if pattern isa Pattern2D
144+
x, y, pattern_ids = uniform2D(params.density, pattern, field_x, field_y)
145+
((x, y), pattern_ids)
144146
else
145-
uniform3D(params.density, pattern, field_x, field_y; zrange=params.zrange)
147+
x, y, z, pattern_ids = uniform3D(params.density, pattern, field_x, field_y; zrange=params.zrange)
148+
((x, y, z), pattern_ids)
146149
end
150+
coords, pattern_ids = coords_with_ids
147151

148152
# Apply labeling to expand binding sites to fluorophore positions
149-
coords = apply_labeling(coords, labeling)
153+
coords, pattern_ids = apply_labeling(coords, pattern_ids, labeling)
150154

151155
# Create emitters for true positions
152156
emitters = if pattern isa Pattern2D
@@ -160,7 +164,8 @@ function simulate(params::StaticSMLMParams;
160164
σ_xy=0.0, # x-y covariance (0 for symmetric PSF)
161165
frame=1,
162166
dataset=1,
163-
track_id=i
167+
track_id=i,
168+
id=pattern_ids[i]
164169
) for i in 1:length(x)]
165170
else
166171
x, y, z = coords
@@ -173,7 +178,8 @@ function simulate(params::StaticSMLMParams;
173178
σ_xy=0.0, # x-y covariance (0 for symmetric PSF)
174179
frame=1,
175180
dataset=1,
176-
track_id=i
181+
track_id=i,
182+
id=pattern_ids[i]
177183
) for i in 1:length(x)]
178184
end
179185

0 commit comments

Comments
 (0)