Skip to content

Commit 5564740

Browse files
authored
Merge pull request #106 from drbergman/development
v0.0.18
2 parents 738ba51 + b84c92a commit 5564740

21 files changed

+760
-291
lines changed

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ jobs:
148148

149149
- uses: julia-actions/julia-docdeploy@v1
150150
env:
151-
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
151+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
152152
DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }}
153153

154154
- name: Run doctests

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "pcvct"
22
uuid = "3c374bc7-7384-4f83-8ca0-87b8c727e6ff"
33
authors = ["Daniel Bergman <[email protected]> and contributors"]
4-
version = "0.0.17"
4+
version = "0.0.18"
55

66
[deps]
77
AutoHashEquals = "15f4f7f2-30c1-5605-9d31-71845cf9641f"

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ makedocs(;
2222
"Known limitations" => "man/known_limitations.md",
2323
"PhysiCell Studio" => "man/physicell_studio.md",
2424
"Sensitivity analysis" => "man/sensitivity_analysis.md",
25+
"Analyzing output" => "man/analyzing_output.md",
2526
"Developer guide" => "man/developer_guide.md",
2627
],
2728
"Documentation" => map(

docs/src/lib/VCTAnalysis.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ CollapsedDocStrings = true
55
# VCTAnalysis
66

77
Analyze output from a pcvct project.
8+
It is anticipated that this will eventually be split off into its own module or even package.
9+
Possibly with VCTLoader.jl.
810

911
```@autodocs
1012
Modules = [pcvct]

docs/src/lib/VCTLoader.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CollapsedDocStrings = true
55
# VCTLoader
66

77
Load PhysiCell data into useful forms for downstream analysis.
8+
This may be split off into its own module or even package eventually, likely with VCTAnalysis.jl.
89

910
```@autodocs
1011
Modules = [pcvct]

docs/src/man/analyzing_output.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Analyzing output
2+
3+
## Loading output
4+
5+
### `PhysiCellSnapshot`
6+
The base unit of PhysiCell output is the [`PhysiCellSnapshot`](@ref).
7+
These are currently considered pcvct internals and so the API may change.
8+
Each snapshot records the path to the PhysiCell output folder, its index in the sequence of outputs, the time of the snapshot in the simulation, and optionally the cell, substrate, and mesh data at that snapshot.
9+
10+
### `PhysiCellSequence`
11+
A [`PhysiCellSequence`](@ref) is the full sequence of snapshots corresponding to a single PhysiCell simulation.
12+
As with `PhysiCellSnapshot`'s, these are currently considered internals and their API may change.
13+
In addition to the path to the PhysiCell output folder and the vector of `PhysiCellSnapshot`'s, it holds metadata for the simulation.
14+
15+
### `getCellDataSequence`
16+
The main function to get sequences of cell data is [`getCellDataSequence`](@ref).
17+
It accepts any of a simulation ID (`<:Integer`), a simulation (`::Simulation`), or a sequence (`::PhysiCellSequence`) and either a single label (`::String`) or a vector of labels (`::Vector{String}`).
18+
For each cell in the simulation (as determined by the cell ID), the output creates a dictionary entry (the key is the integer cell ID) whose value is a named tuple with the input labels as keys as well as `:time`.
19+
This means that if one sets
20+
21+
```julia
22+
data = getCellDataSequence(1, "position")
23+
```
24+
Then one can access the positions of the cell with ID 78 by
25+
```julia
26+
cell_78_positions = data[78].position # an Nx3 matrix for the N integer-indexed outputs (ignores the `initial_*` and `final_*` files)
27+
```
28+
and plot the x-coordinates of this cell over time using
29+
```julia
30+
cell_78_times = data[78].time
31+
32+
using Plots
33+
plot(cell_78_times, cell_78_positions[:,1])
34+
```
35+
36+
**Note**: Each call to `getCellDataSequence` will load *all* the data unless a `PhysiCellSequence` is passed in.
37+
Plan your analyses accordingly as loading simulation data is not fast.
38+
39+
## Population plots
40+
41+
### Group by Monad
42+
Plotting population plots is one the most basic analysis tasks and pcvct makes it super easy!
43+
If you call `plot` on a `Simulation`, `Monad`, `Sampling`, or the return value of a call to `run` (though not for a sensitivity analysis),
44+
then a sequence of panels will be generated in a single figure.
45+
Each panel will correspond to a `Monad` (replicates using the same parameter values) and will plot mean +/- SD for each cell type.
46+
47+
Finer-grained control of the output is possible, too!
48+
- to include dead cells in your counts: `plot(...; ..., include_dead=true, ...)`
49+
- select a subset of cell types to include: `plot(...; ..., include_cell_types="cancer", ...)`
50+
- select a subset of cell types to exclude: `plot(...; ..., exclude_cell_types="cancer", ...)`
51+
52+
The `include_cell_types` and `exclude_cell_types` can also accept a `Vector{String}` to include or exclude certain cell types, respectively.
53+
Furthermore, if the value of `include_cell_types` is a `Vector` and one of its entries is a `Vector{String}`, pcvct will interpret this to sum up those cell types.
54+
In other words, to get the total tumor cell count in addition to the epithelial (`"epi"`) and mesenchymal (`"mes"`) components, you could use
55+
```julia
56+
using Plots
57+
plot(Monad(1); include_cell_types=["epi", "mes", ["epi", "mes"]])
58+
```
59+
60+
Finally, this makes use of Julia's Plot Recipes (see [RecipesBase.jl](https://docs.juliaplots.org/stable/RecipesBase/)) so any standard plotting keywords can be passed in:
61+
```julia
62+
using Plots
63+
colors = [:blue :red] # Note the absence of a `,` or `;`. This is how Julia requires different series parameters to be passed in
64+
plot(Simulation(1); color=colors, include_cell_types=["cd8", "cancer"]) # will plot cd8s in blue and cancer in red.
65+
```
66+
67+
### Group by cell type
68+
Invert the above by including all data for a single cell type across all monads in a single panel with a call to `plotbycelltype`.
69+
This function works on any `T<:AbstractTrial` (`Simulation`, `Monad`, `Sampling`, or `Trial`) as well as any `PCVCTOutput` object (the return value to `run`).
70+
Everything above for `plot` applies here.
71+
72+
```julia
73+
using Plots
74+
plotbycelltype(Sampling(1); include_cell_types=["epi", "mes", ["epi", "mes"]], color=[:blue :red :purple], labels=["epi" "mes" "both"], legend=true)
75+
```
76+
77+
## Substrate analysis
78+
pcvct supports two ways to summarize substrate information over time.
79+
80+
### AverageSubstrateTimeSeries
81+
An [`AverageSubstrateTimeSeries`](@ref) gives the time series for the average substrate across the entire domain.
82+
83+
```julia
84+
simulation_id = 1
85+
asts = AverageSubstrateTimeSeries(simulation_id)
86+
using Plots
87+
plot(asts.time, asts["oxygen"])
88+
```
89+
90+
### `ExtracellularSubstrateTimeSeries`
91+
An [`ExtracellularSubstrateTimeSeries`](@ref) gives the time series for the average substrate concentration in the extracellular space neighboring all cells of a given cell type.
92+
In a simulation with `cd8` cells and `IFNg` diffusible substrate, plot the average concentration of IFNg experienced by CD8+ T cells using the following:
93+
94+
```julia
95+
simulation_id = 1
96+
ests = ExtracellularSubstrateTimeSeries(simulation_id)
97+
using Plots
98+
plot(ests.time, ests["cd8"]["IFNg"])
99+
```
100+
101+
## Motility analysis
102+
The `motilityStatistics` function returns the time alive, distance traveled, and mean speed for each cell in the simulation.
103+
For each cell, these values are split amongst the cell types the given cell assumed throughout (or at least at the save times).
104+
To calculate these values, the cell type at the start of the save interval is used and the net displacement is used to calculate the speed.
105+
Optionally, users can pass in a coordinate direction to only consider speed in a given axis.
106+
107+
```julia
108+
simulation_id = 1
109+
mss = motilityStatistics(simulation_id)
110+
all_mean_speeds_as_mes = [ms["mes"].speed for ms in mss if haskey(ms, "mes")] # concatenate all speeds as a "mes" cell type (if the given cell ever was a "mes")
111+
all_times_as_mes = [ms["mes"].time for ms in mss if haskey(ms, "mes")] # similarly, get the time spent in the "mes" state
112+
mean_mes_speed = all_mean_speeds_as_mes .* all_times_as_mes |> sum # start computing the weighted average of their speeds
113+
mean_mes_speed /= sum(all_times_as_mes) # finish computing weighted average
114+
```
115+
116+
```julia
117+
mss = motilityStatistics(simulation_id; direction=:x) # only consider the movement in the x direction
118+
```

src/VCTAnalysis/motility.jl

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,4 @@
1-
export getCellPositionSequence, computeMeanSpeed
2-
3-
"""
4-
getCellPositionSequence(sequence::PhysiCellSequence; include_dead::Bool=false, include_cell_type::Bool=false)
5-
6-
Return a dictionary where the keys are cell IDs from the PhysiCell simulation and the values are NamedTuples containing the time and the position of the cell.
7-
8-
This is a convenience function for `getCellDataSequence(sequence, "position"; include_dead=include_dead, include_cell_type=include_cell_type)`.
9-
"""
10-
function getCellPositionSequence(sequence::PhysiCellSequence; include_dead::Bool=false, include_cell_type::Bool=false)
11-
return getCellDataSequence(sequence, "position"; include_dead=include_dead, include_cell_type=include_cell_type)
12-
end
13-
14-
function meanSpeed(p; direction=:any)::NTuple{3,Dict{String,Float64}}
1+
function _motilityStatistics(p; direction=:any)::Dict{String, NamedTuple}
152
x, y, z = [col for col in eachcol(p.position)]
163
cell_type_name = p.cell_type_name
174
dx = x[2:end] .- x[1:end-1]
@@ -42,35 +29,46 @@ function meanSpeed(p; direction=:any)::NTuple{3,Dict{String,Float64}}
4229
start_ind += I #! advance the start to the first instance of a new cell_type_name
4330
end
4431
speed_dict = [k => distance_dict[k] / time_dict[k] for k in cell_type_names] |> Dict{String,Float64} #! convert to speed
45-
return speed_dict, distance_dict, time_dict
32+
per_type_stats = Dict{String, NamedTuple}()
33+
for cell_type_name in cell_type_names
34+
per_type_stats[cell_type_name] = [:time=>time_dict[cell_type_name], :distance=>distance_dict[cell_type_name], :speed=>speed_dict[cell_type_name]] |> NamedTuple
35+
end
36+
return per_type_stats
4637
end
4738

48-
function computeMeanSpeed(folder::String; direction=:any)::NTuple{3,Vector{Dict{String,Float64}}}
49-
sequence = PhysiCellSequence(folder; include_cells=true)
50-
pos = getCellPositionSequence(sequence; include_dead=false, include_cell_type=true)
51-
dicts = [meanSpeed(p; direction=direction) for p in values(pos) if length(p.time) > 1]
52-
return [dict[1] for dict in dicts], [dict[2] for dict in dicts], [dict[3] for dict in dicts]
39+
function motilityStatistics(path_to_folder::String; direction=:any)::Vector{Dict{String, NamedTuple}}
40+
sequence = PhysiCellSequence(path_to_folder; include_cells=true)
41+
pos = getCellDataSequence(sequence, "position"; include_dead=false, include_cell_type=true)
42+
return [_motilityStatistics(p; direction=direction) for p in values(pos) if length(p.time) > 1]
5343
end
5444

5545
"""
56-
computeMeanSpeed(simulation_id::Integer[; direction=:any])
46+
motilityStatistics(simulation_id::Integer[; direction=:any])
5747
58-
Return dictionaries containing the mean speed, total distance traveled, and total time spent for each cell type in the PhysiCell simulation.
48+
Return the mean speed, distance traveled, and time alive for each cell in the simulation, broken down by cell type in the case of cell type transitions.
5949
6050
The time is counted from when the cell first appears in simulation output until it dies or the simulation ends, whichever comes first.
61-
62-
To account for cells that may change cell type during the simulation, the dictionaries returned are keyed by cell type.
63-
So, a dictionary with key "A" and value 2.0 indicates that the mean speed of this cell while it was of type "A" is 2.0.
51+
If the cell transitions to a new cell type during the simulation, the time is counted for each cell type separately.
52+
Each cell type taken on by a given cell will be a key in the dictionary returned at that entry.
6453
6554
# Arguments
6655
- `simulation_id::Integer`: The ID of the PhysiCell simulation.
6756
- `direction::Symbol`: The direction to compute the mean speed. Can be `:x`, `:y`, `:z`, or `:any` (default). If `:x`, for example, the mean speed is calculated using only the x component of the cell's movement.
6857
6958
# Returns
70-
- `mean_speed_dicts::Vector{Dict{String,Float64}}`: A vector of dictionaries where each dictionary is specific to a single cell. The key is the cell type and the value is the mean speed of that cell.
71-
- `distance_dicts::Vector{Dict{String,Float64}}`: A vector of dictionaries where each dictionary is specific to a single cell. The key is the cell type and the value is the total distance traveled by that cell.
72-
- `time_dicts::Vector{Dict{String,Float64}}`: A vector of dictionaries where each dictionary is specific to a single cell. The key is the cell type and the value is the total time in the simulation for that cell.
59+
- `Vector{Dict{String, NamedTuple}}`: A vector of dictionaries, one per cell in the simulation. Each dictionary has keys for each cell type taken on by the cell. The values are NamedTuples with fields `:time`, `:distance`, and `:speed`.
60+
61+
# Example
62+
```julia
63+
ms = motilityStatistics(1) # a vector of dictionaries, one per cell in the simulation
64+
ms[1]["epithelial"] # NamedTuple with fields :time, :distance, :speed for the first cell in the simulation corresponding to its time as an `epithelial` cell
65+
ms[1]["mesenchymal"].time # time spent as a `mesenchymal` cell for the first cell in the simulation
66+
ms[1]["mesenchymal"].distance # distance traveled as a `mesenchymal` cell for the first cell in the simulation
67+
ms[1]["mesenchymal"].speed # mean speed as a `mesenchymal` cell for the first cell in the simulation
68+
```
7369
"""
74-
function computeMeanSpeed(simulation_id::Integer; direction=:any)::NTuple{3,Vector{Dict{String,Float64}}}
75-
return joinpath(outputFolder("simulation", simulation_id), "output") |> x -> computeMeanSpeed(x; direction=direction)
76-
end
70+
function motilityStatistics(simulation_id::Integer; direction=:any)::Vector{Dict{String, NamedTuple}}
71+
return joinpath(outputFolder("simulation", simulation_id), "output") |> x -> motilityStatistics(x; direction=direction)
72+
end
73+
74+
motilityStatistics(simulation::Simulation, direction=:any) = motilityStatistics(simulation.id, direction=direction)

0 commit comments

Comments
 (0)