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

Add events support #22

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ jobs:
- name: CompatHelper.main()
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: julia -e 'using CompatHelper; CompatHelper.main()'
run: julia -e 'using CompatHelper; CompatHelper.main()'
2 changes: 1 addition & 1 deletion .github/workflows/TagBot.yml → .github/TagBot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ jobs:
steps:
- uses: JuliaRegistries/TagBot@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
.DS_Store
*Manifest.toml
/dev/
*.code-workspace
12 changes: 6 additions & 6 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[compat]
julia = "1.2"
CSV = "0.5.18"
DataFrames = "0.19,0.20"
DataStructures = "0.17.5"
CSV = "0.5.18, 0.10"
DataFrames = "0.19, 0.20, 1"
DataStructures = "0.17.5, 0.18, 1"
JSON = "0.21"
PrettyTables = "0.6,0.7,0.8"
PrettyTables = "0.6, 0.7, 0.8, 2"
Test = "1.11.0"
julia = "1.2"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ For more info on BIDS, read [the documentation](https://bids-specification.readt

## Features

* Working with BIDS Directory easily
* Working with BIDS Directory easily
* Flexible usage - initialize single object as you wish
* Query to get the desired files
* Other utility functions such as `total_sessions`, `parse_fname`, `parse_path`, etc.
Expand Down
7 changes: 5 additions & 2 deletions src/BIDSTools.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ export
Subject,
Session,
File

# Utility functions
export
total_subjects,
total_sessions,
total_files,
get_metadata_path,
get_events_path,
parse_path,
parse_fname,
print_dataset_description,
Expand All @@ -31,6 +31,9 @@ export
get_files,
get_sub,
get_ses,
construct_fname
construct_fname,
subjects,
sessions,
files

end # module
83 changes: 55 additions & 28 deletions src/File.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* `path` - path to the file
* `metadata` - dictionary parsed from JSON-sidecar
* `entities` - dictionary parsed from key-value filename
* `events` - DataFrame parsed from an `_events.tsv` file

The `File` can be initialized by specifying only `path`. Other behavior can also be
tweaked accordingly with the optional parameters:
Expand Down Expand Up @@ -31,45 +32,53 @@ File:
struct File
path::String
# This is the metadata from JSON-sidecar (doesn't exists in BIDS-like directory)
metadata::OrderedDict{String, Any}
metadata::OrderedDict{String,Any}
# Entities are the key-value from filename
entities::OrderedDict{String, String}
entities::OrderedDict{String,String}
events::DataFrame
end

function File(
path::AbstractString;
load_metadata::Bool=true,
require_modality::Bool=true,
strict::Bool=true,
extract_from_full_path::Bool=true
extract_from_full_path::Bool=true,
load_events::Bool=true
)
entities = parse_path(path, require_modality=require_modality, strict=strict)
# Try extracting sub and ses from full path if not exists in parsed filename
if extract_from_full_path
if !haskey(entities, "sub")
entities["sub"] = get_sub(
path,
from_fname=false,
require_modality=require_modality,
strict=strict
)
path,
from_fname=false,
require_modality=require_modality,
strict=strict
)
end
if !haskey(entities, "ses")
entities["ses"] = get_ses(
path,
from_fname=false,
require_modality=require_modality,
strict=strict
)
path,
from_fname=false,
require_modality=require_modality,
strict=strict
)
end
end
metadata = !load_metadata ? OrderedDict{String,Any}() :
JSON.parsefile(
get_metadata_path(path), dicttype=OrderedDict{String,Any}
)
File(path, metadata, entities)
get_metadata_path(path), dicttype=OrderedDict{String,Any}
)
events = !load_events ? DataFrame() : CSV.read(
get_events_path(path),
Copy link
Contributor

Choose a reason for hiding this comment

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

If I understand correctly, get_events_path(path) could return nothing if for some reason the corresponding _events.tsv doesn't exist.

If that's the case, the CSV.read(nothing) will raise MethodError: no method matching joinpath(::Nothing), which could be unhelpful for user.

I suggest raising a more useful error in get_events_path(path) that indicates joinpath(this_dirname, fname_without_ext * "_events.tsv") couldn't be found.

DataFrame;
delim='\t'
)
File(path, metadata, entities, events)
end


#-------------------------------------------------------------------------------

function Base.show(io::IO, file::File)
Expand All @@ -94,11 +103,29 @@ function get_metadata_path(path::AbstractString)
fname_without_ext = split(basename(path), ".")[1]
this_dirname = dirname(path)

metadata_path = isfile(joinpath(this_dirname, fname_without_ext*".json")) ?
joinpath(this_dirname, fname_without_ext*".json") : nothing
metadata_path = isfile(joinpath(this_dirname, fname_without_ext * ".json")) ?
joinpath(this_dirname, fname_without_ext * ".json") : nothing
return metadata_path
end

"""
function get_events_path(path)

Get file path of events file for a BIDS `path` which can be a string or `File`.
"""
function get_events_path(file::File)
get_events_path(file.path)
end
function get_events_path(path::AbstractString)
fname_without_ext = split(basename(path), ".")[1]
fname_without_ext = join(split(fname_without_ext, "_")[1:end-1], "_")
this_dirname = dirname(path)

events_path = isfile(joinpath(this_dirname, fname_without_ext * "_events.tsv")) ?
joinpath(this_dirname, fname_without_ext * "_events.tsv") : nothing
return events_path
end

"""
function parse_path(path; require_modality::Bool=true, strict::Bool=true)

Expand Down Expand Up @@ -162,25 +189,25 @@ function parse_fname(
error(msg)
else
@warn msg
return OrderedDict{String, String}()
return OrderedDict{String,String}()
end
end
k,v = part
k, v = part
if haskey(d, k)
msg = "Invalid BIDS file name (key $k occurs twice)"
if strict
error(msg)
else
@warn msg
return OrderedDict{String, String}()
return OrderedDict{String,String}()
end
elseif isempty(k)
msg = "Empty key in pair $k-$v"
if strict
error(msg)
else
@warn msg
return OrderedDict{String, String}()
return OrderedDict{String,String}()
end
end
d[k] = v
Expand All @@ -195,7 +222,7 @@ Private function to check whether keywords argument in metadata or in entities.
false if the key could not be found anywhere or the value of given key is not the same.
"""
function check_entities_meta(file::File; kws...)
for (k,v) in kws
for (k, v) in kws
keystr = string(k)
meta_val = get(file.entities, keystr) do
get(file.metadata, keystr, nothing)
Expand Down Expand Up @@ -230,10 +257,10 @@ filtered_files = get_files(layout, run="002", modality="T1w")
```
"""
function get_files(
files::Vector{File}; path::Union{String, Regex, Nothing}=nothing, kws...
files::Vector{File}; path::Union{String,Regex,Nothing}=nothing, kws...
)
if !isnothing(path)
filter!(x->occursin(path, x.path), files)
filter!(x -> occursin(path, x.path), files)
end
result = filter(files) do f
check_entities_meta(f; kws...)
Expand Down Expand Up @@ -271,7 +298,7 @@ function get_sub(
require_modality::Bool=true,
strict::Bool=true
)
sub_rgx = r"[\\/]sub-(.+?)[\\/]"
sub_rgx = r"[\\/]sub-(.+?)[\\/]"
sub_match = get(
parse_fname(basename(path), require_modality=require_modality, strict=strict),
"sub",
Expand Down Expand Up @@ -314,7 +341,7 @@ function get_ses(
require_modality::Bool=true,
strict::Bool=true
)
ses_rgx = r"[\\/]ses-(.+?)[\\/]"
ses_rgx = r"[\\/]ses-(.+?)[\\/]"
ses_match = get(
parse_fname(basename(path), require_modality=require_modality, strict=strict),
"ses",
Expand All @@ -336,7 +363,7 @@ e.g. `_T1w`, use `modality` key, i.e. "modality"=>"T1w".
"""
function construct_fname(entities::AbstractDict; ext::Union{String,Nothing}=nothing)
result_fname = ""
for (k,v) in entities
for (k, v) in entities
k == "modality" && continue
isnothing(v) && continue
!occursin(r"[-_]", k) ||
Expand Down
39 changes: 22 additions & 17 deletions src/Layout.jl
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ struct Layout
subjects::Vector{Subject}
# With this turned off, not going to look for ses- in path
longitudinal::Bool
description::OrderedDict{String, Any}
description::OrderedDict{String,Any}
subjects_detail::DataFrame
end

subjects(layout::Layout) = layout.subjects

function Layout(
root::AbstractString;
search::Bool=true,
Expand Down Expand Up @@ -79,15 +81,15 @@ function Layout(
end
end
description = !isfile(joinpath(root, "dataset_description.json")) ?
OrderedDict{String,Any}() :
JSON.parsefile(
joinpath(root, "dataset_description.json"),
dicttype=OrderedDict{String,Any}
)
OrderedDict{String,Any}() :
JSON.parsefile(
joinpath(root, "dataset_description.json"),
dicttype=OrderedDict{String,Any}
)
subjects_detail = !isfile(joinpath(root, "subjects.tsv")) ?
DataFrame() :
CSV.File(joinpath(root, "subjects.tsv"), delim="\t")|>
DataFrame!
DataFrame() :
CSV.File(joinpath(root, "subjects.tsv"), delim="\t") |>
DataFrame!
Layout(root, subjects, longitudinal, description, subjects_detail)
end

Expand Down Expand Up @@ -118,12 +120,15 @@ end


function Base.show(io::IO, layout::Layout)
print(io, """
Layout:
root = $(layout.root)
total subject = $(total_subjects(layout))
total session = $(total_sessions(layout))
total files = $(total_files(layout))""")
print(
io,
"""
Layout:
root = $(layout.root)
total subject = $(total_subjects(layout))
total session = $(total_sessions(layout))
total files = $(total_files(layout))"""
)
end

"""
Expand All @@ -141,12 +146,12 @@ end
Function to pretty print subject spreadsheet (subjects.tsv) using PrettyTables
"""
function list_subject_detail(layout::Layout)
pretty_table(layout.subjects_detail, crop= :none)
pretty_table(layout.subjects_detail, crop=:none)
end

# Docstring of get_files is in File.jl
function get_files(
layout::Layout; path::Union{String, Regex, Nothing}=nothing, kws...
layout::Layout; path::Union{String,Regex,Nothing}=nothing, kws...
)
result = Vector{File}()
for sub in layout.subjects
Expand Down
20 changes: 12 additions & 8 deletions src/Session.jl
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function Session(
for d in readdir(path)
if isnothing(scans_detail)
scans_detail = isfile(joinpath(path, d)) && endswith(d, "_scans.tsv") ?
CSV.File(joinpath(path, d)) |> DataFrame! : nothing
CSV.File(joinpath(path, d)) |> DataFrame! : nothing
end
end
if search
Expand All @@ -76,7 +76,7 @@ function Session(
!isdir(joinpath(path, d)) && continue
for f in readdir(joinpath(path, d))
# Ignore json-sidecar since it'll go to the metadata
if isfile(joinpath(path, d, f)) && !endswith(f, ".json")
if isfile(joinpath(path, d, f)) && !endswith(f, ".json") && !endswith(f, "_events.tsv")
push!(
files,
File(
Expand All @@ -97,6 +97,7 @@ function Session(
Session(path, identifier, files, scans_detail)
end

files(session::Session) = session.files
#-------------------------------------------------------------------------------

"""
Expand All @@ -110,10 +111,13 @@ function total_files(session::Session)
end

function Base.show(io::IO, session::Session)
print(io, """
Session:
identifier = $(session.identifier)
total files = $(total_files(session))""")
print(
io,
"""
Session:
identifier = $(session.identifier)
total files = $(total_files(session))"""
)
end

"""
Expand All @@ -122,12 +126,12 @@ end
Function to pretty print scans detail spreadsheet (_scans.tsv) using PrettyTables
"""
function list_scans_detail(session::Session)
pretty_table(session.scans_detail, crop= :none)
pretty_table(session.scans_detail, crop=:none)
end

# Docstring of get_files is in File.jl
function get_files(
session::Session; path::Union{String, Regex, Nothing}=nothing, kws...
session::Session; path::Union{String,Regex,Nothing}=nothing, kws...
)
result = get_files(session.files; path=path, kws...)
return result
Expand Down
Loading