Skip to content

Commit 0dbeb83

Browse files
drbergmanclaude
andauthored
Expose Makefile animation parameters in makeMovie (#218)
Add framerate, magick_density, magick_resize_x, and magick_resize_y keyword arguments so users can override the PhysiCell Makefile's FRAMERATE, MAGICK_DENSITY, MAGICK_RESIZE_X, and MAGICK_RESIZE_Y variables when generating a movie, instead of being stuck with whatever the Makefile hardcodes. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent a408be6 commit 0dbeb83

7 files changed

Lines changed: 102 additions & 4 deletions

File tree

PRD.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,29 @@
294294

295295
---
296296

297+
## Feature: Movie Generation
298+
299+
**One-line description:** Render a simulation's SVG snapshots into a movie via the PhysiCell Makefile's `jpeg`/`movie` targets.
300+
301+
**Priority:** Should-have
302+
303+
**Behavioral specification:**
304+
- `makeMovie(simulation_id)` invokes `make jpeg` then `make movie` in `physicellDir()`, deletes the intermediate JPEGs, and leaves `out.mp4` in the simulation's output folder.
305+
- `makeMovie(T::AbstractTrial)` / `makeMovie(out::PCMMOutput)` batch this over every simulation in the trial/output.
306+
- Keyword arguments `framerate`, `magick_density`, `magick_resize_x`, `magick_resize_y` forward directly to the Makefile's `FRAMERATE`, `MAGICK_DENSITY`, `MAGICK_RESIZE_X`, `MAGICK_RESIZE_Y` variables (`movie`/`jpeg` targets respectively). Each defaults to `missing`, in which case the Makefile's own default for that variable is used unchanged.
307+
- `magick_path`/`ffmpeg_path` locate the ImageMagick/FFmpeg executables; `verbose` prints the underlying `make` command output.
308+
309+
**Acceptance criteria:**
310+
- Omitting the new framerate/density/resize keywords reproduces the exact previous behavior (Makefile defaults).
311+
- Passing any of the four keywords changes the corresponding `make` invocation's variable assignment and is reflected in the produced movie.
312+
- Re-running `makeMovie` when `out.mp4` already exists is a no-op (`false` return), regardless of these keywords.
313+
314+
**Edge cases:**
315+
- No `s*.svg` files in the output folder → warn and skip (`false` return), independent of these keywords.
316+
- ImageMagick or FFmpeg not discoverable on `PATH` → throws `ErrorException`.
317+
318+
---
319+
297320
## Feature: Post-Processing Hook & Quantities of Interest
298321

299322
**One-line description:** Let users compute per-simulation quantities of interest (QoIs) from intact output via a `post_processor` callback, and guarantee that PCMM's destructive pruning runs only after that callback.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ julia> out = run(inputs, dv; n_replicates = 3) # 3 replicates per apoptosis rate
9090
- [x] Ready-made PhysiCell QoI builder (`populationCountQoI`) so a `post_processor` can be a one-liner — per-cell-type counts at the final snapshot or any indexed save
9191
- [x] Intracellular model support (custom data, rules)
9292
- [x] IC cell and IC ECM file management
93+
- [x] Movie generation via the PhysiCell Makefile (`makeMovie`) — configurable `framerate`, `magick_density`, `magick_resize_x`/`magick_resize_y` keyword arguments
9394

9495
### Remaining
9596

docs/src/man/analyzing_output.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,29 @@ agent_ids = DataFrame(ID=[a.id for a in connected_components_1]) # get the IDs f
236236
component_df = rightjoin(cells_df, agent_ids, on=:ID) # join on the agent IDs, keeping only the rows in the connected component
237237
```
238238

239+
## Movies
240+
241+
[`makeMovie`](@ref) uses the PhysiCell Makefile to turn a simulation's SVG snapshots into `output/out.mp4`, deleting the intermediate JPEGs afterward. It requires ImageMagick and FFmpeg to be discoverable — on `PATH`, via `PCMM_IMAGEMAGICK_PATH`/`PCMM_FFMPEG_PATH`, or passed directly as `magick_path`/`ffmpeg_path`.
242+
243+
```julia
244+
makeMovie(1) # simulation 1 -> output/out.mp4
245+
makeMovie(sampling) # every simulation in a trial
246+
makeMovie(out) # every simulation in a `run` result
247+
```
248+
249+
The Makefile's own animation variables are exposed as keyword arguments. Omit any of them to keep that Makefile's default:
250+
251+
| Keyword | Makefile variable | Typical default |
252+
|---|---|---|
253+
| `framerate` | `FRAMERATE` | 24 |
254+
| `magick_density` | `MAGICK_DENSITY` | 96 |
255+
| `magick_resize_x` | `MAGICK_RESIZE_X` | 1024 |
256+
| `magick_resize_y` | `MAGICK_RESIZE_Y` | 1024 |
257+
258+
```julia
259+
makeMovie(1; framerate=10, magick_density=48, magick_resize_x=512, magick_resize_y=512)
260+
```
261+
239262
## Post-processing during a run
240263

241264
Everything above analyzes output *after* a run finishes. `run` also accepts a `post_processor` keyword: a callback invoked once per successful simulation, right after it finishes and before PhysiCellModelManager.jl prunes any output — so the callback always sees the intact output folder, however aggressive your `prune_options` are.

docs/src/man/examples.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ using Plots
119119
plot(Simulation(1); include_cell_type_names=["cd8", "cancer"])
120120
```
121121

122+
## Make a movie from a simulation's snapshots
123+
124+
Use `makeMovie` to render a simulation's SVG snapshots into `out.mp4` via the PhysiCell Makefile. Override `framerate`, `magick_density`, `magick_resize_x`, or `magick_resize_y` to change frame rate or JPEG resolution/density; omit any to keep the Makefile's default. → [Analyzing output](@ref)
125+
126+
```julia
127+
makeMovie(1; framerate=10, magick_resize_x=512, magick_resize_y=512)
128+
```
129+
122130
## Extract per-cell time series
123131

124132
Use `cellDataSequence` to pull a labeled quantity for every cell across time. → [Analyzing output](@ref)

progress.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@
55
66
---
77

8+
## 2026-07-08 — Expose Makefile animation parameters (`framerate`, `magick_density`, `magick_resize_x/y`) in `makeMovie`
9+
10+
### Motivation
11+
`makeMovie` (`src/movie.jl`) only ever forwarded `OUTPUT=` to the PhysiCell Makefile's `jpeg`/`movie` targets, even though that Makefile also reads `FRAMERATE`, `MAGICK_DENSITY`, `MAGICK_RESIZE_X`, `MAGICK_RESIZE_Y` (defaults 24 fps / 96 dpi / 1024×1024). Users had no way to control frame rate or JPEG resolution/density from Julia.
12+
13+
### Design
14+
- Added four new keyword arguments to `makeMovie(simulation_id::Int; ...)`, each `Union{Missing,Int}=missing`, mirroring the existing `magick_path`/`ffmpeg_path` sentinel pattern already in this function rather than inventing a new convention.
15+
- Each is only appended as a `"VAR=value"` string to the relevant `Cmd` when not `missing` — an unset keyword falls through to whatever the target project's own Makefile defines for that variable, rather than PCMM silently overriding a user's project-level Makefile customization.
16+
- `framerate` targets the `movie` command; `magick_density`, `magick_resize_x`, `magick_resize_y` target the `jpeg` command (matches which Makefile target actually reads which variable).
17+
- `makeMovie(T::AbstractTrial; kwargs...)` / `makeMovie(T::PCMMOutput; kwargs...)` needed no changes — they already forward `kwargs...` untouched.
18+
19+
### Testing
20+
Extended `test/test-scripts/MovieTests.jl` with a case passing non-default values for all four new keywords and confirming `out.mp4` is still produced; existing no-kwargs path is unchanged and still covered.
21+
22+
### Docs
23+
- `docs/src/lib/movie.md` needed no edit — it's an `@autodocs` page over `movie.jl`, so the updated docstring flows through automatically.
24+
- Added a "Movies" section to `docs/src/man/analyzing_output.md` (before "Post-processing during a run") documenting `makeMovie` and a table mapping each new keyword to its Makefile variable and default.
25+
- Added a matching recipe to the `examples.md` cookbook, linking back to that new section, following the existing task → minimal code → link pattern.
26+
27+
---
28+
829
## 2026-07-08 — Task B: `populationCountQoI`, a ready-made `post_processor` builder
930

1031
### Motivation

src/movie.jl

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ There are three ways to allow this function to find these dependencies:
2626
- `magick_path::Union{Missing,String}`: The path to the ImageMagick executable. If not provided, uses `simulator().path_to_magick`.
2727
- `ffmpeg_path::Union{Missing,String}`: The path to the FFmpeg executable. If not provided, uses `simulator().path_to_ffmpeg`.
2828
- `verbose::Bool`: If `true`, prints the output of the commands used to generate the movie. Default is `false`.
29+
- `framerate::Union{Missing,Int}`: Frames per second, forwarded to the Makefile's `FRAMERATE` variable. If not provided, uses the Makefile's own default.
30+
- `magick_density::Union{Missing,Int}`: JPEG rendering density (dpi), forwarded to the Makefile's `MAGICK_DENSITY` variable. If not provided, uses the Makefile's own default.
31+
- `magick_resize_x::Union{Missing,Int}`: JPEG resize width, forwarded to the Makefile's `MAGICK_RESIZE_X` variable. If not provided, uses the Makefile's own default.
32+
- `magick_resize_y::Union{Missing,Int}`: JPEG resize height, forwarded to the Makefile's `MAGICK_RESIZE_Y` variable. If not provided, uses the Makefile's own default.
2933
3034
# Example
3135
```julia
@@ -38,8 +42,12 @@ makeMovie(sampling) # make movies for all simulations in the sampling
3842
out = run(sampling) # run the sampling
3943
makeMovie(out) # make movies for all simulations in the output
4044
```
45+
```julia
46+
makeMovie(123; framerate=10, magick_density=48, magick_resize_x=512, magick_resize_y=512)
47+
```
4148
"""
42-
function makeMovie(simulation_id::Int; magick_path::Union{Missing,String}=simulator().path_to_magick, ffmpeg_path::Union{Missing,String}=simulator().path_to_ffmpeg, verbose::Bool=false)
49+
function makeMovie(simulation_id::Int; magick_path::Union{Missing,String}=simulator().path_to_magick, ffmpeg_path::Union{Missing,String}=simulator().path_to_ffmpeg, verbose::Bool=false,
50+
framerate::Union{Missing,Int}=missing, magick_density::Union{Missing,Int}=missing, magick_resize_x::Union{Missing,Int}=missing, magick_resize_y::Union{Missing,Int}=missing)
4351
assertInitialized()
4452
path_to_output_folder = joinpath(trialFolder(Simulation, simulation_id), "output")
4553
if isfile("$(path_to_output_folder)/out.mp4")
@@ -67,9 +75,16 @@ function makeMovie(simulation_id::Int; magick_path::Union{Missing,String}=simula
6775
movie_generated = false
6876
return movie_generated
6977
end
70-
cmd = Cmd(`make jpeg OUTPUT=$(path_to_output_folder)`; env=env, dir=physicellDir())
78+
jpeg_args = ["make", "jpeg", "OUTPUT=$(path_to_output_folder)"]
79+
ismissing(magick_density) || push!(jpeg_args, "MAGICK_DENSITY=$(magick_density)")
80+
ismissing(magick_resize_x) || push!(jpeg_args, "MAGICK_RESIZE_X=$(magick_resize_x)")
81+
ismissing(magick_resize_y) || push!(jpeg_args, "MAGICK_RESIZE_Y=$(magick_resize_y)")
82+
cmd = Cmd(Cmd(jpeg_args); env=env, dir=physicellDir())
7183
verbose ? run(cmd) : quietRun(cmd)
72-
cmd = Cmd(`make movie OUTPUT=$(path_to_output_folder)`; env=env, dir=physicellDir())
84+
85+
movie_args = ["make", "movie", "OUTPUT=$(path_to_output_folder)"]
86+
ismissing(framerate) || push!(movie_args, "FRAMERATE=$(framerate)")
87+
cmd = Cmd(Cmd(movie_args); env=env, dir=physicellDir())
7388
verbose ? run(cmd) : quietRun(cmd)
7489
movie_generated = true
7590
jpgs = readdir(joinpath(trialFolder(Simulation, simulation_id), "output"), sort=false)

test/test-scripts/MovieTests.jl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@ if Sys.isapple()
99
@test makeMovie(1; verbose=true) === false
1010
@test makeMovie(run(Simulation(1)); verbose=true) |> isnothing #! makeMovie on the PCMMOutput object
1111

12-
#! Test that makeMovie returns false if no SVGs are found
1312
sim = Simulation(1)
1413
inputs = sim.inputs
1514
variation_id = sim.variation_id
15+
16+
#! Test that the framerate/magick_density/magick_resize_x/magick_resize_y kwargs still produce a movie
17+
custom_params_sim = Simulation(inputs, variation_id)
18+
run(custom_params_sim)
19+
@test makeMovie(custom_params_sim.id; framerate=10, magick_density=48, magick_resize_x=256, magick_resize_y=256)
20+
@test isfile(joinpath(PhysiCellModelManager.dataDir(), "outputs", "simulations", string(custom_params_sim.id), "output", "out.mp4"))
21+
22+
#! Test that makeMovie returns false if no SVGs are found
1623
new_sim = Simulation(inputs, variation_id)
1724
out = run(new_sim)
1825

0 commit comments

Comments
 (0)