Projects

Final projects:

  • Problem Endometriosis is an inflammatory disease characterized by the endometrial-like tissue growth outside of the uterine cavity. This ectopic growth leads to hormonal imbalances, systemic inflammation, and debilitating pain. This condition recurs in 40–50% of patients within 5 years of surgery, and it is mostly because of minimal residual disease (resulting from incomplete excision, invisible peritoneal lesions, or impaired immune clearance of remaining endometriotic cells as a result of surgical transient immunosuppression) [2]. Although it affects 10–15% of reproductive age women, there is currently no cure and current clinical management is limited to hormonal suppression, pain control and surgical excision [3]. Consequently, there is a critical need for non-invasive, targeted therapies that can modulate the immune response and minimize recurrence rates without compromising the patient’s reproductive health.

Subsections of Projects

Individual Final Project

cover image cover image

Problem

Endometriosis is an inflammatory disease characterized by the endometrial-like tissue growth outside of the uterine cavity. This ectopic growth leads to hormonal imbalances, systemic inflammation, and debilitating pain. This condition recurs in 40–50% of patients within 5 years of surgery, and it is mostly because of minimal residual disease (resulting from incomplete excision, invisible peritoneal lesions, or impaired immune clearance of remaining endometriotic cells as a result of surgical transient immunosuppression) [2]. Although it affects 10–15% of reproductive age women, there is currently no cure and current clinical management is limited to hormonal suppression, pain control and surgical excision [3]. Consequently, there is a critical need for non-invasive, targeted therapies that can modulate the immune response and minimize recurrence rates without compromising the patient’s reproductive health.


fpp1 fpp1

Solution

I propose a single intraperitoneal dose of lipid nanoparticles administered at the close of laparoscopic surgery. In residual endometriotic cells overexpressing lncH19, a toehold switch activates expression of a bioPROTAC, an anti-STAT3 monobody fused to a VHL-based E3 ligase recruitment domain, driving targeted proteasomal degradation of STAT3, a key driver of endometriotic cell survival, invasion, and inflammation. Normal peritoneal tissue remains unaffected because the goal is to eliminate what the scalpel misses, before it has the chance to establish the disease again.

How does a toehold switch works?

A toehold switch is a synthetic riboregulator that controls gene expression as a programmable gatekeeper ensuring that the treatment is only “turned on” when it detects the specific marker for endometriosis.

  • The Guarded State (OFF): To keep healthy cells safe, the switch is designed to fold into a hairpin structure that physically hides the “start” signals (the RBS and start codon) needed to make the protein, the cell’s machinery—the ribosome—cannot see them, so the treatment (bioPROTAC) is never produced.

  • The Identification (The Toehold): The switch has a small, exposed “tail” called a toehold which is specially engineered to recognize and bind to lncH19, which is only overexpressed in the diseased cells I’m targeting.

  • The Activation (ON): When lncH19 finds the toehold, it attaches to it and initiates a process called strand displacement. Essentially, it “unzips” the hairpin, forcing the RNA to unfold

  • The Result: Once the structure is open, the start signals that were previously hidden are finally exposed. The ribosome can now easily latch onto the RNA and begin building the bioPROTAC protein to eliminate the residual endometriotic cells

toehold_switch toehold_switch Adapted from Choi, Lee & Kim (2022), Int. J. Mol. Sci., 23(8), 4265. CC BY 4.0.

How does a bioPROTAC works?

BioPROTAC (biological Proteolysis-Targeting Chimeras) are molecules capable of degrading target proteins by marking them for proteasomal degradation through the addition of polyubiquitin chains [4].

The mechanism functions by recruitment of an E3 ligase complex in close proximity of a target protein, which leads to target ubiquitination and subsequent proteasomal degradation [4].

bioPROTAC bioPROTAC Adapted from Life Sensors, 2024.

The advantage of targeted protein degradation over traditional methods is that it can act on “undruggable” proteins, those lacking deep binding pockets or active sites that small-molecule inhibitors typically require.

Why STAT3?

STAT3 acts as a central regulatory axis in the endometriosis microenvironment, integrating inflammatory signals from IL-6 and IL-22 that drive ectopic cell survival, invasion, and chronic inflammation. Preclinical studies show that inhibiting STAT3 significantly reduces lesion area and modulates the signaling pathways necessary for disease persistence. By using a monobody-based bioPROTAC to degrade STAT3, we can permanently eliminate the protein in residual cells that escape surgical excision, effectively disrupting the molecular networks responsible for recurrence

RESULTS

results results

For the toehold switch the trigger has to come from a región of lncH19 that’s accesible. So I scanned the 2315 nucleotide transcript with NUPACK, computed unpaired probabilities and ranked them by GC content, no premature stops, etc.

The winner was position 1,734

Then I built the switch following Green 2014 geometry and validated in NUPACK as well.

On the left, the switch alone:

  • It folds into a hairpin that hides the start codon
  • You’ll notice that the toehold isn’t free as it should, it’s base-pairs with part of the linker. That’s something I need to improve but it doesnt break the system

On the right, when the trigger appears: it binds the toehold and displaces the hairpin, exposing the RBS and start codon.

  • bioPROTAC translation begins.

The energy difference between these two states is 35 kcal/mol - this strong driving force overcomes the residual pairing.


At first, I couldn’t find a tool for building toehold switches and the gold standar (NUPACK) needed a paid subscription. I wanted to choose the best trigger that didn’t formed a secundary structure and that was unpaired, so I used RNAfold to see the dot-bracket notation and see positional entropy. Then, I realized that I could use the NUPACK python software to choose the trigger and build the toehold switch. This is my code:

01_load_DNA
def read_fasta(path):
    with open(path) as f:
        lines = f.readlines()
    # Skip lines starting with ">" (headers)
    seq = ''.join(line.strip() for line in lines if not line.startswith('>'))
    return seq.upper()

dna = read_fasta('h19.fasta')
print(f"DNA length: {len(dna)} nt")
print(f"First 100 nt: {dna[:100]}")
print(f"Unique bases: {set(dna)}")
rna = dna.replace('T', 'U')
print(f"RNA length: {len(rna)} nt")
print(f"First 100 nt: {rna[:100]}")
# Save for the next notebooks
with open('h19_rna.txt', 'w') as f:
    f.write(rna)
print("\n✓ Saved to h19_rna.txt")
02_accessibility_scan
from nupack import Strand, Model, pairs
import numpy as np

# Load the sequence we saved in step 1
with open('h19_rna.txt') as f:
    h19 = f.read().strip()
print(f"H19 loaded: {len(h19)} nt")

# Thermodynamic model: RNA at 37 °C (cell-free temperature)
model = Model(material='rna', celsius=37)

# Create the NUPACK Strand object
h19_strand = Strand(h19, name='h19')
print("✓ Ready to compute pairing probabilities")
from nupack import pairs

matrix = pairs(strands=[h19], model=model)
P_matrix = matrix.to_array()

print(f"Matrix shape: {P_matrix.shape}")
N = len(h19)

# In NUPACK 4, P[i,i] = probability that base i is UNPAIRED
prob_unpaired = np.diag(P_matrix)

print(f"prob_unpaired statistics:")
print(f"  Min:    {prob_unpaired.min():.3f}")
print(f"  Max:    {prob_unpaired.max():.3f}")
print(f"  Mean:   {prob_unpaired.mean():.3f}")

# Sanity check: each row should sum to ~1
row_sum = P_matrix[100, :].sum()
window = 30

# For each start position, average prob_unpaired over the next 30 nt
window_score = np.array([
    prob_unpaired[i:i+window].mean()
    for i in range(N - window + 1)
])

print(f"Total windows evaluated: {len(window_score)}")
print(f"Max score:  {window_score.max():.3f}")
print(f"Mean score: {window_score.mean():.3f}")

# Keep the top 30 candidates
top_indices = np.argsort(window_score)[::-1][:30]

print(f"\n{'Pos':>5} | {'Sequence (30 nt)':<32} | {'Score':>5}")
print("-"*55)
for idx in top_indices:
    fragment = h19[idx:idx+window]
    score = window_score[idx]
    print(f"{idx:>5} | {fragment} | {score:.3f}")
with open('candidates.txt', 'w') as f:
    for idx in top_indices:
        fragment = h19[idx:idx+window]
        score = window_score[idx]
        f.write(f"{idx}\t{fragment}\t{score:.4f}\n")
print("✓ Saved 30 candidates to candidates.txt")
03_filter_candidates
def gc_content(seq):
    """Fraction of G+C in the sequence"""
    return (seq.count('G') + seq.count('C')) / len(seq)

def has_homopolymer(seq, length=5):
    """True if there are 5 or more identical bases in a row"""
    for base in 'AUGC':
        if base * length in seq:
            return True
    return False

def has_stop_in_critical_zone(trigger):
    """
    When the switch opens, the 9 nt after the AUG are trigger[9:18]
    read 5'→3'. Those 9 nt are translated as 3 consecutive codons.
    None of them can be a stop: UAA, UAG, UGA.
    """
    stops = {'UAA', 'UAG', 'UGA'}
    region = trigger[9:18]
    codons = [region[0:3], region[3:6], region[6:9]]
    return any(c in stops for c in codons), codons

def has_internal_aug(seq):
    """An internal AUG could initiate spurious translation"""
    return 'AUG' in seq
candidates = []
with open('candidates.txt') as f:
    for line in f:
        pos, seq, score = line.strip().split('\t')
        candidates.append((int(pos), seq, float(score)))

print(f"{'Position':>5} | {'Sequence':<32} | {'Score':>5} | {'GC':>4} | {'Homopolymers':>12} | {'Stop':>5} | {'AUG':>4} | OK")
print("=" * 95)

passed = []
for pos, seq, score in candidates:
    gc = gc_content(seq)
    homo = has_homopolymer(seq)
    stop_flag, codons = has_stop_in_critical_zone(seq)
    aug = has_internal_aug(seq)

    ok_gc = 0.40 <= gc <= 0.60
    passes = ok_gc and not homo and not stop_flag and not aug

    mark = "✓" if passes else "✗"
    homo_str = "yes" if homo else "no"
    stop_str = "yes" if stop_flag else "no"
    aug_str = "yes" if aug else "no"

    print(f"{pos:>5} | {seq} | {score:.3f} | {gc:.2f} | {homo_str:>12} | {stop_str:>5} | {aug_str:>4} | {mark}")

    if passes:
        passed.append((pos, seq, score))

print(f"\n{len(passed)} candidates pass ALL strict filters.")
final_list = passed if passed else passed_relaxed

if not final_list:
    print("No candidate passes. We'll need to adjust further.")
else:
    # The winner is the one with the highest score
    pos, seq, score = final_list[0]

    print("="*60)
    print("CHOSEN TRIGGER")
    print("="*60)
    print(f"Position in H19:      {pos}")
    print(f"Sequence (30 nt):     {seq}")
    print(f"Accessibility score:  {score:.3f}")
    print(f"GC content:           {gc_content(seq):.2f}")

    with open('chosen_trigger.txt', 'w') as f:
        f.write(seq)

    print("\nFunctional breakdown:")
    print(f"  trigger[0:18]  (pairs with stem):    {seq[0:18]}")
    print(f"  trigger[18:30] (pairs with toehold): {seq[18:30]}")
    print(f"  trigger[9:18]  (stop-free zone):     {seq[9:18]}")

    print("\n✓ Saved to chosen_trigger.txt")
04_build_switch
def revcomp(seq):
    """Reverse complement of RNA"""
    comp = {'A':'U', 'U':'A', 'G':'C', 'C':'G'}
    return ''.join(comp[b] for b in reversed(seq))

with open('chosen_trigger.txt') as f:
    trigger = f.read().strip()

assert len(trigger) == 30, f"Trigger must be 30 nt, got {len(trigger)}"
print(f"Trigger loaded: {trigger} ({len(trigger)} nt)")
# Toehold a (12 nt): RC of the last 12 nt of the trigger
a_toehold = revcomp(trigger[18:30])

# Ascending stem b (18 nt): RC of the first 18 nt of the trigger
b_asc = revcomp(trigger[0:18])

# Loop (11 nt) with strong Shine-Dalgarno RBS (AGGAG embedded)
# From Green 2014 switch #2
loop = "AACAGAGGAGA"
assert len(loop) == 11

# Desc_top (6 nt before AUG): pairs with the last 6 nt of b_asc
desc_top = revcomp(b_asc[-6:])

# AUG bulge (3 nt unpaired)
aug = "AUG"

# Desc_bot (9 nt after AUG): pairs with the first 9 nt of b_asc
desc_bot = revcomp(b_asc[:9])

# Check that desc_bot has NO stop codons (read 5'→3', in frame with AUG)
desc_codons = [desc_bot[0:3], desc_bot[3:6], desc_bot[6:9]]
stops = {'UAA', 'UAG', 'UGA'}
assert not any(c in stops for c in desc_codons), \
    f"STOP in desc_bot! Codons: {desc_codons}. Need to pick another candidate."
print(f"✓ desc_bot stop-free. Codons: {desc_codons}")

# Green 2014: encodes N-L-A-A-A-Q-K
linker = "AACCUGGCGGCAGCGCAAAAG"
assert len(linker) == 21

# Check that linker has no stops (in frame with AUG)
linker_codons = [linker[i:i+3] for i in range(0, 21, 3)]
assert not any(c in stops for c in linker_codons), f"STOP in linker: {linker_codons}"
print(f"✓ linker stop-free. Codons: {linker_codons}")

print("\n" + "="*60)
print("SWITCH PARTS")
print("="*60)
print(f"  a_toehold  (12 nt):  {a_toehold}")
print(f"  b_asc      (18 nt):  {b_asc}")
print(f"  loop+RBS   (11 nt):  {loop}")
print(f"  desc_top    (6 nt):  {desc_top}")
print(f"  AUG         (3 nt):  {aug}")
print(f"  desc_bot    (9 nt):  {desc_bot}")
print(f"  linker     (21 nt):  {linker}")
# Regulatory part of the switch
regulatory_switch = a_toehold + b_asc + loop + desc_top + aug + desc_bot + linker
print(f"Regulatory switch: {len(regulatory_switch)} nt")
print(f"Expected: 12 + 18 + 11 + 6 + 3 + 9 + 21 = {12+18+11+6+3+9+21}")
print(f"\nFull sequence (regulatory):\n{regulatory_switch}")

with open('regulatory_switch.txt', 'w') as f:
    f.write(regulatory_switch)
print(f"\n✓ Saved to regulatory_switch.txt")
initial_orf = aug + desc_bot + linker
print(f"Initial ORF (without reporter, {len(initial_orf)} nt):")
print(f"  {initial_orf}")
print(f"\nTranslated codons:")

codons = [initial_orf[i:i+3] for i in range(0, len(initial_orf), 3)]

codon_table = {
    'AUG':'M', 'AAA':'K', 'AAG':'K', 'AAU':'N', 'AAC':'N',
    'ACA':'T', 'ACC':'T', 'ACG':'T', 'ACU':'T',
    'AGA':'R', 'AGG':'R', 'AGU':'S', 'AGC':'S',
    'AUA':'I', 'AUC':'I', 'AUU':'I',
    'CAA':'Q', 'CAG':'Q', 'CAU':'H', 'CAC':'H',
    'CCA':'P', 'CCC':'P', 'CCG':'P', 'CCU':'P',
    'CGA':'R', 'CGG':'R', 'CGU':'R', 'CGC':'R',
    'CUA':'L', 'CUG':'L', 'CUU':'L', 'CUC':'L',
    'GAA':'E', 'GAG':'E', 'GAU':'D', 'GAC':'D',
    'GCA':'A', 'GCC':'A', 'GCG':'A', 'GCU':'A',
    'GGA':'G', 'GGG':'G', 'GGU':'G', 'GGC':'G',
    'GUA':'V', 'GUG':'V', 'GUU':'V', 'GUC':'V',
    'UAA':'*', 'UAG':'*', 'UGA':'*',
    'UAU':'Y', 'UAC':'Y',
    'UCA':'S', 'UCC':'S', 'UCG':'S', 'UCU':'S',
    'UGG':'W', 'UGU':'C', 'UGC':'C',
    'UUA':'L', 'UUG':'L', 'UUU':'F', 'UUC':'F',
}

aas = []
for c in codons:
    aa = codon_table.get(c, '?')
    aas.append(aa)
    print(f"  {c}{aa}")

peptide = ''.join(aas)
print(f"\nN-terminal added to sfGFP: {peptide}")

if '*' in peptide:
    print("STOP CODON PRESENT! This will truncate translation.")
else:
    print("No stops. The ribosome will read through to sfGFP without issue."
05_validate_structure
from nupack import Strand, Tube, Model, mfe, complex_concentrations, SetSpec
import numpy as np

with open('regulatory_switch.txt') as f:
    switch_seq = f.read().strip()
with open('chosen_trigger.txt') as f:
    trigger_seq = f.read().strip()

print(f"Switch:  {switch_seq}")
print(f"         ({len(switch_seq)} nt)")
print(f"Trigger: {trigger_seq}")
print(f"         ({len(trigger_seq)} nt)")

model = Model(material='rna', celsius=37)
print("\n✓ Model configured (RNA, 37 °C)")
print("="*70)
print("TEST 1: MFE structure of the switch ALONE (no trigger)")
print("="*70)

switch_result = mfe(strands=[switch_seq], model=model)
switch_structure = str(switch_result[0].structure)
switch_energy = switch_result[0].energy

print(f"\nSequence:  {switch_seq}")
print(f"Structure: {switch_structure}")
print(f"ΔG = {switch_energy:.2f} kcal/mol")

# Annotate functional regions under the structure
# The switch has: G(1) + toehold(12) + asc_stem(18) + loop(11) + desc_top(6) + AUG(3) + desc_bot(9) + linker(21)
G_extra = 1
N_a = 12
N_b = 18
N_loop = 11
N_desc_top = 6
N_AUG = 3
N_desc_bot = 9
N_linker = 21

regions = []
regions.append(('G', 0, G_extra))
regions.append(('toehold', G_extra, G_extra+N_a))
regions.append(('asc_stem', G_extra+N_a, G_extra+N_a+N_b))
regions.append(('loop+RBS', G_extra+N_a+N_b, G_extra+N_a+N_b+N_loop))
regions.append(('desc_top', G_extra+N_a+N_b+N_loop, G_extra+N_a+N_b+N_loop+N_desc_top))
regions.append(('AUG', G_extra+N_a+N_b+N_loop+N_desc_top, G_extra+N_a+N_b+N_loop+N_desc_top+N_AUG))
regions.append(('desc_bot', G_extra+N_a+N_b+N_loop+N_desc_top+N_AUG, G_extra+N_a+N_b+N_loop+N_desc_top+N_AUG+N_desc_bot))
regions.append(('linker', G_extra+N_a+N_b+N_loop+N_desc_top+N_AUG+N_desc_bot, len(switch_seq)))

print("\nRegion breakdown:")
for name, start, end in regions:
    region_struct = switch_structure[start:end]
    region_seq = switch_seq[start:end]
    n_paired = sum(1 for c in region_struct if c in '()')
    n_unpaired = region_struct.count('.')
    print(f"  {name:>10} ({start:>2}-{end-1:>2}): {region_seq}")
    print(f"  {'':>10}  {'':>2}     {region_struct}{n_paired} paired, {n_unpaired} unpaired")
print("="*70)
print("TEST 2: MFE structure of the SWITCH + TRIGGER complex")
print("="*70)

duplex_result = mfe(strands=[switch_seq, trigger_seq], model=model)
duplex_structure = str(duplex_result[0].structure)
duplex_energy = duplex_result[0].energy

print(f"\nDuplex structure:")
print(f"  {duplex_structure}")
print(f"\nΔG duplex = {duplex_energy:.2f} kcal/mol")
print(f"ΔG switch alone = {switch_energy:.2f} kcal/mol")
print(f"ΔΔG (duplex - switch alone) = {duplex_energy - switch_energy:.2f} kcal/mol")
print(f"  (the more negative, the stronger the drive to open the switch)")
print("="*70)
print("TEST 3: Equilibrium concentrations (100 nM each)")
print("="*70)

A = Strand(switch_seq, name='switch')
B = Strand(trigger_seq, name='trigger')

t1 = Tube(
    strands={A: 100e-9, B: 100e-9},  # 100 nM each
    complexes=SetSpec(max_size=2),
    name='reaction'
)

from nupack import tube_analysis
analysis = tube_analysis(tubes=[t1], model=model, compute=['mfe'])
print(analysis)
results results

All Aim 1 objectives are done: trigger identified, switch tested, a validated STAT3 binder and the constructs ready for synthesis by Twist.

The next step is to synthesize the constructs through Ginkgo, validate activation in a cell-free System, and then move into endometriotic cell lines.

Long-term vision is breaking the cycle of regrowth and repeated surgeries without affecting normal tisssue.


Bibliography

[1] M. Sahni and E. S. Day, “Nanotechnologies for the detection and treatment of endometriosis,” Front. Biomater. Sci., vol. 2, Nov. 2023, doi: 10.3389/fbiom.2023.1279358.

[2] Masferrer-Ferragutcasas, C., Delgado-Gil, R. & Colas, E. Rethinking endometriosis recurrence: from clinical challenge to biological opportunity. npj Womens Health 4, 4 (2026). https://doi.org/10.1038/s44294-026-00128-9

[3] “Endometriosis.” Accessed: Feb. 08, 2026. [Online]. Available: https://medlineplus.gov/endometriosis.html

[4] D. Winkelvoß et al., “Molecular features defining the efficiency of bioPROTACs,” Commun. Biol., vol. 8, no. 1, p. 946, Jun. 2025, doi: 10.1038/s42003-025-08352-w.

Group Final Project

cover image cover image