Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace getproperty parameter data access with typed getindex to improve inference #9

Merged
merged 9 commits into from
Apr 1, 2021
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
fail-fast: false
matrix:
version:
- '1.5'
- '1.6'
- 'nightly'
os:
- ubuntu-latest
Expand Down
6 changes: 3 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
name = "C3D"
uuid = "de436766-0b2a-5f82-bc5b-1ccc5f599b83"
authors = ["Allen Hill <allenofthehills@gmail.com>"]
version = "0.6.3"
version = "0.7.0"

[deps]
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
VaxData = "a6f58a78-e649-57e2-81bc-1d865c7b74f7"

[compat]
julia = "1"
VaxData = "0.5"
julia = "1.6"
VaxData = "0.5,0.6"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,14 @@ Symbol[:DESCRIPTIONS, :RATE, :DATA_START, :FRAMES, :USED, :UNITS, :Y_SCREEN, :LA

There are two ways to access a specific parameter. The first (and most convenient) directly references the data contained in the parameter.

**BREAKING**: Previous versions (<v0.7.0) of C3D.jl used a different syntax for this:
`pc_real.groups[:POINT].USED`. See PR#9 for the reasons for the change.

```julia
julia> pc_real.groups[:POINT].USED
julia> pc_real.groups[:POINT][:USED]
26

julia> pc_real.groups[:POINT].LABELS
julia> pc_real.groups[:POINT][:LABELS]
48-element Array{String,1}:
"RFT1"
"RFT2"
Expand Down
160 changes: 82 additions & 78 deletions src/C3D.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,24 @@ function C3DFile(name::String, header::Header, groups::Dict{Symbol,Group},
l = size(point, 1)
allpoints = 1:l

if !iszero(groups[:POINT].USED)
for (idx, symname) in enumerate(groups[:POINT].LABELS[1:groups[:POINT].USED])
if !iszero(groups[:POINT][Int, :USED])
for (idx, symname) in enumerate(groups[:POINT][Vector{String}, :LABELS][1:groups[:POINT][Int, :USED]][:])
fpoint[symname] = point[:,((idx-1)*3+1):((idx-1)*3+3)]
fresiduals[symname] = residuals[:,idx]
if missingpoints
invalidpoints = findall(x -> x === -1.0f0, fresiduals[symname])
calculatedpoints = findall(iszero, fresiduals[symname])
goodpoints = setdiff(allpoints, invalidpoints ∪ calculatedpoints)
fpoint[symname][invalidpoints, :] .= missing
fresiduals[symname][goodpoints] = calcresiduals(fresiduals[symname], abs(groups[:POINT].SCALE))[goodpoints]
fresiduals[symname][goodpoints] = calcresiduals(fresiduals[symname], abs(groups[:POINT][Float32, :SCALE]))[goodpoints]
fresiduals[symname][invalidpoints] .= missing
fresiduals[symname][calculatedpoints] .= 0.0f0
end
end
end

if !iszero(groups[:ANALOG].USED)
for (idx, symname) in enumerate(groups[:ANALOG].LABELS[1:groups[:ANALOG].USED])
if !iszero(groups[:ANALOG][Int, :USED])
for (idx, symname) in enumerate(groups[:ANALOG][Vector{String}, :LABELS][1:groups[:ANALOG][Int, :USED]])
fanalog[symname] = analog[:, idx]
end
end
Expand All @@ -64,33 +64,33 @@ function Base.show(io::IO, f::C3DFile)
if get(io, :compact, true)
print(io, "C3DFile(\"", f.name, "\")")
else
length = (f.groups[:POINT].FRAMES == typemax(UInt16)) ?
f.groups[:POINT].LONG_FRAMES :
f.groups[:POINT].FRAMES
length = (f.groups[:POINT][Int, :FRAMES] == typemax(UInt16)) ?
f.groups[:POINT][Int, :LONG_FRAMES] :
f.groups[:POINT][Int, :FRAMES]

print(io, "C3DFile(\"", f.name, "\", ",
length, "sec, ",
f.groups[:POINT].USED, " points, ",
f.groups[:ANALOG].USED, " analog channels)")
f.groups[:POINT][Int, :USED], " points, ",
f.groups[:ANALOG][Int, :USED], " analog channels)")
end
end

function calcresiduals(x::AbstractVector, scale)
residuals = (reinterpret.(UInt32, x) .>> 16) .& 0xff .* scale
end

function readdata(f::IOStream, groups::Dict{Symbol,Group}, FEND::Endian, FType::Type{T}) where T <: Union{Float32,VaxFloatF}
seek(f, (groups[:POINT].DATA_START-1)*512)
function readdata(io::IOStream, groups::Dict{Symbol,Group}, FEND::Endian, FType::Type{T}) where T <: Union{Float32,VaxFloatF}
seek(io, (groups[:POINT][Int, :DATA_START]-1)*512)

format = groups[:POINT].SCALE > 0 ? Int16 : FType
format = groups[:POINT][Float32, :SCALE] > 0 ? Int16 : FType

# Read data in a transposed structure for better read/write speeds due to Julia being
# column-order arrays
numframes = convert(Int, groups[:POINT].FRAMES)
if numframes == typemax(UInt16) && haskey(groups, :TRIAL) && haskey(groups[:TRIAL].params, :ACTUAL_END_FIELD)
numframes = convert(Int, reinterpret(Int32, groups[:TRIAL].ACTUAL_END_FIELD)[1])
numframes::Int = groups[:POINT][Int, :FRAMES]
if numframes == typemax(UInt16) && haskey(groups, :TRIAL) && haskey(groups[:TRIAL][:params], :ACTUAL_END_FIELD)
numframes = only(reinterpret(Int32, groups[:TRIAL][Vector{Int16}, :ACTUAL_END_FIELD]))
end
nummarkers = convert(Int, groups[:POINT].USED)
nummarkers = groups[:POINT][Int, :USED]
hasmarkers = !iszero(nummarkers)
if hasmarkers
point = Array{Float32,2}(undef, nummarkers*3, numframes)
Expand All @@ -100,51 +100,55 @@ function readdata(f::IOStream, groups::Dict{Symbol,Group}, FEND::Endian, FType::
pointidxs = filter(x -> x % 4 != 0, 1:nb)
residxs = filter(x -> x % 4 == 0, 1:nb)

pointtmp = Array{format}(undef, nb)
pointtmp = Vector{format}(undef, nb)
pointview = view(pointtmp, pointidxs)
resview = view(pointtmp, residxs)
else
point = Array{Float32,2}(undef, 0,0)
residuals = Array{Float32,2}(undef, 0,0)
end

numchannels = convert(Int, groups[:ANALOG].USED)
numchannels = groups[:ANALOG][Int, :USED]
haschannels = !iszero(numchannels)
if haschannels
# Analog Samples Per Frame => ASPF
aspf = convert(Int, groups[:ANALOG].RATE/groups[:POINT].RATE)
aspf = convert(Int, groups[:ANALOG][Float32, :RATE]/groups[:POINT][Float32, :RATE])
analog = Array{Float32,2}(undef, numchannels, aspf*numframes)

analogtmp = Array{format}(undef, (numchannels,aspf))
analogtmp = Matrix{format}(undef, (numchannels,aspf))
else
analog = Array{Float32,2}(undef, 0,0)
end

@inbounds for i in 1:numframes
if hasmarkers
saferead!(f, pointtmp, FEND)
point[:,i] = convert.(Float32, pointview) # Convert from `format` (eg Int16 or FType)
residuals[:,i] = convert.(Float32, resview)
saferead!(io, pointtmp, FEND)
point[:,i] = pointview
residuals[:,i] = resview
end
if haschannels
saferead!(f, analogtmp, FEND)
analog[:,((i-1)*aspf+1):(i*aspf)] = convert.(Float32, analogtmp)
saferead!(io, analogtmp, FEND)
analog[:,((i-1)*aspf+1):(i*aspf)] = analogtmp
end
end

if hasmarkers && format == Int16
# Multiply or divide by [:point][:scale]
point .*= abs(groups[:POINT].SCALE)
POINT_SCALE = groups[:POINT][Float32, :SCALE]
point .*= abs(POINT_SCALE)
end

if haschannels
if numchannels == 1
analog[:] = (analog .- groups[:ANALOG].OFFSET) .*
(groups[:ANALOG].GEN_SCALE * groups[:ANALOG].SCALE)
ANALOG_OFFSET = groups[:ANALOG][Float32, :OFFSET]
SCALE = (groups[:ANALOG][Float32, :GEN_SCALE] * groups[:ANALOG][Float32, :SCALE])
analog .= (analog .- ANALOG_OFFSET) .* SCALE
else
analog[:] = (analog .- groups[:ANALOG].OFFSET[1:numchannels]) .*
groups[:ANALOG].GEN_SCALE .*
groups[:ANALOG].SCALE[1:numchannels]
VECANALOG_OFFSET = groups[:ANALOG][Vector{Int}, :OFFSET][1:numchannels]
VECSCALE = groups[:ANALOG][Float32, :GEN_SCALE] .*
groups[:ANALOG][Vector{Float32}, :SCALE][1:numchannels]

analog .= (analog .- VECANALOG_OFFSET) .* VECSCALE
end
end

Expand Down Expand Up @@ -184,11 +188,11 @@ function saferead(io::IOStream, ::Type{VaxFloatF}, FEND::Endian)::Float32
end
end

function saferead(io::IOStream, ::Type{VaxFloatF}, FEND::Endian, dims)::Array{Float32}
function saferead(io::IOStream, ::Type{VaxFloatF}, FEND::Endian, dims::NTuple{N,Int})::Array{Float32,N} where N
if FEND == LE
return convert.(Float32, ltoh.(read!(io, Array{VaxFloatF}(undef, dims))))
return convert(Array{Float32,N}, ltoh.(read!(io, Array{VaxFloatF}(undef, dims))))
else
return convert.(Float32, ntoh.(read!(io, Array{VaxFloatF}(undef, dims))))
return convert(Array{Float32,N}, ntoh.(read!(io, Array{VaxFloatF}(undef, dims))))
end
end

Expand All @@ -208,45 +212,45 @@ function readc3d(fn::AbstractString; paramsonly=false, validate=true,
error("File ", fn, " cannot be found")
end

f = open(fn, "r")
io = open(fn, "r")

groups, header, FEND, FType = _readparams(fn, f)
groups, header, FEND, FType = _readparams(fn, io)

if validate
validatec3d(header, groups, complete=false)
validatec3d(header, groups)
end

if paramsonly
point = Dict{String,Array{Union{Missing, Float32},2}}()
residual = Dict{String,Array{Union{Missing, Float32},1}}()
analog = Dict{String,Array{Float32,1}}()
else
(point, residual, analog) = readdata(f, groups, FEND, FType)
(point, residual, analog) = readdata(io, groups, FEND, FType)
end

res = C3DFile(fn, header, groups, point, residual, analog; missingpoints=missingpoints)
close(io)

close(f)
res = C3DFile(fn, header, groups, point, residual, analog; missingpoints=missingpoints)

return res
end

function _readparams(fn::String, f::IOStream)
params_ptr = read(f, UInt8)
function _readparams(fn::String, io::IOStream)
params_ptr = read(io, UInt8)

if read(f, UInt8) != 0x50
if read(io, UInt8) != 0x50
error("File ", fn, " is not a valid C3D file")
end

# Jump to parameters block
seek(f, (params_ptr - 1) * 512)
seek(io, (params_ptr - 1) * 512)

# Skip 2 reserved bytes
# TODO: store bytes for saving modified files
skip(f, 2)
skip(io, 2)

paramblocks = read(f, UInt8)
proctype = read(f, Int8) - 83
paramblocks = read(io, UInt8)
proctype = read(io, Int8) - 83

FType = Float32

Expand All @@ -265,89 +269,89 @@ function _readparams(fn::String, f::IOStream)
error("Malformed processor type. Expected 1, 2, or 3. Found ", proctype)
end

mark(f)
header = readheader(f, FEND, FType)
reset(f)
unmark(f)
mark(io)
header = readheader(io, FEND, FType)
reset(io)
unmark(io)

gs = Array{Group,1}()
ps = Array{AbstractParameter,1}()
ps = Array{Parameter,1}()
moreparams = true
fail = 0
np = 0

skip(f, 1)
if read(f, Int8) < 0
skip(io, 1)
if read(io, Int8) < 0
# Group
skip(f, -2)
push!(gs, readgroup(f, FEND, FType))
skip(io, -2)
push!(gs, readgroup(io, FEND, FType))
np = gs[end].pos + gs[end].np + abs(gs[end].nl) + 2
moreparams = gs[end].np != 0 ? true : false
else
# Parameter
skip(f, -2)
push!(ps, readparam(f, FEND, FType))
skip(io, -2)
push!(ps, readparam(io, FEND, FType))
np = ps[end].pos + ps[end].np + abs(ps[end].nl) + 2
moreparams = ps[end].np != 0 ? true : false
end

while moreparams
# Mark current position in file in case the pointer is incorrect
mark(f)
if fail === 0 && np != position(f)
@debug "Pointer mismatch at position $(position(f)) where pointer was $np"
seek(f, np)
mark(io)
if fail === 0 && np != position(io)
# @debug "Pointer mismatch at position $(position(io)) where pointer was $np"
seek(io, np)
elseif fail > 1 # this is the second failed attempt
@debug "Second failed parameter read attempt from $(position(f))"
# @debug "Second failed parameter read attempt from $(position(io))"
break
end

# Read the next two bytes to get the gid
skip(f, 1)
local gid = read(f, Int8)
skip(io, 1)
local gid = read(io, Int8)
if gid < 0 # Group
# Reset to the beginning of the group
skip(f, -2)
skip(io, -2)
try
push!(gs, readgroup(f, FEND, FType))
push!(gs, readgroup(io, FEND, FType))
np = gs[end].pos + gs[end].np + abs(gs[end].nl) + 2
moreparams = gs[end].np != 0 ? true : false # break if the pointer is 0 (ie the parameters are finished)
fail = 0 # reset fail counter following a successful read
catch e
# Last readgroup failed, possibly due to a bad pointer. Reset to the ending
# location of the last successfully read parameter and try again. Count the failure.
reset(f)
@debug "Read group failed, last parameter ended at $(position(f)), pointer at $np" fail
reset(io)
# @debug "Read group failed, last parameter ended at $(position(io)), pointer at $np" fail
fail += 1
finally
unmark(f) # Unmark the file regardless
unmark(io) # Unmark the file regardless
end
elseif gid > 0 # Parameter
# Reset to the beginning of the parameter
skip(f, -2)
skip(io, -2)
try
push!(ps, readparam(f, FEND, FType))
push!(ps, readparam(io, FEND, FType))
np = ps[end].pos + ps[end].np + abs(ps[end].nl) + 2
moreparams = ps[end].np != 0 ? true : false
fail = 0
catch e
reset(f)
@debug "Read group failed, last parameter ended at $(position(f)), pointer at $np" fail
reset(io)
# @debug "Read group failed, last parameter ended at $(position(io)), pointer at $np" fail
fail += 1
finally
unmark(f)
unmark(io)
end
else # Last parameter pointer is incorrect (assumption)
# The group ID should never be zero, if it is, the most likely explanation is
# that the pointer is incorrect (eg the pointer was not fixed when the previously
# last parameter was deleted or moved)
@debug "Bad last position. Assuming parameter section is finished."
# @debug "Bad last position. Assuming parameter section is finished."
break
end
end

groups = Dict{Symbol,Group}()
gids = Dict{Int,Symbol}()
gids = Dict{Int8,Symbol}()

for group in gs
groups[group.symname] = group
Expand Down
Loading