--- title: "Getting started with nutrigon: the Lee et al. (2008) example" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Getting started with nutrigon} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 6, fig.height = 5 ) ``` ## Overview This vignette replicates the worked Lee et al. (2008) example from the Nutrigonometry validation paper, using the `lee_drosophila` dataset shipped with this package. It walks through the core workflow: 1. `nutrigon_fit()` -- fit a quadratic response surface and locate the nutritional optimum for each trait. 2. `nutrigon_compare()` -- bootstrap the angular separation between the two optima. 3. `nutrigon_snr()`, `nutrigon_build_grid()` and `nutrigon_check()` -- estimate the coverage expected for this result from simulations matched to its design and signal-to-noise ratio. 4. `nutrigon_equivalence()` -- test whether two optima are aligned. The Lee dataset records nutrient intake, lifespan, and daily egg production for female *Drosophila melanogaster* reared across a geometric array of protein:carbohydrate diets. Both traits were measured on the same individual flies, so the comparison below is **paired**. ```{r setup} library(nutrigon) data(lee_drosophila) str(lee_drosophila) ``` ## Fitting the two surfaces ```{r fit-surfaces} fit_lifespan <- nutrigon_fit( lee_drosophila, "protein_eaten", "carb_eaten", "lifespan", design_id_col = "treatment", peak = TRUE ) print(fit_lifespan) fit_eggs <- nutrigon_fit( lee_drosophila, "protein_eaten", "carb_eaten", "dailyeggs", design_id_col = "treatment", peak = TRUE ) print(fit_eggs) ``` ## Comparing the optima ```{r compare, cache = FALSE} result_lee <- nutrigon_compare( lee_drosophila, "protein_eaten", "carb_eaten", "lifespan", lee_drosophila, "protein_eaten", "carb_eaten", "dailyeggs", paired = TRUE, # both traits measured on the same flies resample = "case", # case resampling (the validated scheme) B = 500, seed = 1 ) print(result_lee) result_lee$point1 # location of the lifespan optimum result_lee$angular_support # angular range of each design hull ``` The point estimate and confidence interval should closely match the published result: theta = 20.51 degrees, 95% CI [10.99, 26.14]. Theta is the difference in nutrient balance (protein:carbohydrate direction) between the two optima. It does not measure how far apart they are in total intake, nor the fitness cost of feeding between them, so report it together with the locations of the optima. ## Design-matched coverage check `nutrigon_check()` estimates the coverage expected for this interval from simulated configurations that resemble it in three risk factors (curvature elongation, distance of each optimum from the edge of its design's angular support, and angle difference). The validation grid should be simulated on **your own design** and at the **signal-to-noise ratio** of your fitted surfaces: ```{r snr} snr <- c(lifespan = nutrigon_snr(fit_lifespan), eggs = nutrigon_snr(fit_eggs)) snr ``` Building the grid is slow (100 configurations x 20 simulated datasets x 50 bootstrap replicates); build it once and save it. ```{r grid, eval = FALSE} grid_lee <- nutrigon_build_grid( design = lee_drosophila, x_col = "protein_eaten", y_col = "carb_eaten", design_id_col = "treatment", snr = unname(snr), seed = 99) saveRDS(grid_lee, "grid_lee.rds") check_lee <- nutrigon_check( result_lee, elong1 = nutrigon_elongation(fit_lifespan), elong2 = nutrigon_elongation(fit_eggs), validation_grid = grid_lee, k = 10) check_lee ``` For a quick look, the generic grid shipped with the package can be used; `nutrigon_check()` warns that it was simulated on a different design: ```{r reliability} check_generic <- nutrigon_check( result_lee, elong1 = nutrigon_elongation(fit_lifespan), elong2 = nutrigon_elongation(fit_eggs)) check_generic ``` The check is a diagnostic: it does not change the confidence interval. It assumes quadratic surfaces with Gaussian noise and does not account for model misspecification. ## Small angles and alignment Near zero, the unsigned interval lies above zero even when the true separation is zero, so it cannot show that two optima are aligned. `nutrigon_check()` does not assess results below 5 degrees; use an equivalence test with a smallest angle of biological interest, chosen in advance: ```{r equivalence} nutrigon_equivalence(result_lee, delta = 10) ``` ## Plotting the surfaces The plot helpers `nutrigon_grid()`, `nutrigon_hull()`, and `nutrigon_region_polygon()` require the \pkg{ggplot2} package (listed under Suggests). ```{r plot-surfaces, eval = requireNamespace("ggplot2", quietly = TRUE)} library(ggplot2) grid_lifespan <- nutrigon_grid(fit_lifespan, n = 100) hull_lifespan <- nutrigon_hull(fit_lifespan) region_lifespan <- nutrigon_region_polygon(fit_lifespan) ggplot(grid_lifespan, aes(x = protein_eaten, y = carb_eaten, fill = predicted)) + geom_raster(interpolate = TRUE) + geom_contour(aes(z = predicted), colour = "black", linewidth = 0.08) + geom_polygon(data = hull_lifespan, aes(x = protein_eaten, y = carb_eaten), fill = NA, colour = "black", linewidth = 0.5, inherit.aes = FALSE) + geom_polygon(data = region_lifespan, aes(x = protein_eaten, y = carb_eaten), fill = "red", colour = "grey50", alpha = 0.3, linewidth = 0.6, inherit.aes = FALSE) + scale_fill_distiller(palette = "Spectral", name = "Lifespan (days)") + labs(x = "Protein (mg)", y = "Carbohydrate (mg)", title = "Lifespan response surface") + theme_bw(base_size = 12) + theme(panel.grid = element_blank()) ``` ## Local curvature and surface area `nutrigon_curvature()` computes local Gaussian and mean curvature of a fitted surface -- a different notion of "curvature" from the Hessian-based elongation ratio above. Elongation describes how stretched the peak is along its principal axes; Gaussian/mean curvature describe how much the surface itself bends at each point, treating it as a 2-manifold embedded in 3-space. ```{r curvature} curv_lifespan <- nutrigon_curvature(fit_lifespan, n = 60) head(curv_lifespan$grid) cat(sprintf("Surface area ratio (fitted vs flat): %.3f\n", curv_lifespan$surface_area_ratio)) ``` A surface-area ratio near 1 indicates an almost-flat response; larger values indicate a more strongly bowl- or saddle-shaped surface. This uses \pkg{pracma} for numerical integration; if it is not installed, `surface_area_ratio` is `NA` with a warning. ```{r curvature-plot, eval = requireNamespace("ggplot2", quietly = TRUE)} ggplot(curv_lifespan$grid, aes(x = protein_eaten, y = carb_eaten, fill = Mean_curv)) + geom_raster(interpolate = TRUE) + geom_polygon(data = hull_lifespan, aes(x = protein_eaten, y = carb_eaten), fill = NA, colour = "black", linewidth = 0.5, inherit.aes = FALSE) + scale_fill_distiller(palette = "RdBu", name = "Mean\ncurvature") + labs(x = "Protein (mg)", y = "Carbohydrate (mg)", title = "Lifespan: local mean curvature") + theme_bw(base_size = 12) + theme(panel.grid = element_blank()) ``` ## References Lee, K.P., Simpson, S.J., Clissold, F.J., Brooks, R., Ballard, J.W.O., Taylor, P.W., Soran, N. & Raubenheimer, D. (2008). Lifespan and reproduction in *Drosophila*: new insights from nutritional geometry. *PNAS*, 105(7), 2498-2503. Morimoto, J. (in review). Bootstrap inference for angular separation between nutritional optima in the Geometric Framework for Nutrition. *Methods in Ecology and Evolution*.