Skip to content

Commit

Permalink
Return file paths from store_edf_as_onda (#31)
Browse files Browse the repository at this point in the history
* Minor fix to `validate_arrow_prefix` regex
* Return file paths from `store_edf_as_onda`
* Use `Onda.Signal` returned from store
* Add test for `AbstractPath` support
* Set package version to 0.9.1
  • Loading branch information
omus committed Nov 15, 2021
1 parent 775c1ce commit a1d077b
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 54 deletions.
8 changes: 6 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
name = "OndaEDF"
uuid = "e3ed2cd1-99bf-415e-bb8f-38f4b42a544e"
authors = ["Beacon Biosignals, Inc."]
version = "0.9.0"
version = "0.9.1"

[deps]
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
EDF = "ccffbfc1-f56e-50fb-a33b-53d1781b2825"
Onda = "e853f5be-6863-11e9-128d-476edb89bfb5"
Expand All @@ -13,17 +14,20 @@ TimeSpans = "bb34ddd2-327f-4c4a-bfb0-c98fc494ece1"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[compat]
Compat = "3.32"
EDF = "0.6"
FilePathsBase = "0.9"
Onda = "0.12, 0.13, 0.14"
StatsBase = "0.33"
Tables = "1.4"
TimeSpans = "0.2"
julia = "1.4"

[extras]
FilePathsBase = "48062228-2e41-5def-b9a4-89aafe57970f"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test", "Random", "Statistics"]
test = ["FilePathsBase", "Test", "Random", "Statistics"]
1 change: 1 addition & 0 deletions src/OndaEDF.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module OndaEDF

using Base.Iterators
using Compat: @compat
using Dates, UUIDs
using Onda, EDF
using StatsBase
Expand Down
44 changes: 31 additions & 13 deletions src/import_edf.jl
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ function onda_samples_from_edf_signals(target::Onda.SamplesInfo, edf_signals,
end

"""
store_edf_as_onda(path, edf::EDF.File, uuid::UUID=uuid4();
store_edf_as_onda(edf::EDF.File, onda_dir, recording_uuid::UUID=uuid4();
custom_extractors=STANDARD_EXTRACTORS, import_annotations::Bool=true,
postprocess_samples=identity,
signals_prefix="edf", annotations_prefix=signals_prefix)
Expand All @@ -330,7 +330,7 @@ in `\$path/samples/`, and write the Onda signals and annotations tables to
"edf", and if a prefix is provided for signals but not annotations both will use
the signals prefix. The prefixes cannot reference (sub)directories.
Returns `uuid => (signals, annotations)`.
Returns `(; recording_uuid, signals, annotations, signals_path, annotations_path)`.
Samples are extracted with [`edf_to_onda_samples`](@ref), and EDF+ annotations are
extracted with [`edf_to_onda_annotations`](@ref) if `import_annotations==true`
Expand Down Expand Up @@ -365,44 +365,62 @@ and an `AmbiguousChannelError` displayed as a warning.
See the OndaEDF README for additional details regarding EDF formatting expectations.
"""
function store_edf_as_onda(path, edf::EDF.File, uuid::UUID=uuid4();
function store_edf_as_onda(edf::EDF.File, onda_dir, recording_uuid::UUID=uuid4();
custom_extractors=STANDARD_EXTRACTORS, import_annotations::Bool=true,
postprocess_samples=identity,
signals_prefix="edf", annotations_prefix=signals_prefix)

# Validate input argument early on
signals_path = joinpath(onda_dir, "$(validate_arrow_prefix(signals_prefix)).onda.signals.arrow")
annotations_path = joinpath(onda_dir, "$(validate_arrow_prefix(annotations_prefix)).onda.annotations.arrow")

EDF.read!(edf)
file_format = "lpcm.zst"

signals = Any[]
mkpath(joinpath(onda_dir, "samples"))

signals = Onda.Signal[]
edf_samples, diagnostics = edf_to_onda_samples(edf; custom_extractors=custom_extractors)
for e in diagnostics.errors
@warn sprint(showerror, e)
end
edf_samples = postprocess_samples(edf_samples)
for samples in edf_samples
file_path = joinpath(path, "samples", string(uuid, "_", samples.info.kind, ".", file_format))
signal = rowmerge(store(file_path, file_format, samples, uuid, Second(0)); file_path=string(file_path))
sample_filename = string(recording_uuid, "_", samples.info.kind, ".", file_format)
file_path = joinpath(onda_dir, "samples", sample_filename)
signal = store(file_path, file_format, samples, recording_uuid, Second(0))
push!(signals, signal)
end

signals_path = joinpath(path, "$(validate_arrow_prefix(signals_prefix)).onda.signals.arrow")
write_signals(signals_path, signals)
if import_annotations
annotations = edf_to_onda_annotations(edf, uuid)
annotations = edf_to_onda_annotations(edf, recording_uuid)
if !isempty(annotations)
annotations_path = joinpath(path, "$(validate_arrow_prefix(annotations_prefix)).onda.annotations.arrow")
write_annotations(annotations_path, annotations)
else
@warn "No annotations found in $path"
@warn "No annotations found in $onda_dir"
annotations_path = nothing
end
else
annotations = Annotation[]
annotations = Onda.Annotation[]
end
return uuid => (signals, annotations)

return @compat (; recording_uuid, signals, annotations, signals_path, annotations_path)
end

function store_edf_as_onda(path, edf::EDF.File, uuid::UUID=uuid4(); kwargs...)
Base.depwarn("`store_edf_as_onda(path, edf, ...)` is deprecated, use " *
"`nt = store_edf_as_onda(edf, path, ...); nt.recording_uuid => (nt.signals, nt.annotations)` " *
"instead.",
:store_edf_as_onda)
nt = store_edf_as_onda(edf, path, uuid; kwargs...)
signals = [rowmerge(s; file_path=string(s.file_path)) for s in nt.signals]
return nt.recording_uuid => (nt.signals, nt.annotations)
end

function validate_arrow_prefix(prefix)
prefix == basename(prefix) || throw(ArgumentError("prefix \"$prefix\" is invalid: cannot contain directory separator"))
pm = match(r"(.*).onda.(signals|annotations).arrow", prefix)
pm = match(r"(.*)\.onda\.(signals|annotations)\.arrow", prefix)
if pm !== nothing
@warn "Extracting prefix \"$(pm.captures[1])\" from provided prefix \"$prefix\""
prefix = pm.captures[1]
Expand Down
22 changes: 11 additions & 11 deletions test/export.jl
Original file line number Diff line number Diff line change
Expand Up @@ -86,29 +86,29 @@

@testset "full service" begin
# import annotations
recordings, round_tripped = store_edf_as_onda(mktempdir(), exported_edf, uuid).second
@test round_tripped isa Vector{<:Onda.Annotation}
nt = store_edf_as_onda(exported_edf, mktempdir(), uuid)
@test nt.annotations isa Vector{<:Onda.Annotation}
# annotations are sorted by start time on export
ann_sorted = sort(annotations; by=row -> Onda.start(row.span))
@test getproperty.(round_tripped, :span) == getproperty.(ann_sorted, :span)
@test getproperty.(round_tripped, :value) == getproperty.(ann_sorted, :value)
@test getproperty.(nt.annotations, :span) == getproperty.(ann_sorted, :span)
@test getproperty.(nt.annotations, :value) == getproperty.(ann_sorted, :value)
# same recording UUID passed as original:
@test getproperty.(round_tripped, :recording) == getproperty.(ann_sorted, :recording)
@test getproperty.(nt.annotations, :recording) == getproperty.(ann_sorted, :recording)
# new UUID for each annotation created during import
@test all(getproperty.(round_tripped, :id) .!= getproperty.(ann_sorted, :id))
@test all(getproperty.(nt.annotations, :id) .!= getproperty.(ann_sorted, :id))
info_orig = first(onda_samples).info
info_round_tripped = SamplesInfo(first(recordings))
info_round_tripped = SamplesInfo(first(nt.signals))

@test all(getproperty(info_orig, p) == getproperty(info_round_tripped, p) for p in propertynames(info_orig))

# don't import annotations
recordings, round_tripped = store_edf_as_onda(mktempdir(), exported_edf, uuid; import_annotations=false).second
@test round_tripped isa Vector{<:Onda.Annotation}
@test length(round_tripped) == 0
nt = store_edf_as_onda(exported_edf, mktempdir(), uuid; import_annotations=false)
@test nt.annotations isa Vector{<:Onda.Annotation}
@test length(nt.annotations) == 0

# import empty annotations
exported_edf2 = onda_to_edf(samples_to_export)
@test_logs (:warn, r"No annotations found in") store_edf_as_onda(mktempdir(), exported_edf2, uuid; import_annotations=true)
@test_logs (:warn, r"No annotations found in") store_edf_as_onda(exported_edf2, mktempdir(), uuid; import_annotations=true)
end

end
73 changes: 45 additions & 28 deletions test/import.jl
Original file line number Diff line number Diff line change
Expand Up @@ -314,14 +314,16 @@ end
@testset "store_edf_as_onda" begin
root = mktempdir()
uuid = uuid4()
returned_uuid, (returned_signals, annotations) = OndaEDF.store_edf_as_onda(root, edf, uuid)
signals = Dict(s.kind => s for s in returned_signals)
nt = OndaEDF.store_edf_as_onda(edf, root, uuid)
signals = Dict(s.kind => s for s in nt.signals)

@test isfile(joinpath(root, "edf.onda.signals.arrow"))
@test isfile(joinpath(root, "edf.onda.annotations.arrow"))
@test nt.signals_path == joinpath(root, "edf.onda.signals.arrow")
@test nt.annotations_path == joinpath(root, "edf.onda.annotations.arrow")
@test isfile(nt.signals_path)
@test isfile(nt.annotations_path)

@test returned_uuid == uuid
@test length(returned_signals) == 13
@test nt.recording_uuid == uuid
@test length(nt.signals) == 13
@testset "samples info" begin
@test signals["tidal_volume"].channels == ["tidal_volume"]
@test signals["tidal_volume"].sample_unit == "milliliter"
Expand Down Expand Up @@ -365,17 +367,17 @@ end
end

@testset "Annotations import" begin
@test length(annotations) == n_records * 4
@test length(nt.annotations) == n_records * 4
# check whether all four types of annotations are preserved on import:
for i in 1:n_records
start = Nanosecond(Second(i))
stop = start + Nanosecond(Second(i + 1))
# two annotations with same 1s span and different values:
@test any(a -> a.value == "$i a" && a.span.start == start && a.span.stop == stop, annotations)
@test any(a -> a.value == "$i b" && a.span.start == start && a.span.stop == stop, annotations)
@test any(a -> a.value == "$i a" && a.span.start == start && a.span.stop == stop, nt.annotations)
@test any(a -> a.value == "$i b" && a.span.start == start && a.span.stop == stop, nt.annotations)
# two annotations with instantaneous (1ns) span and different values
@test any(a -> a.value == "$i c" && a.span.start == start && a.span.stop == start + Nanosecond(1), annotations)
@test any(a -> a.value == "$i d" && a.span.start == start && a.span.stop == start + Nanosecond(1), annotations)
@test any(a -> a.value == "$i c" && a.span.start == start && a.span.stop == start + Nanosecond(1), nt.annotations)
@test any(a -> a.value == "$i d" && a.span.start == start && a.span.stop == start + Nanosecond(1), nt.annotations)
end
end

Expand All @@ -392,39 +394,54 @@ end
@test prefix == "edf"

mktempdir() do root
OndaEDF.store_edf_as_onda(root, edf, uuid; signals_prefix="edfff")
@test isfile(joinpath(root, "edfff.onda.signals.arrow"))
@test isfile(joinpath(root, "edfff.onda.annotations.arrow"))
nt = OndaEDF.store_edf_as_onda(edf, root, uuid; signals_prefix="edfff")
@test nt.signals_path == joinpath(root, "edfff.onda.signals.arrow")
@test nt.annotations_path == joinpath(root, "edfff.onda.annotations.arrow")
end

mktempdir() do root
OndaEDF.store_edf_as_onda(root, edf, uuid; annotations_prefix="edff")
@test isfile(joinpath(root, "edf.onda.signals.arrow"))
@test isfile(joinpath(root, "edff.onda.annotations.arrow"))
nt = OndaEDF.store_edf_as_onda(edf, root, uuid; annotations_prefix="edff")
@test nt.signals_path == joinpath(root, "edf.onda.signals.arrow")
@test nt.annotations_path == joinpath(root, "edff.onda.annotations.arrow")
end

mktempdir() do root
OndaEDF.store_edf_as_onda(root, edf, uuid; annotations_prefix="edff")
@test isfile(joinpath(root, "edf.onda.signals.arrow"))
@test isfile(joinpath(root, "edff.onda.annotations.arrow"))
nt = OndaEDF.store_edf_as_onda(edf, root, uuid; annotations_prefix="edff")
@test nt.signals_path == joinpath(root, "edf.onda.signals.arrow")
@test nt.annotations_path == joinpath(root, "edff.onda.annotations.arrow")
end

mktempdir() do root
OndaEDF.store_edf_as_onda(root, edf, uuid; signals_prefix="edfff", annotations_prefix="edff")
@test isfile(joinpath(root, "edfff.onda.signals.arrow"))
@test isfile(joinpath(root, "edff.onda.annotations.arrow"))
nt = OndaEDF.store_edf_as_onda(edf, root, uuid; signals_prefix="edfff", annotations_prefix="edff")
@test nt.signals_path == joinpath(root, "edfff.onda.signals.arrow")
@test nt.annotations_path == joinpath(root, "edff.onda.annotations.arrow")
end

mktempdir() do root
@test_logs (:warn, r"Extracting prefix") OndaEDF.store_edf_as_onda(root, edf, uuid; signals_prefix="edff.onda.signals.arrow", annotations_prefix="edf")
@test isfile(joinpath(root, "edff.onda.signals.arrow"))
@test isfile(joinpath(root, "edf.onda.annotations.arrow"))
@test_logs (:warn, r"Extracting prefix") begin
nt = OndaEDF.store_edf_as_onda(edf, root, uuid; signals_prefix="edff.onda.signals.arrow", annotations_prefix="edf")
end
@test nt.signals_path == joinpath(root, "edff.onda.signals.arrow")
@test nt.annotations_path == joinpath(root, "edf.onda.annotations.arrow")
end

mktempdir() do root
@test_throws ArgumentError OndaEDF.store_edf_as_onda(root, edf, uuid; signals_prefix="stuff/edf", annotations_prefix="edf")
@test_throws ArgumentError OndaEDF.store_edf_as_onda(edf, root, uuid; signals_prefix="stuff/edf", annotations_prefix="edf")
end
end

@testset "AbstractPath support" begin
mktempdir() do dir
root = PosixPath(dir)
nt = OndaEDF.store_edf_as_onda(edf, root, uuid)

@test nt.signals_path isa AbstractPath
@test isfile(nt.signals_path)
@test nt.annotations_path isa AbstractPath
@test isfile(nt.annotations_path)
@test isdir(joinpath(dirname(nt.signals_path), "samples"))
@test all(p -> p isa AbstractPath, (s.file_path for s in nt.signals))
end

end
end

Expand Down
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Test, Dates, Random, UUIDs, Statistics
using OndaEDF, Onda, EDF, Tables
using FilePathsBase: AbstractPath, PosixPath

function test_edf_signal(rng, label, transducer, physical_units,
physical_min, physical_max,
Expand Down

2 comments on commit a1d077b

@omus
Copy link
Member Author

@omus omus commented on a1d077b Nov 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/48815

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.9.1 -m "<description of version>" a1d077b1ff9b20e9256bd6045ddf022a875f5303
git push origin v0.9.1

Please sign in to comment.