Portfolio piece ā a synthetic recreation of a non-destructive
vibration-based firmness measurement workflow I built
during a fruit-quality project. The original recordings are not
shareable, so this repo simulates qualitatively similar two-channel
waveforms (stem end and calyx end), recovers the characteristic minimum
from each trace, extracts its time t_min, and looks at the
joint (CH1 t_min, CH2 t_min) distribution.
The pipeline reproduces the four steps I used in practice:
CH1 Ć CH2
scatter coloured by trial.source("R/simulate.R")
source("R/process.R")
traces <- readRDS("data/traces.rds")
n_fruit <- length(unique(traces$fruit))
n_trial <- length(unique(traces$trial))
sprintf("%d fruits across %d trials, %d samples per trace.",
n_fruit, n_trial, sum(traces$fruit == 1))
## [1] "104 fruits across 13 trials, 101 samples per trace."
Vertical bars mark the detected minimum on each channel (orange = CH1, green = CH2).
plot_trace(traces, fruit_id = 4)
plot_trace(traces, fruit_id = 60)
plot_trace(traces, fruit_id = 95)
A larger PNG gallery is rendered by R/batch_plots.R into
figures/traces/.
summary_df <- summarise_traces(traces)
head(summary_df)
## # A tibble: 6 Ć 6
## trial trial_no fruit channel t_min_ms y_min
## <fct> <int> <int> <chr> <dbl> <dbl>
## 1 Sep-24 1 1 CH1 0.54 -0.916
## 2 Sep-24 1 1 CH2 0.56 -0.978
## 3 Sep-24 1 2 CH1 0.56 -0.985
## 4 Sep-24 1 2 CH2 0.62 -0.825
## 5 Sep-24 1 3 CH1 0.54 -1.00
## 6 Sep-24 1 3 CH2 0.56 -0.924
summary_wide <- tidyr::pivot_wider(summary_df,
id_cols = c(trial, trial_no, fruit),
names_from = channel,
values_from = t_min_ms)
Each point is one fruit. Most points sit near the CH1 = CH2 diagonal; vertical excursions are CH2-only outliers (skin defect on the calyx end of the fruit, in the original interpretation).
library(ggplot2)
ggplot(summary_wide, aes(CH1, CH2, colour = trial)) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed",
colour = "grey60") +
geom_point(size = 2.2, alpha = 0.85) +
scale_x_continuous(limits = c(0.4, 1.05)) +
scale_y_continuous(limits = c(0.4, 1.20)) +
labs(title = "CH2 vs CH1 (synthetic, 2024-2025)",
x = "CH1 t_min (ms)",
y = "CH2 t_min (ms)",
colour = "Trial") +
theme_minimal(base_size = 12) +
theme(legend.key.height = unit(0.8, "lines"))
ggsave("figures/scatter_ch1_ch2.png", width = 7, height = 5.2, dpi = 150)
Same scatter as above but coloured by the mean t_min across the two channels ā a purely geometric ranking that highlights the diagonal trend without claiming any biophysical firmness model. Earlier minima cluster in the lower-left (purple/blue); later minima in the upper-right (orange/red).
summary_wide$t_min_mean <- (summary_wide$CH1 + summary_wide$CH2) / 2
ss1_palette <- c("blueviolet", "deepskyblue", "forestgreen",
"yellowgreen", "gold", "orange", "red")
ggplot(summary_wide, aes(CH1, CH2, colour = t_min_mean)) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed",
colour = "grey60") +
geom_point(size = 2.4, alpha = 0.9) +
scale_x_continuous(limits = c(0.4, 1.05)) +
scale_y_continuous(limits = c(0.4, 1.20)) +
scale_colour_gradientn(colours = ss1_palette) +
labs(title = "SS1 ā CH2 vs CH1, coloured by mean t_min",
x = "CH1 t_min (ms)",
y = "CH2 t_min (ms)",
colour = "mean t_min (ms)") +
theme_minimal(base_size = 12) +
theme(legend.key.height = unit(1.2, "lines"))
ggsave("figures/ss1_firmness.png", width = 7.2, height = 5.4, dpi = 150)
Off-diagonal points indicate asymmetry between stem-end and calyx-end response on the same fruit.
Traces are simulated with a damped Gabor wavelet plus a small initial transient and Gaussian noise. The repo demonstrates the processing pipeline, not a biological finding.
sessionInfo()
## R version 4.2.3 (2023-03-15 ucrt)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 10 x64 (build 26200)
##
## Matrix products: default
##
## locale:
## [1] LC_COLLATE=English_United Kingdom.utf8
## [2] LC_CTYPE=English_United Kingdom.utf8
## [3] LC_MONETARY=English_United Kingdom.utf8
## [4] LC_NUMERIC=C
## [5] LC_TIME=English_United Kingdom.utf8
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] tidyr_1.3.1 dplyr_1.1.2 ggplot2_3.5.1
##
## loaded via a namespace (and not attached):
## [1] pillar_1.9.0 bslib_0.6.2 compiler_4.2.3 jquerylib_0.1.4
## [5] highr_0.11 tools_4.2.3 digest_0.6.34 jsonlite_1.8.9
## [9] evaluate_1.0.1 lifecycle_1.0.4 tibble_3.2.1 gtable_0.3.6
## [13] pkgconfig_2.0.3 rlang_1.1.3 cli_3.6.1 yaml_2.3.10
## [17] xfun_0.43 fastmap_1.1.1 withr_3.0.2 knitr_1.45
## [21] systemfonts_1.1.0 generics_0.1.3 vctrs_0.6.5 sass_0.4.9
## [25] grid_4.2.3 tidyselect_1.2.1 glue_1.8.0 R6_2.5.1
## [29] textshaping_0.4.0 fansi_1.0.4 rmarkdown_2.29 purrr_1.0.2
## [33] farver_2.1.2 magrittr_2.0.3 scales_1.3.0 htmltools_0.5.7
## [37] colorspace_2.1-1 ragg_1.3.3 labeling_0.4.3 utf8_1.2.3
## [41] munsell_0.5.1 cachem_1.0.8