Skip to content

Commit

Permalink
format
Browse files Browse the repository at this point in the history
  • Loading branch information
ericphanson committed Oct 16, 2023
1 parent 179fd4f commit 301b1d1
Show file tree
Hide file tree
Showing 11 changed files with 408 additions and 217 deletions.
27 changes: 17 additions & 10 deletions OndaEDFSchemas.jl/src/OndaEDFSchemas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,14 @@ export PlanV1, PlanV2, FilePlanV1, FilePlanV2, EDFAnnotationV1
kind::Union{Missing,AbstractString} = lift(String, kind)
channel::Union{Missing,AbstractString} = lift(String, channel)
sample_unit::Union{Missing,AbstractString} = lift(String, sample_unit)
sample_resolution_in_unit::Union{Missing,LPCM_SAMPLE_TYPE_UNION} = lift(convert_number_to_lpcm_sample_type, sample_resolution_in_unit)
sample_offset_in_unit::Union{Missing,LPCM_SAMPLE_TYPE_UNION} = lift(convert_number_to_lpcm_sample_type, sample_offset_in_unit)
sample_type::Union{Missing,AbstractString} = lift(onda_sample_type_from_julia_type, sample_type)
sample_rate::Union{Missing,LPCM_SAMPLE_TYPE_UNION} = lift(convert_number_to_lpcm_sample_type, sample_rate)
sample_resolution_in_unit::Union{Missing,LPCM_SAMPLE_TYPE_UNION} = lift(convert_number_to_lpcm_sample_type,
sample_resolution_in_unit)
sample_offset_in_unit::Union{Missing,LPCM_SAMPLE_TYPE_UNION} = lift(convert_number_to_lpcm_sample_type,
sample_offset_in_unit)
sample_type::Union{Missing,AbstractString} = lift(onda_sample_type_from_julia_type,
sample_type)
sample_rate::Union{Missing,LPCM_SAMPLE_TYPE_UNION} = lift(convert_number_to_lpcm_sample_type,
sample_rate)
# errors, use `nothing` to indicate no error
error::Union{Nothing,String} = coalesce(error, nothing)
end
Expand All @@ -52,21 +56,21 @@ end
seconds_per_record::Float64
# Onda.SignalV2 fields (channels -> channel), may be missing
recording::Union{UUID,Missing} = lift(UUID, recording)
sensor_type::Union{Missing,AbstractString} = lift(_validate_signal_sensor_type, sensor_type)
sensor_type::Union{Missing,AbstractString} = lift(_validate_signal_sensor_type,
sensor_type)
sensor_label::Union{Missing,AbstractString} = lift(_validate_signal_sensor_label,
coalesce(sensor_label, sensor_type))
channel::Union{Missing,AbstractString} = lift(_validate_signal_channel, channel)
sample_unit::Union{Missing,AbstractString} = lift(String, sample_unit)
sample_resolution_in_unit::Union{Missing,Float64}
sample_offset_in_unit::Union{Missing,Float64}
sample_type::Union{Missing,AbstractString} = lift(onda_sample_type_from_julia_type, sample_type)
sample_type::Union{Missing,AbstractString} = lift(onda_sample_type_from_julia_type,
sample_type)
sample_rate::Union{Missing,Float64}
# errors, use `nothing` to indicate no error
error::Union{Nothing,String} = coalesce(error, nothing)
end



const PLAN_DOC_TEMPLATE = """
@version PlanV{{ VERSION }} begin
# EDF.SignalHeader fields
Expand Down Expand Up @@ -159,11 +163,14 @@ end
@doc _file_plan_doc(1) FilePlanV1
@doc _file_plan_doc(2) FilePlanV2

const OndaEDFSchemaVersions = Union{PlanV1SchemaVersion,PlanV2SchemaVersion,FilePlanV1SchemaVersion,FilePlanV2SchemaVersion}
const OndaEDFSchemaVersions = Union{PlanV1SchemaVersion,PlanV2SchemaVersion,
FilePlanV1SchemaVersion,FilePlanV2SchemaVersion}
Legolas.accepted_field_type(::OndaEDFSchemaVersions, ::Type{String}) = AbstractString
# we need this because Arrow write can introduce a Missing for the error column
# (I think because of how missing/nothing sentinels are handled?)
Legolas.accepted_field_type(::OndaEDFSchemaVersions, ::Type{Union{Nothing,String}}) = Union{Nothing,Missing,AbstractString}
function Legolas.accepted_field_type(::OndaEDFSchemaVersions, ::Type{Union{Nothing,String}})
return Union{Nothing,Missing,AbstractString}
end

@schema "edf.annotation" EDFAnnotation

Expand Down
19 changes: 10 additions & 9 deletions OndaEDFSchemas.jl/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,19 @@ function mock_plan(; v, rng=GLOBAL_RNG)
physical_dimension="uV",
physical_minimum=0.0,
physical_maximum=2.0,
digital_minimum=-1f4,
digital_maximum=1f4,
digital_minimum=-1.0f4,
digital_maximum=1.0f4,
prefilter="HP 0.1Hz; LP 80Hz; N 60Hz",
samples_per_record=128,
seconds_per_record=1.0,
channel=ingested ? "cz-m1" : missing,
sample_unit=ingested ? "microvolt" : missing,
sample_resolution_in_unit=ingested ? 1f-4 : missing,
sample_resolution_in_unit=ingested ? 1.0f-4 : missing,
sample_offset_in_unit=ingested ? 1.0 : missing,
sample_type=ingested ? "float32" : missing,
sample_rate=ingested ? 1/128 : missing,
sample_rate=ingested ? 1 / 128 : missing,
error=errored ? "Error blah blah" : nothing,
recording= (ingested && rand(rng, Bool)) ? uuid4() : missing,
recording=(ingested && rand(rng, Bool)) ? uuid4() : missing,
specific_kwargs...)
end

Expand All @@ -63,7 +63,7 @@ end

@testset "Schema version $v" for v in (1, 2)
SamplesInfo = v == 1 ? Onda.SamplesInfoV1 : SamplesInfoV2

@testset "ondaedf.plan@$v" begin
rng = StableRNG(10)
plans = mock_plan(30; v, rng)
Expand All @@ -75,21 +75,22 @@ end
# conversion to samples info with channel -> channels
@test all(x -> isa(x, SamplesInfo),
SamplesInfo(Tables.rowmerge(p; channels=[p.channel]))
for p in plans if !ismissing(p.channel))
for p in plans if !ismissing(p.channel))
end

@testset "ondaedf.file-plan@$v" begin
rng = StableRNG(11)
file_plans = mock_file_plan(50; v, rng)
schema = Tables.schema(file_plans)
@test nothing === Legolas.validate(schema, Legolas.SchemaVersion("ondaedf.file-plan", v))
@test nothing ===
Legolas.validate(schema, Legolas.SchemaVersion("ondaedf.file-plan", v))
tbl = Arrow.Table(Arrow.tobuffer(file_plans; maxdepth=9))
@test isequal(Tables.columntable(tbl), Tables.columntable(file_plans))

# conversion to samples info with channel -> channels
@test all(x -> isa(x, SamplesInfo),
SamplesInfo(Tables.rowmerge(p; channels=[p.channel]))
for p in file_plans if !ismissing(p.channel))
for p in file_plans if !ismissing(p.channel))
end
end

Expand Down
4 changes: 2 additions & 2 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using OndaEDF
using Documenter

makedocs(modules=[OndaEDF, OndaEDF.OndaEDFSchemas],
makedocs(; modules=[OndaEDF, OndaEDF.OndaEDFSchemas],
sitename="OndaEDF",
authors="Beacon Biosignals and other contributors",
pages=["OndaEDF" => "index.md",
"Converting from EDF" => "convert-to-onda.md",
"API Documentation" => "api.md"])

deploydocs(repo="github.com/beacon-biosignals/OndaEDF.jl.git",
deploydocs(; repo="github.com/beacon-biosignals/OndaEDF.jl.git",
push_preview=true)
3 changes: 2 additions & 1 deletion src/OndaEDF.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ using Legolas: lift
using Tables: rowmerge

export write_plan
export edf_to_onda_samples, edf_to_onda_annotations, plan_edf_to_onda_samples, plan_edf_to_onda_samples_groups, store_edf_as_onda
export edf_to_onda_samples, edf_to_onda_annotations, plan_edf_to_onda_samples,
plan_edf_to_onda_samples_groups, store_edf_as_onda
export onda_to_edf

include("standards.jl")
Expand Down
43 changes: 29 additions & 14 deletions src/export_edf.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ end
SignalExtrema(samples::Samples) = SignalExtrema(samples.info)
function SignalExtrema(info::SamplesInfoV2)
digital_extrema = (typemin(sample_type(info)), typemax(sample_type(info)))
physical_extrema = @. (info.sample_resolution_in_unit * digital_extrema) + info.sample_offset_in_unit
physical_extrema = @. (info.sample_resolution_in_unit * digital_extrema) +
info.sample_offset_in_unit
return SignalExtrema(physical_extrema..., digital_extrema...)
end

Expand All @@ -23,7 +24,9 @@ end
const DATA_RECORD_SIZE_LIMIT = 30720
const EDF_BYTE_LIMIT = 8

edf_sample_count_per_record(samples::Samples, seconds_per_record::Float64) = Int16(samples.info.sample_rate * seconds_per_record)
function edf_sample_count_per_record(samples::Samples, seconds_per_record::Float64)
return Int16(samples.info.sample_rate * seconds_per_record)
end

_rationalize(x) = rationalize(x)
_rationalize(x::Int) = x // 1
Expand All @@ -40,10 +43,12 @@ function edf_record_metadata(all_samples::AbstractVector{<:Onda.Samples})
else
scale = gcd(numerator.(sample_rates) .* seconds_per_record)
samples_per_record ./= scale
sum(samples_per_record) > DATA_RECORD_SIZE_LIMIT && throw(RecordSizeException(all_samples))
sum(samples_per_record) > DATA_RECORD_SIZE_LIMIT &&
throw(RecordSizeException(all_samples))
seconds_per_record /= scale
end
sizeof(string(seconds_per_record)) > EDF_BYTE_LIMIT && throw(EDFPrecisionError(seconds_per_record))
sizeof(string(seconds_per_record)) > EDF_BYTE_LIMIT &&
throw(EDFPrecisionError(seconds_per_record))
end
record_duration_in_nanoseconds = Nanosecond(seconds_per_record * 1_000_000_000)
signal_duration = maximum(Onda.duration, all_samples)
Expand All @@ -53,7 +58,7 @@ function edf_record_metadata(all_samples::AbstractVector{<:Onda.Samples})
end

struct RecordSizeException <: Exception
samples
samples::Any
end

struct EDFPrecisionError <: Exception
Expand All @@ -64,13 +69,13 @@ function Base.showerror(io::IO, exception::RecordSizeException)
print(io, "RecordSizeException: sample rates ")
print(io, [s.info.sample_rate for s in exception.samples])
print(io, " cannot be resolved to a data record size smaller than ")
print(io, DATA_RECORD_SIZE_LIMIT * 2, " bytes")
return print(io, DATA_RECORD_SIZE_LIMIT * 2, " bytes")
end

function Base.showerror(io::IO, exception::EDFPrecisionError)
print(io, "EDFPrecisionError: String representation of value ")
print(io, exception.value)
print(io, " is longer than 8 ASCII characters")
return print(io, " is longer than 8 ASCII characters")
end

#####
Expand All @@ -88,8 +93,10 @@ end

function onda_samples_to_edf_header(samples::AbstractVector{<:Samples};
version::AbstractString="0",
patient_metadata=EDF.PatientID(missing, missing, missing, missing),
recording_metadata=EDF.RecordingID(missing, missing, missing, missing),
patient_metadata=EDF.PatientID(missing, missing,
missing, missing),
recording_metadata=EDF.RecordingID(missing, missing,
missing, missing),
is_contiguous::Bool=true,
start::DateTime=DateTime(Year(1985)))
return EDF.FileHeader(version, patient_metadata, recording_metadata, start,
Expand Down Expand Up @@ -177,7 +184,8 @@ function reencode_samples(samples::Samples, sample_type::Type{<:Integer}=Int16)
return encode(new_samples)
end

function onda_samples_to_edf_signals(onda_samples::AbstractVector{<:Samples}, seconds_per_record::Float64)
function onda_samples_to_edf_signals(onda_samples::AbstractVector{<:Samples},
seconds_per_record::Float64)
edf_signals = Union{EDF.AnnotationsSignal,EDF.Signal{Int16}}[]
for samples in onda_samples
# encode samples, rescaling if necessary
Expand All @@ -187,15 +195,19 @@ function onda_samples_to_edf_signals(onda_samples::AbstractVector{<:Samples}, se
for channel_name in samples.info.channels
sample_count = edf_sample_count_per_record(samples, seconds_per_record)
physical_dimension = onda_to_edf_unit(samples.info.sample_unit)
edf_signal_header = EDF.SignalHeader(export_edf_label(signal_name, channel_name),
edf_signal_header = EDF.SignalHeader(export_edf_label(signal_name,
channel_name),
"", physical_dimension,
extrema.physical_min, extrema.physical_max,
extrema.digital_min, extrema.digital_max,
"", sample_count)
# manually convert here in case we have input samples whose encoded
# values are convertible losslessly to Int16:
sample_data = Int16.(vec(samples[channel_name, :].data))
padding = Iterators.repeated(zero(Int16), (sample_count - (length(sample_data) % sample_count)) % sample_count)
padding = Iterators.repeated(zero(Int16),
(sample_count -
(length(sample_data) % sample_count)) %
sample_count)
edf_signal_samples = append!(sample_data, padding)
push!(edf_signals, EDF.Signal(edf_signal_header, edf_signal_samples))
end
Expand Down Expand Up @@ -243,13 +255,16 @@ function onda_to_edf(samples::AbstractVector{<:Samples}, annotations=[]; kwargs.
edf_header = onda_samples_to_edf_header(samples; kwargs...)
edf_signals = onda_samples_to_edf_signals(samples, edf_header.seconds_per_record)
if !isempty(annotations)
records = [[EDF.TimestampedAnnotationList(edf_header.seconds_per_record * i, nothing, String[""])]
records = [[EDF.TimestampedAnnotationList(edf_header.seconds_per_record * i,
nothing, String[""])]
for i in 0:(edf_header.record_count - 1)]
for annotation in sort(Tables.rowtable(annotations); by=row -> start(row.span))
annotation_onset_in_seconds = start(annotation.span).value / 1e9
annotation_duration_in_seconds = duration(annotation.span).value / 1e9
matching_record = records[Int(fld(annotation_onset_in_seconds, edf_header.seconds_per_record)) + 1]
tal = EDF.TimestampedAnnotationList(annotation_onset_in_seconds, annotation_duration_in_seconds, [annotation.value])
tal = EDF.TimestampedAnnotationList(annotation_onset_in_seconds,
annotation_duration_in_seconds,
[annotation.value])
push!(matching_record, tal)
end
push!(edf_signals, EDF.AnnotationsSignal(records))
Expand Down
Loading

0 comments on commit 301b1d1

Please sign in to comment.