Skip to content

Commit f22ebe3

Browse files
authored
Add StructArrays support (#3)
This requires more modern versions of Julia, support just 1.6+.
1 parent 0cc45e9 commit f22ebe3

7 files changed

Lines changed: 94 additions & 6 deletions

File tree

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ jobs:
1818
fail-fast: false
1919
matrix:
2020
version:
21-
- '1.0'
2221
- '1.6'
22+
- '1'
2323
- 'nightly'
2424
os:
2525
- ubuntu-latest

Project.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
1010
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
1111
FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93"
1212
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
13+
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
1314

1415
[compat]
1516
ColorTypes = "0.11"
@@ -18,10 +19,12 @@ Colors = "0.12"
1819
Compat = "3.36"
1920
FixedPointNumbers = "0.8"
2021
Reexport = "1"
21-
julia = "1"
22+
Requires = "1"
23+
julia = "1.6"
2224

2325
[extras]
26+
StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a"
2427
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2528

2629
[targets]
27-
test = ["Test"]
30+
test = ["StructArrays", "Test"]

src/FluorophoreColors.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@ using Reexport
77
@reexport using ColorTypes
88
using Colors
99
using ColorVectorSpace
10+
using Requires
1011

1112
export fluorophore_rgb, @fluorophore_rgb_str, ColorMixture
1213

1314
include("types.jl")
1415
include("fluorophores.jl")
16+
include("utils.jl")
17+
18+
function __init__()
19+
@require StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" include("structarrays.jl")
20+
end
1521

1622
end

src/structarrays.jl

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
channelname(N::Int, i::Int) = Symbol("channel" * lpad(string(i), ndigits(N), '0'))
2+
3+
function StructArrays.staticschema(::Type{ColorMixture{T,N,Cs}}) where {T, N, Cs}
4+
# Define the desired names and eltypes of the "fields"
5+
names = ntuple(i -> channelname(N, i), N)
6+
types = Tuple{ntuple(i -> T, N)...}
7+
return NamedTuple{names, types}
8+
end
9+
10+
# Extract components
11+
@noinline throw_channelerror(key::Symbol) = error("channel $key not available")
12+
StructArrays.component(c::ColorMixture{T,1}, key::Symbol) where T = key == :channel1 ? c.channels[1] : throw_channelerror(key)
13+
StructArrays.component(c::ColorMixture{T,2}, key::Symbol) where T = key == :channel1 ? c.channels[1] :
14+
key == :channel2 ? c.channels[2] : throw_channelerror(key)
15+
StructArrays.component(c::ColorMixture{T,3}, key::Symbol) where T = key == :channel1 ? c.channels[1] :
16+
key == :channel2 ? c.channels[2] :
17+
key == :channel3 ? c.channels[3] : throw_channelerror(key)
18+
19+
@generated function StructArrays.component(c::ColorMixture{T,N}, key::Symbol) where {T,N}
20+
ex0 = ex = Expr(:if, :(key == $(QuoteNode(channelname(N, 1)))), :(return c.channels[1]))
21+
for i = 2:N
22+
push!(ex.args, Expr(:elseif, :(key == $(QuoteNode(channelname(N, i)))), :(return c.channels[$i])))
23+
ex = ex.args[end]
24+
end
25+
push!(ex.args, :(throw_channelerror(key)))
26+
return ex0
27+
end
28+
29+
function StructArrays.createinstance(::Type{ColorMixture{T,N,Cs}}, args...) where {T, N, Cs}
30+
return ColorMixture{T,N,Cs}(args)
31+
end

src/types.jl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
# Making the main representation by RGB means we can do the latter efficiently without requiring
55
# world-age violations.
66

7-
const subscript = ('', '', '', '', '', '', '', '', '')
8-
97
"""
108
ColorMixture((rgb₁, rgb₂), (i₁, i₂)) # store intensities
119
ColorMixture{T}((rgb₁, rgb₂), (i₁, i₂)) # same, but coerce to element type T for colors and intensities
@@ -99,7 +97,8 @@ function Base.show(io::IO, c::ColorMixture)
9997
print(io, '(')
10098
for (j, intensity) in enumerate(c.channels)
10199
j > 1 && print(io, ", ")
102-
print(io, intensity, subscript[j])
100+
print(io, intensity)
101+
print_subscript(io, length(c), j)
103102
end
104103
print(io, ')')
105104
end

src/utils.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const subscripts = ('', '', '', '', '', '', '', '', '', '')
2+
function print_subscript(io::IO, N::Int, i::Int)
3+
nd = ndigits(N)
4+
base = 10^(nd-1)
5+
while nd > 1
6+
id, i = divrem(i, base)
7+
print(io, subscripts[id+1])
8+
nd -= 1
9+
base ÷= 10
10+
end
11+
print(io, subscripts[i+1])
12+
return nothing
13+
end

test/runtests.jl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using FluorophoreColors
22
using Test
33

4+
# interacts via @require
5+
using StructArrays
6+
47
@testset "FluorophoreColors.jl" begin
58
@test isempty(detect_ambiguities(FluorophoreColors))
69

@@ -27,6 +30,7 @@ using Test
2730
@test c.channels[1] == c.channels[2] == 0.5
2831
@test convert(RGB, c) 0.5*channels[1] + 0.5*channels[2]
2932

33+
# Macro syntax & inferrability
3034
f_infer(i1, i2) = ColorMixture{N0f8}((fluorophore_rgb"EGFP", fluorophore_rgb"tdTomato"), (i1, i2))
3135
f_noinfer(i1, i2) = ColorMixture((fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"]), (i1, i2))
3236
@test f_infer(1, 0) == f_noinfer(1, 0)
@@ -38,13 +42,45 @@ using Test
3842
f_infer16(i1, i2) = ColorMixture{N0f16}((fluorophore_rgb"EGFP", fluorophore_rgb"tdTomato"), (i1, i2))
3943
@test_broken @inferred f_infer16(1, 0)
4044

45+
# Inferrability from a template
4146
ctmpl = ColorMixture(channels)
4247
@test @inferred(ctmpl(1, 0)) === ColorMixture{N0f8}(channels, (1, 0))
4348
end
4449

50+
@testset "StructArrays" begin
51+
channels = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"])
52+
ctemplate = ColorMixture(channels)
53+
green, red = N0f8[0.2, 0.4], N0f8[0.8, 0.6]
54+
soa = StructArray{typeof(ctemplate)}((green, red))
55+
@test soa[1] === ctemplate(green[1], red[1])
56+
@test soa[2] === ctemplate(green[2], red[2])
57+
soa[1] = ctemplate((0.5, 0.0))
58+
@test green[1] === 0.5N0f8
59+
@test red[1] === 0.0N0f8
60+
61+
# Hyperspectral
62+
cols = FluorophoreColors.Colors.distinguishable_colors(16, [RGB(0,0,0)]; dropseed=true)
63+
ctemplate = ColorMixture{Float32}((cols...,))
64+
comps = collect(reshape((0:31)/32f0, 16, 2))
65+
compsr = reinterpret(reshape, typeof(ctemplate), comps)
66+
soa = StructArray{typeof(ctemplate)}(comps; dims=1)
67+
@test soa == compsr
68+
@test size(soa) == (2,)
69+
@test soa[1] == ctemplate(ntuple(i->(i-1)/32, 16))
70+
compst = collect(transpose(comps))
71+
soa = StructArray{typeof(ctemplate)}(compst; dims=2)
72+
@test soa[1] == ctemplate(ntuple(i->(i-1)/32, 16))
73+
end
74+
4575
@testset "IO" begin
4676
channels = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"])
4777
c = ColorMixture(channels, (1, 0))
4878
@test sprint(show, c) == "(1.0₁, 0.0₂)"
79+
80+
# Hyperspectral
81+
cols = FluorophoreColors.Colors.distinguishable_colors(16, [RGB(0,0,0)]; dropseed=true)
82+
ctemplate = ColorMixture{Float32}((cols...,))
83+
c = ctemplate([i/16 for i = 0:15]...)
84+
@test sprint(show, c) == "(0.0₀₁, 0.0625₀₂, 0.125₀₃, 0.1875₀₄, 0.25₀₅, 0.3125₀₆, 0.375₀₇, 0.4375₀₈, 0.5₀₉, 0.5625₁₀, 0.625₁₁, 0.6875₁₂, 0.75₁₃, 0.8125₁₄, 0.875₁₅, 0.9375₁₆)"
4985
end
5086
end

0 commit comments

Comments
 (0)