OSCR

A thyroid hormone-mediated opsin switch initiates metamorphosis in a proto-vertebrate.

Code ↔ Paper

10 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 10 matches
  1. [1] § MATERIALS AND METHODS › Thyroid hormone inhibitors ↔ code/Fig-S10A_goidrogendExp.qmd, lines 149–185 · score 0.97 · 0–30 %, 30–50 %, 50–80 %, 80–100 %, Iopanoic acid, tail regression categories
  2. [2] § MATERIALS AND METHODS › Attachment measurement ↔ code/Fig-4D_rescueExp_photoreceptor.qmd, lines 471–604 · score 0.68 · confidence intervals, generalized linear mixed, binomial, lacz, fitted, rescue
  3. [3] § RESULTS › Thyroid receptor controls endodermal strand migration ↔ code/Fig-S6_S7_endodermal_strand.qmd, lines 296–388 · score 0.63 · endodermal strand, S6, Nkx2.1, S7, migration, WRPW
  4. [4] § RESULTS › An Opsin switch in photoreceptor cells ↔ code/Fig-4C_OEexp_photoreceptor.qmd, lines 521–579 · score 0.62 · binomial GLMM, confidence intervals, Larval attachment, lacz, mixed, model
  5. [5] § MATERIALS AND METHODS › Attachment measurement ↔ code/Fig-4C_OEexp_photoreceptor.qmd, lines 521–579 · score 0.61 · confidence intervals, Larval attachment, binomial, lacz, fitted, mixed
  6. [6] § RESULTS › An Opsin switch in photoreceptor cells ↔ code/Fig-4D_rescueExp_photoreceptor.qmd, lines 471–604 · score 0.61 · confidence intervals, generalized linear mixed, GLMM, binomial, biological replicates, Error
  7. [7] § RESULTS › Overview of TH signaling in Ciona ↔ code/Fig-1A_thyroid-receptor.qmd, lines 169–239 · score 0.55 · mid tailbud, hatching larva, Fert, post, juveniles, receptor
  8. [8] § MATERIALS AND METHODS › RNA isolation, cDNA synthesis, and RT-qPCR ↔ code/Fig-S5H_thr_RT-qPCR_nkx2.1-KD.qmd, lines 182–222 · score 0.55 · technical replicates, RT, actin, PCR, Bio, nkx2
  9. [9] § MATERIALS AND METHODS › Endodermal strand measurement ↔ code/Fig-S6_S7_endodermal_strand.qmd, lines 296–388 · score 0.53 · endodermal strand, Nkx2.1, WRPW, hox10
  10. [10] § RESULTS › Conserved thyroid gene regulatory network ↔ code/Fig-S5H_thr_RT-qPCR_nkx2.1-KD.qmd, lines 723–746 · score 0.52 · binding site, qPCR, RT, Thr expression, Nkx2.1, diminish

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Quarto · 843 lines · 27 KB · no license · 2 matches

  1. ---
  2. title: "Fig-4D_rescueExp_photoreceptor"
  3. author: "Andrea Mariossi"
  4. format:
  5. html:
  6. embed-resources: true
  7. toc: true
  8. toc-location: left
  9. toc-depth: 5
  10. code-fold: false
  11. code-line-numbers: true
  12. page-layout: article
  13. grid:
  14. sidebar-width: 100px
  15. body-width: 2000px
  16. margin-width: 100px
  17. gutter-width: 3.5rem
  18. execute:
  19. echo: true
  20. warning: false
  21. message: true
  22. fig-width: 18
  23. fig-height: 10
  24. fig-align: center
  25. fig-format: png
  26. out-width: "90%"
  27. editor_options:
  28. chunk_output_type: inline
  29. ---
  30. # Photoreceptor Overexpression: Opsin1 & Opsin2 & Thr
  31. For each biological replicate, embryos were dissected from a pooled parent set of 4–5 wild caught adults. Following fertilization, embryos were electroporated across four distinct experimental groups:
  32. - **Control**: Scrambled sgRNA.
  33. - **Knockdown**: sgRNA targeting *Thr*.
  34. - **Rescue (20µg)**: sgRNA targeting *Thr* co-electroporated with 20µg *Opsin2* plasmid.
  35. - **Rescue (40µg)**: sgRNA targeting *Thr* co-electroporated with 40µg *Opsin2* plasmid.
  36. Larvae were cultured until reaching the settlement stage, at which point they were fixed. Attachment success was quantified as a binary outcome (Yes/No). The assay utilized a standardized "swirl test," where attachment was defined as the larva's ability to
  37. remain adhered to the petri dish bottom while the water was swirled. Each petri dish contained approximately 40–60 embryos. The specific sample size per dish varied due to the volumetric transfer of embryos from the electroporation cuvettes via pipetting.
  38. The entire experimental workflow was repeated across four independent biological runs on different days, with each condition represented in every run.
  39. ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  40. - **Inputs** come either from my local folder or from the repository’s folder (when downloaded from GitHub).
  41. - **Outputs** of GitHub are written to a new local folder named `output_rerun/`
  42. ## Setup
  43. ```{r setup}
  44. #| include: false
  45. rm(list = ls())
  46. # ---- packages ----
  47. suppressPackageStartupMessages({
  48. library(here)
  49. library(tidyverse)
  50. library(readxl)
  51. library(paletteer)
  52. library(rstatix)
  53. library(ggpubr)
  54. library(knitr)
  55. library(Cairo)
  56. library(lme4)
  57. library(lmerTest)
  58. library(emmeans)
  59. library(kableExtra)
  60. library(patchwork)
  61. library(binom)
  62. })
  63. ```
  64. ## Configuration
  65. ```{r config}
  66. # ---- configuration ----
  67. pick_pipeline <- "local" # "local" or "github"
  68. # ---- my paths (inputs+outputs) ----
  69. file_path <- "/Users/andrea/Library/CloudStorage/[email hidden]/My Drive/4.paper_archive/2025_thyroid_pathway_&_metamorphosis/04.SciAdv_rev/source_data"
  70. out_dir <- file.path(file_path, "output_rerun")
  71. # ---- github repo paths ----
  72. repo_dir <- here()
  73. # fallback: check if repo exists in Downloads folder
  74. if (!dir.exists(file.path(repo_dir, "data"))) {
  75. repo_dir_try <- path.expand("~/Downloads/thyroid_hormone_ciona-main")
  76. if (dir.exists(file.path(repo_dir_try, "data"))) {
  77. repo_dir <- repo_dir_try
  78. }
  79. }
  80. # ---- switch ----
  81. if (pick_pipeline == "local") {
  82. data_dir <- file_path
  83. SAVE_OUTPUTS <- TRUE
  84. dir.create(out_dir, showWarnings = FALSE, recursive = TRUE)
  85. opath <- function(...) file.path(out_dir, ...)
  86. } else if (pick_pipeline == "github") {
  87. data_dir <- file.path(repo_dir, "data")
  88. stopifnot(dir.exists(data_dir))
  89. SAVE_OUTPUTS <- TRUE
  90. out_dir <- file.path(repo_dir, "output_rerun")
  91. dir.create(out_dir, showWarnings = FALSE, recursive = TRUE)
  92. opath <- function(...) file.path(out_dir, ...)
  93. } else {
  94. stop("pick_pipeline must be either 'local' or 'github'")
  95. }
  96. dpath <- function(...) file.path(data_dir, ...)
  97. message("Pipeline: ", pick_pipeline)
  98. message("Data dir (inputs): ", data_dir)
  99. message("Outputs enabled: ", SAVE_OUTPUTS)
  100. if (SAVE_OUTPUTS) message("Outputs dir: ", out_dir)
  101. ```
  102. ## Theme & Style
  103. ```{r theme}
  104. # ---- theme & style ----
  105. custom_theme <- function() {
  106. theme(
  107. axis.ticks = element_line(linewidth = 0.5),
  108. axis.ticks.length = unit(0.09, "cm"),
  109. legend.key.height = unit(20, "pt"),
  110. axis.line = element_blank(),
  111. panel.border = element_rect(colour = "black", fill = NA, linewidth = 1),
  112. panel.background = element_blank(),
  113. plot.background = element_blank(),
  114. panel.grid.major = element_blank(),
  115. panel.grid.minor = element_blank(),
  116. legend.background = element_blank(),
  117. legend.box.background = element_blank(),
  118. legend.key = element_blank(),
  119. strip.background = element_blank()
  120. )
  121. }
  122. theme_set(theme_pubr(base_size = 13, base_family = "Helvetica") + custom_theme())
  123. no_legend <- theme(legend.position = "none")
  124. axis_text_45 <- theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1))
  125. # ----extra ----
  126. stage_order <- c("earTII", "midTII", "latTII", "latTIII", "larva")
  127. panel_a_colors <- c("#636363", "#00AAAB", "#1970DB")
  128. # ---- pretty p-values for kable ----
  129. format_pvalues <- function(df) {
  130. df %>%
  131. mutate(across(
  132. any_of(c("p.value", "p.value.x", "p.value.y", "adj.p.value")),
  133. ~ case_when(
  134. . < 0.001 ~ format(., scientific = TRUE, digits = 2),
  135. TRUE ~ as.character(round(., 4))
  136. )
  137. ))
  138. }
  139. ```
  140. ## Load Data
  141. ### Photoreceptors Thr KD & rescue with Opsin2 using Opsin1 enhancer
  142. **Key Results:**
  143. - sgRNA_Thr shows reduced attachment compared to control
  144. - sgThr_opsin2_20ug shows increased attachment compared to control
  145. - 40μg opsin2 cDNA shows even stronger effects
  146. ```{r load_data}
  147. rescue_file <- dpath("13_photoreceptor_CRISPR_rescue.xlsx")
  148. stopifnot(file.exists(rescue_file))
  149. # read and preprocess data
  150. opsn1_cas_rescue.df <- read_excel(rescue_file) %>%
  151. slice(1:17) |>
  152. drop_na()
  153. # 'lacz' as the reference level
  154. opsn1_cas_rescue.df$condition <- factor(opsn1_cas_rescue.df$condition,
  155. levels = c("sgRNA_scramble", "sgRNA_thr", "sgThr_opsin2_20ug"))
  156. # QC number
  157. sample_summary <- opsn1_cas_rescue.df %>%
  158. group_by(condition, bioRep) %>%
  159. summarise(n_larvae = sum(attached_larvae + swimming_larvae),
  160. n_attached = sum(attached_larvae),.groups = "drop") |>
  161. group_by(condition) %>%
  162. summarise(
  163. n_replicates = n(),
  164. total_larvae = sum(n_larvae),
  165. min_larvae_per_rep = min(n_larvae),
  166. max_larvae_per_rep = max(n_larvae)
  167. )
  168. sample_summary %>%
  169. kable(caption = "Per-condition summary") %>%
  170. kable_styling(bootstrap_options = c("striped", "hover"))
  171. # percentages per replicate
  172. opsn1_cas_rescue.tidy <- opsn1_cas_rescue.df %>%
  173. mutate(total_larvae = swimming_larvae + attached_larvae) %>%
  174. pivot_longer(
  175. cols = c(attached_larvae, swimming_larvae),
  176. names_to = "status",
  177. values_to = "count"
  178. ) %>%
  179. group_by(condition, bioRep) %>%
  180. mutate(percent = round(count * 100 / sum(count), 1)) %>% # % per replicate
  181. ungroup()
  182. opsn1_cas_rescue.tidy %>%
  183. kable(caption = "Per-replicate percentages") %>%
  184. kable_styling(bootstrap_options = c("striped", "hover"))
  185. # summarize per condition
  186. opsn1_cas_rescue_condition.tidy <- opsn1_cas_rescue.df %>%
  187. mutate(total_larvae = swimming_larvae + attached_larvae) %>%
  188. group_by(condition) %>%
  189. summarise(
  190. attached = sum(attached_larvae),
  191. not_attached = sum(swimming_larvae),
  192. total = sum(total_larvae)
  193. ) %>%
  194. pivot_longer(
  195. cols = c(attached, not_attached),
  196. names_to = "status",
  197. values_to = "count"
  198. ) %>%
  199. mutate(percent = count / total * 100)
  200. opsn1_cas_rescue_condition.tidy %>%
  201. kable(caption = "Per-condition summary") %>%
  202. kable_styling(bootstrap_options = c("striped", "hover"))
  203. # summary data: mean and SE for each segment
  204. opsn1_cas_rescue_summary.df <- opsn1_cas_rescue.tidy %>%
  205. filter(condition != "sgThr_opsin2_40ug") %>%
  206. group_by(condition, status) %>%
  207. summarise(
  208. mean_percent = mean(percent),
  209. se = sd(percent) / sqrt(n()),
  210. .groups = "drop"
  211. ) %>%
  212. arrange(condition, status) %>%
  213. # calculate cumulative y-position for error bars
  214. group_by(condition) %>%
  215. arrange(condition, desc(status)) %>% # stacking order
  216. mutate(
  217. y_cumulative = cumsum(mean_percent), # top of each segment
  218. y_start = y_cumulative - mean_percent # bottom of each segment
  219. )
  220. opsn1_cas_rescue_summary.df %>%
  221. kable(caption = "Summary statistics with error bar positions") %>%
  222. kable_styling(bootstrap_options = c("striped", "hover"))
  223. ```
  224. ## Quick Sanity Check
  225. ```{r quick_check}
  226. # quick visualization
  227. ggbarplot(
  228. opsn1_cas_rescue.tidy %>% drop_na(),
  229. x = "condition",
  230. y = "percent",
  231. add = "mean_se",
  232. error.plot = "errorbar",
  233. ylab = "% larvae attached",
  234. xlab = "",
  235. color = "black",
  236. fill = "status",
  237. palette = "uchicago",
  238. label = TRUE,
  239. lab.col = "black",
  240. lab.size = 5,
  241. lab.pos = "in",
  242. lab.nb.digit = 1
  243. ) +
  244. custom_theme()
  245. # side-by-side comparison
  246. opsn1_cas_rescue_summary.df %>%
  247. filter(condition != "sgThr_opsin2_40ug") %>%
  248. mutate(status = factor(status, levels = c("swimming_larvae", "attached_larvae"))) %>%
  249. ggplot(aes(x = condition, y = mean_percent, fill = status)) +
  250. geom_col(position = "dodge", width = 0.7, alpha = 0.8) +
  251. geom_errorbar(
  252. aes(ymin = mean_percent - se, ymax = mean_percent + se),
  253. position = position_dodge(width = 0.7),
  254. width = 0.2
  255. ) +
  256. scale_fill_manual(
  257. values = c("attached_larvae" = "#882222", "swimming_larvae" = "gray40"),
  258. labels = c("no", "yes")
  259. ) +
  260. scale_y_continuous(
  261. labels = scales::percent_format(scale = 1),
  262. breaks = c(25, 50, 75, 100),
  263. limits = c(0, 100)
  264. ) +
  265. labs(y = "% of Larvae", x = "Condition") +
  266. custom_theme()
  267. ```
  268. ## Stacked Plot with Symmetrical CI
  269. ```{r stacked_plot_symmetric}
  270. #-------------------------------------------------------------------------------
  271. # stacked plot
  272. rescue_plot_onlyLower <- ggplot(
  273. opsn1_cas_rescue_summary.df,
  274. aes(x = condition, y = mean_percent, fill = status)
  275. ) +
  276. # stacked bars (mean %)
  277. geom_col(width = 0.7, alpha = 0.8, color = "black") +
  278. # segment-specific error bars
  279. geom_errorbar(
  280. data = subset(opsn1_cas_rescue_summary.df, status == "attached_larvae"),
  281. aes(
  282. ymin = y_start - se, # at the boundary
  283. ymax = y_start + se
  284. ),
  285. width = 0.2,
  286. color = "black",
  287. linewidth = 0.5,
  288. position = position_dodge(width = 0)
  289. ) +
  290. geom_text(
  291. data = opsn1_cas_rescue_condition.tidy %>%
  292. filter(condition != "sgThr_opsin2_40ug") %>%
  293. distinct(condition, total),
  294. aes(x = condition, y = 105, label = paste0("n = ", total)),
  295. inherit.aes = FALSE
  296. ) +
  297. scale_fill_manual(
  298. values = c("attached_larvae" = "#882222", "swimming_larvae" = "gray70"),
  299. labels = c("Attached", "Not Attached")
  300. ) +
  301. labs(x = "Condition", y = "% larvae attached", fill = "Status") +
  302. theme_minimal(base_size = 14) +
  303. theme(legend.position = "top", panel.grid.major.x = element_blank())
  304. rescue_plot_onlyLower +
  305. scale_y_continuous(
  306. breaks = seq(0, 100, by = 25),
  307. # labels = scales::label_number(suffix = "%"),
  308. expand = expansion(mult = c(0, 0.1))
  309. ) +
  310. # significance stars
  311. geom_text(
  312. data = data.frame(
  313. condition = c("sgRNA_thr", "sgThr_opsin2_20ug"),
  314. y = c(125, 125),
  315. label = c("**", "***") # this was *** wrong!
  316. ),
  317. aes(x = condition, y = y, label = label),
  318. inherit.aes = FALSE, size = 6, vjust = 0.5
  319. ) +
  320. # brackets for comparisons
  321. geom_segment(
  322. data = data.frame(
  323. x = c(1, 1), # starting bar index
  324. xend = c(2, 3), # ending bar index
  325. y = c(115, 120), # height of the bracket
  326. yend = c(115, 120)
  327. ),
  328. aes(x = x, xend = xend, y = y, yend = yend),
  329. inherit.aes = FALSE
  330. ) +
  331. custom_theme()
  332. rescue_plot_onlyLower + theme(
  333. legend.position = "bottom",
  334. panel.spacing.x = unit(0.1, "cm"), # Tighter than 0.2
  335. axis.text.x = element_text(margin = margin(t = 2)), # Less padding
  336. aspect.ratio = 1.2
  337. )
  338. ```
  339. ## Batch consistency
  340. ```{r}
  341. # ---- batch inspection ----
  342. # Calculate proportions per replicate and condition
  343. batch_data <- opsn1_cas_rescue.tidy %>%
  344. filter(status == "attached_larvae" & condition != "sgThr_opsin2_40ug")
  345. batch_plot <- ggplot(batch_data, aes(x = condition, y = percent, fill = condition)) +
  346. # Add a background area to show the "trend" per batch
  347. geom_line(aes(group = bioRep), alpha = 0.3, color = "gray50") +
  348. geom_point(shape = 21, size = 3, color = "black", stroke = 1) +
  349. # Facet by the biological replicate
  350. facet_wrap(~bioRep, ncol = 4) +
  351. scale_fill_manual(values = panel_a_colors) +
  352. scale_y_continuous(limits = c(0, 100)) +
  353. labs(
  354. subtitle = "Each panel represents a different electroporation day",
  355. y = "% Larvae Attached",
  356. x = ""
  357. ) +
  358. custom_theme() +
  359. theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
  360. no_legend
  361. batch_plot
  362. ```
  363. ## Statistical Analysis
  364. Binomial outcomes (successes vs. failures).
  365. Just to have an idea BUT IT IS NOT accurate, it doesn't consider biological replicate (pooling them)
  366. - Fisher's exact test: better for small samples
  367. - Chi-square test: large sample size
  368. ```{r statistical_tests}
  369. #-------------------------------------------------------------------------------
  370. # Fisher's Exact Test | Chi test
  371. #-------------------------------------------------------------------------------
  372. # ---- fisher's exact test | chi test ----
  373. contingency_table <- opsn1_cas_rescue.df %>%
  374. drop_na() %>%
  375. mutate(total_larvae = swimming_larvae + attached_larvae) %>%
  376. group_by(condition) %>%
  377. summarise(
  378. attached = sum(attached_larvae),
  379. swimming = sum(swimming_larvae)
  380. ) %>%
  381. column_to_rownames("condition") %>%
  382. as.matrix()
  383. ### stat Fisher's Exact Test ==> 2.142e-14 p value
  384. # fisher.test(contingency_table) # failing large sampel size ==> Chi-square Test
  385. # pearson's chi-square test
  386. chi_test <- chisq.test(contingency_table)
  387. chi_test
  388. # perform all pairwise comparisons
  389. pairwise_results <- pairwise.prop.test(
  390. contingency_table,
  391. p.adjust.method = "bonferroni"
  392. ) %>%
  393. tidy()
  394. pairwise_results %>%
  395. kable(caption = "Pairwise comparisons (Bonferroni-corrected)") %>%
  396. kable_styling(bootstrap_options = c("striped", "hover"))
  397. ```
  398. ## Logistic Regression
  399. I used a **Generalized Linear Mixed Model (GLMM)** with **`bioRep` as a random intercept** to account for biological and technical variability inherent in **mosaic electroporation** and **wild-caught populations**.
  400. - The simplest model is a glm, but there is some variability in the electgroporation, the glmm wirh batch of larvae as a random effect works better to account for the variance.
  401. - **Random Effects**: The `bioRep` works as a **random intercept** to capture baseline shifts in attachment probability between parental sets and electroporation batches. The random effect `(1 | bioRep)` captures variability between biological replicates
  402. that the GLM ignores.
  403. <!-- -->
  404. - **Fixed Effects**: Treatment conditions (Control, *Thr* Knockdown, and *Opsin2* Rescues) were treated as fixed effects.
  405. - Glmm is penalizing for additional parameter but still lower AIC (97.2 vs 107.1) and BIC (99.2 vs 108.6)
  406. ```{r logistic_regression}
  407. # ---- logistic regression (for modeling probability of attachment) ----
  408. # Standardizing the data object for all model comparisons
  409. analysis_data <- opsn1_cas_rescue.df %>%
  410. filter(condition != "sgThr_opsin2_40ug") %>%
  411. mutate(condition = factor(condition,
  412. levels = c("sgRNA_scramble", "sgRNA_thr", "sgThr_opsin2_20ug")))
  413. #-----------------------------------------------------------------------------
  414. # fit the simpler GLM (for comparison)
  415. #-------------------------------------------------------------------------------
  416. # ignores the batch/day effect
  417. rescue_glm <- glm(
  418. cbind(attached_larvae, swimming_larvae) ~ condition,
  419. family = binomial,
  420. data = analysis_data
  421. )
  422. summary(rescue_glm)
  423. # overdispersion check
  424. performance::check_overdispersion(rescue_glm) # Significant overdispersion present
  425. rescue_glm %>%
  426. tidy(conf.int = TRUE, exponentiate = TRUE) %>%
  427. kable(caption = "GLM Results (Odds Ratios)") %>%
  428. kable_styling(bootstrap_options = c("striped", "hover"))
  429. # interpretation:
  430. # - Intercept: Baseline odds (control = lacz)
  431. # - sgRNA_thr: OR = 0.496 (50% lower odds vs control)
  432. # - opsin2_20ug: OR = 2.63 ( 163% higher odds vs control)
  433. #-----------------------------------------------------------------------------
  434. # fit the robust GLMM (accounting for electroporation and shipping batches)
  435. #-------------------------------------------------------------------------------
  436. # mixed model: with random intercepts for replicates
  437. rescue_glmer <- glmer(
  438. cbind(attached_larvae, swimming_larvae) ~ condition + (1 | bioRep),
  439. family = binomial,
  440. data = analysis_data
  441. )
  442. summary(rescue_glmer)
  443. exp(fixef(rescue_glmer)) # get odds ratios
  444. summary(rescue_glmer)$varcor # checking the "Random Effect" Variance
  445. # extract confidence intervals for odds ratios
  446. confint_model <- confint(rescue_glmer, method = "Wald")
  447. confint_model
  448. # extract p-values for the plot
  449. # compare everything back to the intercept (Control)
  450. stats_for_plot <- summary(rescue_glmer)$coefficients %>%
  451. as.data.frame() %>%
  452. rownames_to_column("group2") %>%
  453. filter(group2 != "(Intercept)") %>%
  454. mutate(group1 = "sgRNA_scramble",
  455. group2 = str_remove(group2, "condition"),
  456. p.adj = `Pr(>|z|)`, # You can apply p.adjust() here if desired
  457. label = case_when(p.adj < 0.001 ~ "***", p.adj < 0.01 ~ "**", p.adj < 0.05 ~ "*", TRUE ~ "ns")
  458. )
  459. stats_for_plot
  460. #-----------------------------------------------------------------------------
  461. # ---- Model Comparison ----
  462. # Likelihood Ratio Test to justify GLMM
  463. lrt_result <- anova(rescue_glmer, rescue_glm)
  464. lrt_result # pvale < 0.05 confirms GLMM is required
  465. # overdispersion check
  466. dispersion_ratio <- sum(residuals(rescue_glmer, type = "pearson")^2) / df.residual(rescue_glmer)
  467. message("Dispersion Ratio: ", round(dispersion_ratio, 3))
  468. performance::check_overdispersion(rescue_glmer)
  469. # No significant overdispersion
  470. #-----------------------------------------------------------------------------
  471. # tidy results
  472. bind_rows(
  473. broom.mixed::tidy(rescue_glm, effects = "fixed", conf.int = TRUE) %>% mutate(model = "GLM"),
  474. broom.mixed::tidy(rescue_glmer, effects = "fixed", conf.int = TRUE) %>% mutate(model = "GLMM")
  475. ) %>%
  476. mutate(OR = exp(estimate),lower_CI = exp(conf.low),upper_CI = exp(conf.high)) %>%
  477. dplyr::select(model, term, OR, lower_CI, upper_CI, std.error, p.value) %>%
  478. kable(caption = "Comparison of GLM and GLMM models") %>%
  479. kable_styling(bootstrap_options = c("striped", "hover"))
  480. # ---- Table: GLMM Odds Ratios ----
  481. table_ready <- broom.mixed::tidy(rescue_glmer, conf.int = TRUE, effects = "fixed") %>%
  482. mutate(OR = exp(estimate), lower_CI = exp(conf.low), upper_CI = exp(conf.high)) %>%
  483. dplyr::select(term, OR, lower_CI, upper_CI, std.error, p.value) %>%
  484. mutate(term = case_when(
  485. term == "(Intercept)" ~ "Control (sgRNA_scramble)",
  486. term == "conditionsgRNA_thr" ~ "Thr Knockdown (sgRNA_thr)",
  487. term == "conditionsgThr_opsin2_20ug" ~ "Rescue (sgThr_opsin2_20ug)",
  488. TRUE ~ term))
  489. format_pvalues_vec <- function(p_vec) {case_when(
  490. p_vec < 0.001 ~ format(p_vec, scientific = TRUE, digits = 2),
  491. TRUE ~ as.character(round(p_vec, 4))
  492. )
  493. }
  494. table_ready %>%
  495. mutate(p.value = format_pvalues_vec(p.value)) %>%
  496. kbl(
  497. caption = "Table 1: Logistic Regression Analysis of Attachment Probability (GLMM)",
  498. digits = 3,
  499. booktabs = TRUE,
  500. col.names = c("Condition", "Odds Ratio", "95% CI Lower", "95% CI Upper", "Std. Error", "p-value"),
  501. align = "lccccc"
  502. ) %>%
  503. kable_styling(bootstrap_options = c("striped", "condensed"), full_width = FALSE) %>%
  504. column_spec(6, bold = (table_ready$p.value < 0.05)) # bold significant p-value
  505. ```
  506. | | | | |
  507. |------------------------|---------------------|-------------|--------------------------------------------------------------------------------|
  508. | **Contrast** | **Odds Ratio (OR)** | **p-value** | **Biological Interpretation** |
  509. | **KD vs. Control** | **0.551** | $0.0013$ | *Thr* knockdown reduces attachment odds by \~45%. |
  510. | **Rescue vs. Control** | **3.986** | $<0.0001$ | *Opsin2* increases attachment odds by nearly 4-fold over baseline. |
  511. | **Rescue vs. KD** | **7.227** | $<0.0001$ | **The Key Result:** Rescue has \>7x higher odds of success than the knockdown. |
  512. ```{r}
  513. # ---- Forest Plot of Odds Ratios (effect size) ----
  514. forest_plot <- ggplot(table_ready %>% filter(term != "Control (sgRNA_scramble)"),
  515. aes(x = OR, y = term)) +
  516. geom_vline(xintercept = 1, linetype = "dashed", color = "red") +
  517. geom_errorbarh(aes(xmin = lower_CI, xmax = upper_CI), height = 0.2) +
  518. geom_point(size = 4, aes(color = term)) +
  519. scale_x_log10(breaks = c(0.5, 1, 3, 5)) +
  520. labs(
  521. title = "Effect Size (Odds Ratios)",
  522. x = "Odds Ratio (log scale)",
  523. y = ""
  524. ) +
  525. scale_color_manual(values = c("Thr Knockdown (sgRNA_thr)" = "#00AAAB",
  526. "Rescue (sgThr_opsin2_20ug)" = "#1970DB")) +
  527. custom_theme() +
  528. no_legend
  529. forest_plot
  530. ```
  531. ## Binomial Confidence Intervals
  532. Wilson CIs for asymmetry
  533. ```{r wilson_ci}
  534. # ---- wilson CIs ----
  535. opsn1_cas_rescue_BiCI.df <- opsn1_cas_rescue_condition.tidy %>%
  536. drop_na() %>%
  537. group_by(condition, status) %>%
  538. mutate(
  539. BiCI_low = binom.confint(count, total, methods = "wilson")$lower * 100,
  540. BiCI_high = binom.confint(count, total, methods = "wilson")$upper * 100
  541. )
  542. # merge CI data with positioning info
  543. CI_data <- opsn1_cas_rescue_summary.df %>%
  544. # align status names between datasets
  545. mutate(status = case_when(
  546. status == "attached_larvae" ~ "attached",
  547. status == "swimming_larvae" ~ "not_attached",
  548. TRUE ~ status
  549. )) %>%
  550. left_join(opsn1_cas_rescue_BiCI.df, by = c("condition", "status")) %>%
  551. # calculate error bar positions
  552. mutate(
  553. BiCI_low_position = y_start + BiCI_low - mean_percent,
  554. BiCI_high_position = y_start + BiCI_high - mean_percent
  555. )
  556. CI_data %>%
  557. select(
  558. condition, status, mean_percent, y_start, BiCI_low, BiCI_high,
  559. BiCI_low_position, BiCI_high_position
  560. ) %>%
  561. kable(caption = "Wilson CI positions") %>%
  562. kable_styling(bootstrap_options = c("striped", "hover"))
  563. # eg repertive code but it works; to fix later
  564. # calculate pooled wilson CIs
  565. wilson_stats <- opsn1_cas_rescue.df %>%
  566. drop_na() %>%
  567. mutate(total_larvae = swimming_larvae + attached_larvae) %>%
  568. group_by(condition) %>%
  569. summarise(
  570. attached_sum = sum(attached_larvae),
  571. total_sum = sum(total_larvae),
  572. .groups = "drop"
  573. ) %>%
  574. rowwise() %>%
  575. mutate(
  576. # calculate Wilson CI using the binom package
  577. res = list(binom.confint(attached_sum, total_sum, method = "wilson")),
  578. mean_prop = res$mean * 100,
  579. lower = res$lower * 100,
  580. upper = res$upper * 100
  581. ) %>%
  582. select(-res)
  583. # simple bar plot with wilson CIs
  584. ggplot() +
  585. # bar for the pooled proportion (attached only)
  586. geom_col(
  587. data = wilson_stats %>% filter(condition != "sgThr_opsin2_40ug"),
  588. aes(x = condition, y = mean_prop,fill = condition), width = 0.6, alpha = 0.7,
  589. color = "black" ) +
  590. scale_fill_manual(values = panel_a_colors) +
  591. # wilson confidence intervals (asymmetric)
  592. geom_errorbar(
  593. data = wilson_stats,
  594. aes(x = condition, ymin = lower, ymax = upper),
  595. width = 0.15, linewidth = 0.8, color = "black") +
  596. # individual replicate points (to show the "raw" variance)
  597. geom_jitter(
  598. data = filter(opsn1_cas_rescue.tidy, status == "attached_larvae" & condition != "sgThr_opsin2_40ug"),
  599. aes(x = condition, y = percent),
  600. shape = 21, fill = "white", color = "black",
  601. size = 3, stroke = 1, width = 0.1) +
  602. # significance and N-labels
  603. geom_text(
  604. data = wilson_stats,
  605. aes(x = condition, y = upper + 5, label = paste0("n=", total_sum)),
  606. size = 3.5, vjust = 0
  607. ) +
  608. stat_pvalue_manual(stats_for_plot, y.position = 95, step.increase = 0.1, label = "label", tip.length = 0.02) +
  609. labs(y = "Larvae Attached (%)", x = NULL) +
  610. scale_y_continuous(limits = c(0, 110), breaks = seq(0, 100, 20)) +
  611. custom_theme() +
  612. no_legend
  613. ```
  614. ## Main Figure: Stacked Plot with Wilson CIs
  615. ```{r main_figure, fig.width=10, fig.height=8}
  616. # ---- stacked plot + wilson CIs ----
  617. rescue_plot_BiCI <- ggplot(
  618. opsn1_cas_rescue_summary.df,
  619. aes(x = condition, y = mean_percent, fill = status)
  620. ) +
  621. geom_col(width = 0.7, alpha = 0.8, color = "black") + # stacked bars
  622. # error bars at attachment boundary (using calculated positions)
  623. geom_errorbar(
  624. data = CI_data %>% filter(status == "attached"),
  625. aes(ymin = BiCI_low_position, ymax = BiCI_high_position),
  626. width = 0.25,
  627. color = "black",
  628. linewidth = 0.8
  629. ) +
  630. # sample size labels
  631. geom_text(
  632. data = opsn1_cas_rescue_BiCI.df %>%
  633. filter(condition != "sgThr_opsin2_40ug") %>%
  634. group_by(condition) %>%
  635. summarize(total = first(total)),
  636. aes(x = condition, y = 110, label = paste0("n=", total)),
  637. inherit.aes = FALSE,
  638. size = 3.5
  639. ) +
  640. scale_fill_manual(
  641. values = c("attached_larvae" = "#882222", "swimming_larvae" = "gray70"),
  642. labels = c("Attached", "Swimming")
  643. ) +
  644. labs(
  645. x = "",
  646. y = "Percentage of Larvae (%)",
  647. fill = "Attachment Status"
  648. ) +
  649. custom_theme() +
  650. theme(
  651. legend.position = "top",
  652. panel.grid.major.x = element_blank()
  653. ) +
  654. coord_cartesian(ylim = c(0, 115)) +
  655. # significance brackets and stars
  656. stat_pvalue_manual(
  657. data = data.frame(
  658. group1 = "sgRNA_scramble",
  659. group2 = c("sgRNA_thr", "sgThr_opsin2_20ug"),
  660. p.signif = c("**", "***"), # GLMM p-values
  661. #== thi was my pug i previusly put *** and ***
  662. # but summary(rescue_glmer) is
  663. # conditionsgRNA_thr -0.5952 0.1848 -3.220 0.00128 **
  664. # conditionsgThr_opsin2_20ug 1.3827 0.1754 7.881 3.25e-15 ***
  665. status = c("attached", "attached"),
  666. y.position = c(110, 115)
  667. ),
  668. label = "p.signif",
  669. tip.length = 0.01,
  670. bracket.size = 0.6
  671. )
  672. rescue_plot_BiCI
  673. ```
  674. ## Save Plots
  675. ```{r save_plots}
  676. if (SAVE_OUTPUTS) {
  677. # save with cowplot - various sizes
  678. cowplot::save_plot(
  679. plot = rescue_plot_BiCI,
  680. base_height = 4,
  681. base_asp = 4 / 1.9,
  682. file = opath("photoreceptor_rescue_plot_a.pdf")
  683. )
  684. cowplot::save_plot(
  685. plot = rescue_plot_BiCI,
  686. base_height = 8,
  687. base_asp = 1.9,
  688. file = opath("photoreceptor_rescue_plot_b.pdf"),
  689. limitsize = FALSE
  690. )
  691. # save with CairoPDF
  692. width_in <- 40
  693. aspect_ratio <- 3/4
  694. height_in <- width_in * aspect_ratio
  695. CairoPDF(
  696. opath("photoreceptor_rescue_plot_cairo.pdf"),
  697. width = width_in,
  698. height = height_in
  699. )
  700. print(rescue_plot_BiCI)
  701. dev.off()
  702. # additional sizes
  703. cowplot::save_plot(
  704. plot = rescue_plot_BiCI,
  705. base_width = 2.4 * 5,
  706. base_height = 3.6 * 5,
  707. file = opath("photoreceptor_rescue_plot_final.pdf")
  708. )
  709. message("\nPlots saved to: ", out_dir)
  710. }
  711. ```
  712. ## Session Info
  713. ```{r session_info}
  714. sessionInfo()
  715. ```

Fig-4D_rescueExp_photoreceptor.qmd at commit 90cf06d, no license · at the source

Overview

  1. Lewis-Sigler Institute for Integrative Genomics, Princeton University, Princeton, NJ, USA
  2. Department of Molecular Biology, Princeton University, Princeton, NJ, USA
Institutions: Princeton University (United States)
Journal: Science advances, volume 12, issue 20, article eaeb8106
Dates: received 27 August 2025; accepted 9 April 2026; published online 13 May 2026; in print May 2026
Type: Research article · Language: English
License: CC BY-NC
Identifiers: DOI 10.1126/sciadv.aeb8106 · PMID 42127196 · PMCID PMC13170672 · OpenAlex W7161043649
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: other (organism)
Methods: Statistics, Evoked potentials
MeSH: Ciona intestinalis*, Metamorphosis, Biological*, Opsins*, Thyroid Hormones*, Animals, Larva (* major topic)
Topic: Retinal Development and Disorders (Molecular Biology, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Citations: not cited yet (Europe PMC); 87 references in the paper

Abstract

Thyroid hormones (THs) initiate metamorphosis in vertebrates, although the evolutionary origins of this process are uncertain. Here, we show that most TH processing genes are present in the proto-vertebrate model Ciona and play an instructive role in initiating metamorphosis, whereby swimming tadpoles with a chordate body plan are transformed into sessile filter feeders. Exogenous thyroxine (T4) accelerates metamorphosis, whereas TH inhibitors delay onset. Most notably, we present evidence that TH activates Opsin2 (Opsin1/2b) in a subset of photoreceptor cells in the simple brain of swimming tadpoles to initiate attachment, the first step in metamorphosis. Our findings suggest a deep evolutionary origin of TH-driven visual plasticity in vertebrates. We highlight the parallels between attachment of Ciona tadpoles and smoltification, whereby young salmonid fishes switch from UV to blue opsins for their transition from fresh to saltwater.

Reproduced under the paper's license (CC BY-NC), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 10 matches between paragraphs and lines of code.

andreamariossi/thyroid_hormone_Ciona

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 90cf06d76def753ad91c1af586545d0e54c29cb7, 10 July 2026
Languages: Quarto (13)
Size: 65 files, 13 scripts
Software Heritage: not archived
Found in: “Data, code, and materials availability:”
Holds: README, 13 notebooks
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: tidyverse (13 files), ggpubr (12 files), cowplot (10 files), emmeans (10 files), patchwork (10 files), rstatix (10 files), lme4 (8 files), lmerTest (8 files), broom (4 files), easystats (4 files), car (1 file), ggplot2 (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
14 files

Zenodo 19336448

License: CC-BY-4.0
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Data, code, and materials availability:”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: tidyverse (13 files), ggpubr (12 files), cowplot (10 files), emmeans (10 files), patchwork (10 files), rstatix (10 files), lme4 (8 files), lmerTest (8 files), broom (4 files), easystats (4 files), car (1 file), ggplot2 (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers (HTTP 200)
  • 28 September 2026: the link answers (HTTP 200)
14 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 26 scripts, each with its path and the digest of its content;
  • 10 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data, code, and materials availability

All data and code needed to evaluate and reproduce the conclusions in the paper are present in the paper and/or the Supplementary Materials. Scripts for the analysis are available on GitHub (https://github.com/andreamariossi/thyroid_hormone_Ciona) and Zenodo [https://doi.org/10.5281/zenodo.19336448 (87)]. The constructs used in this study are available through a material transfer agreement with M.S.L. Requests should be directed to M.S.L. at .

Reproduced under the paper's license (CC BY-NC), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 2, 28 September 2026

  • Funding: added Princeton University: NS076542; National Institute of Neurological Disorders and Stroke: NS076542

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 2 authors, 6 MeSH terms, 86 references.

Cite

This paper

Mariossi, A., & Levine, M. S. (2026). A thyroid hormone-mediated opsin switch initiates metamorphosis in a proto-vertebrate. Science advances, 12(20), eaeb8106. https://doi.org/10.1126/sciadv.aeb8106

BibTeX

@article{mariossi2026thyroid,
author = {Mariossi, Andrea and Levine, Michael S},
title = {{A thyroid hormone-mediated opsin switch initiates metamorphosis in a proto-vertebrate}},
journal = {Science advances},
year = {2026},
month = may,
volume = {12},
number = {20},
pages = {eaeb8106},
publisher = {American Association for the Advancement of Science},
issn = {2375-2548},
doi = {10.1126/sciadv.aeb8106},
url = {https://doi.org/10.1126/sciadv.aeb8106},
pmid = {42127196},
pmcid = {PMC13170672}
}

RIS

TY - JOUR
AU - Mariossi, Andrea
AU - Levine, Michael S
TI - A thyroid hormone-mediated opsin switch initiates metamorphosis in a proto-vertebrate
T2 - Science advances
J2 - Sci Adv
PY - 2026
DA - 2026/05/13
VL - 12
IS - 20
SP - eaeb8106
SN - 2375-2548
PB - American Association for the Advancement of Science
DO - 10.1126/sciadv.aeb8106
UR - https://doi.org/10.1126/sciadv.aeb8106
LA - en
ER -

CSL-JSON

{
"id": "10.1126/sciadv.aeb8106",
"type": "article-journal",
"title": "A thyroid hormone-mediated opsin switch initiates metamorphosis in a proto-vertebrate",
"container-title": "Science advances",
"author": [
{
"family": "Mariossi",
"given": "Andrea"
},
{
"family": "Levine",
"given": "Michael S"
}
],
"container-title-short": "Sci Adv",
"volume": "12",
"issue": "20",
"page": "eaeb8106",
"DOI": "10.1126/sciadv.aeb8106",
"PMID": "42127196",
"PMCID": "PMC13170672",
"ISSN": "2375-2548",
"publisher": "American Association for the Advancement of Science",
"URL": "https://doi.org/10.1126/sciadv.aeb8106",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
13
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s41467-026-73865-9 [code]
Histamine shapes the neurocomputational dynamics of human learning.
Journal: Nature communications
In common: rstatix, easystats, car, 8 other tools
[2] doi:10.1073/pnas.2606871123 [code]
Oxytocin modulates the neurocomputational mechanisms engaged in learning rank relationships in social networks.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: easystats, car, broom, 8 other tools
[3] doi:10.1126/sciadv.aec9291 [code]
Computational mechanisms of perception in autism revealed using games inspired by rodent operant tasks.
Journal: Science advances
In common: rstatix, easystats, car, 7 other tools
[4] doi:10.1016/j.isci.2026.116747 [code]
Age and loneliness relate to reduced trust learning and alterations in amygdala function.
Journal: iScience
In common: easystats, car, broom, 7 other tools
[5] doi:10.1016/j.neuroimage.2026.122115 [code]
Midfrontal theta power relates to response speeding following frustrative nonreward.
Journal: NeuroImage
In common: rstatix, easystats, car, 7 other tools
[6] doi:10.1073/pnas.2603114123 [code]
The human hippocampus can pattern separate memories by meaning.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: rstatix, easystats, broom, 7 other tools
[7] doi:10.64898/2026.05.08.26348885 [code]
Insights from nine nights of self-applied, low-density sleep EEG during sleep restriction therapy: a proof-of-concept evaluation
Journal: medRxiv (preprint)
In common: rstatix, easystats, broom, 7 other tools
[8] doi:10.1038/s41398-026-04010-9 [code]
Bullying victimization and brain development: a longitudinal structural magnetic resonance imaging study from adolescence to early adulthood.
Journal: Translational psychiatry
In common: rstatix, easystats, broom, 7 other tools
[9] doi:10.3390/ijms27135713 [code]
Chronic Administration of Marinobufagenin in Mice Causes Hyperlocomotion and Decrease in Anxiety by Altering Monoamine Turnover Unaccompanied by Motor Deficits or Oxidative Stress.
Journal: International journal of molecular sciences
In common: rstatix, car, broom, 7 other tools
[10] doi:10.1093/nc/niag029 [code]
A data-driven approach to identifying and evaluating connectivity-based neural correlates of conscious visual perception.
Journal: Neuroscience of consciousness
In common: easystats, car, broom, 7 other tools

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.