Skip to content

Crossover Design

Comprehensive guide for crossover clinical trial simulation with within-subject comparisons.


Overview

Crossover designs allow each subject to receive multiple treatments in different periods, enabling within-subject comparisons with reduced variability.

using NeoPKPD

design = CrossoverDesign(
    n_periods = 2,
    sequences = [["Test", "Reference"], ["Reference", "Test"]],
    washout_days = 14
)

CrossoverDesign Structure

struct CrossoverDesign <: TrialDesign
    n_periods::Int                    # Number of treatment periods
    n_sequences::Int                  # Number of sequences
    sequences::Vector{Vector{String}} # Treatment sequences
    washout_days::Int                 # Washout period between treatments
    period_duration_days::Int         # Duration of each period
end

Standard Designs

2×2 Crossover

# Classic AB/BA design
design = crossover_2x2(
    treatments = ["Test", "Reference"],
    washout_days = 14
)

# Equivalent to:
design = CrossoverDesign(
    n_periods = 2,
    sequences = [
        ["Test", "Reference"],    # Sequence 1: TR
        ["Reference", "Test"]     # Sequence 2: RT
    ],
    washout_days = 14
)

3×3 Latin Square

# Three treatments, three periods
design = crossover_3x3(
    treatments = ["A", "B", "C"],
    washout_days = 7
)

# Sequences: ABC, BCA, CAB (Latin square)

Williams Design

# Balanced for first-order carryover
design = williams_design(
    n_treatments = 2,
    washout_days = 14
)
# Returns 2×2 design

design = williams_design(
    n_treatments = 3,
    washout_days = 7
)
# Returns 6 sequences (3×6 design)

design = williams_design(
    n_treatments = 4,
    washout_days = 7
)
# Returns 4 sequences (4×4 design)

Replicate Designs

# 2×4 replicate (TRTR/RTRT)
design = replicate_crossover_2x4(
    treatments = ["Test", "Reference"],
    washout_days = 7
)

# Sequences:
# TRTR, RTRT

# 3×3 partial replicate
design = partial_replicate_3x3(
    treatments = ["Test", "Reference"],
    washout_days = 7
)

# Sequences:
# TRR, RTR, RRT

Trial Specification

Complete Crossover Setup

using NeoPKPD

# Design
design = crossover_2x2(
    treatments = ["Test", "Reference"],
    washout_days = 14
)

# Dosing regimens for each treatment
regimens = Dict(
    "Test" => dosing_single(dose=100.0),
    "Reference" => dosing_single(dose=100.0)
)

# Population (equal allocation to sequences)
spec = healthy_volunteer_spec()
population = generate_virtual_population(spec, 24)  # 12 per sequence

# PK model
model = TwoCompOral()
params = TwoCompOralParams(
    Ka = 1.5,
    CL = 10.0,
    V1 = 50.0,
    Q = 5.0,
    V2 = 100.0
)

# IIV
omega = OmegaMatrix([
    0.09 0.0;
    0.0  0.04
])

# Formulation effect (Test may have different Ka)
formulation_effects = Dict(
    "Test" => Dict(:Ka => 1.0),      # No change
    "Reference" => Dict(:Ka => 1.0)  # Baseline
)

# Trial spec
trial = CrossoverTrialSpec(
    name = "BE Crossover Study",
    design = design,
    regimens = regimens,
    population = population,
    pk_model = model,
    pk_params = params,
    omega = omega,
    sigma = 0.1,
    formulation_effects = formulation_effects,
    observation_times = [0.0, 0.25, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 12.0, 24.0]
)

Simulation

Running Crossover Trial

result = simulate_crossover_trial(trial, seed=42)

# Access by period
for period in 1:result.n_periods
    println("Period $period:")
    for treatment in result.treatments
        period_data = result.period_data[period, treatment]
        println("  $treatment: mean AUC = $(period_data.mean_auc)")
    end
end

# Access by subject
for subject in result.subjects
    println("Subject $(subject.id):")
    for (period, treatment) in enumerate(subject.sequence)
        println("  Period $period ($treatment): AUC = $(subject.auc[period])")
    end
end

Washout Period

Configuring Washout

# Standard washout (5 half-lives recommended)
design = crossover_2x2(
    treatments = ["Test", "Reference"],
    washout_days = 14  # Adequate for drug with t½ ~ 3 days
)

# Long-acting drug
design = crossover_2x2(
    treatments = ["Test", "Reference"],
    washout_days = 28  # For drug with t½ ~ 5-6 days
)

Washout Verification

# Check washout adequacy
t_half = 3.0  # days
recommended_washout = 5 * t_half
actual_washout = design.washout_days

if actual_washout >= recommended_washout
    println("✓ Washout adequate ($(actual_washout)$(recommended_washout) days)")
else
    println("⚠ Washout may be insufficient")
end

Period and Sequence Effects

Testing Period Effect

result = simulate_crossover_trial(trial, seed=42)

period_effect = test_period_effect(result)

println("Period effect p-value: $(period_effect.pvalue)")
if period_effect.pvalue < 0.05
    println("⚠ Significant period effect detected")
    println("  Period 1 mean: $(period_effect.period1_mean)")
    println("  Period 2 mean: $(period_effect.period2_mean)")
else
    println("✓ No significant period effect")
end

Testing Sequence Effect

sequence_effect = test_sequence_effect(result)

println("Sequence effect p-value: $(sequence_effect.pvalue)")
if sequence_effect.pvalue < 0.05
    println("⚠ Significant sequence (carryover) effect detected")
else
    println("✓ No significant sequence effect")
end

Within-Subject Variability

Calculating Intra-Subject CV

result = simulate_crossover_trial(trial, seed=42)

# For replicate designs
cv_within = compute_within_subject_cv(result, :auc)

println("Within-subject CV (AUC): $(cv_within * 100)%")

# Classification
if cv_within < 0.30
    println("Standard variability")
elseif cv_within < 0.40
    println("Moderate variability")
else
    println("Highly variable drug (HVD)")
end

Subject-by-Formulation Interaction

# For replicate designs
sbf = test_subject_by_formulation(result)

println("Subject × Formulation p-value: $(sbf.pvalue)")
if sbf.pvalue < 0.05
    println("⚠ Significant subject-by-formulation interaction")
end

Bioequivalence Analysis

Standard BE Assessment

result = simulate_crossover_trial(trial, seed=42)

# Extract paired data
test_auc = result.treatment_data["Test"].auc_values
ref_auc = result.treatment_data["Reference"].auc_values

# Assess BE
be_auc = assess_bioequivalence(
    test = test_auc,
    reference = ref_auc,
    theta1 = 0.80,
    theta2 = 1.25,
    alpha = 0.05
)

println("=== AUC Bioequivalence ===")
println("GMR: $(round(be_auc.gmr * 100, digits=2))%")
println("90% CI: [$(round(be_auc.ci_lower * 100, digits=2))%, $(round(be_auc.ci_upper * 100, digits=2))%]")
println("Within-subject CV: $(round(be_auc.cv_within * 100, digits=1))%")
println("BE demonstrated: $(be_auc.is_bioequivalent)")

Multiple Endpoints

# Assess both AUC and Cmax
endpoints = [:auc, :cmax]
be_results = Dict()

for endpoint in endpoints
    test_vals = getfield(result.treatment_data["Test"], endpoint * :_values)
    ref_vals = getfield(result.treatment_data["Reference"], endpoint * :_values)

    be_results[endpoint] = assess_bioequivalence(
        test = test_vals,
        reference = ref_vals
    )
end

# Overall BE requires both to pass
overall_be = all(r.is_bioequivalent for r in values(be_results))
println("Overall BE: $(overall_be ? "PASS" : "FAIL")")

Highly Variable Drugs

Reference-Scaled BE (FDA RSABE)

# For HVD with CV > 30%
rsabe_result = rsabe_analysis(
    result,
    regulatory = :FDA
)

println("Reference CV: $(rsabe_result.cv_reference * 100)%")
println("Scaling applied: $(rsabe_result.scaling_applied)")
println("RSABE criterion met: $(rsabe_result.criterion_met)")
println("Point estimate constraint: $(rsabe_result.point_estimate_ok)")
println("RSABE conclusion: $(rsabe_result.is_bioequivalent)")

ABEL (EMA)

# Average Bioequivalence with Expanding Limits
abel_result = abel_analysis(
    result,
    regulatory = :EMA
)

println("Reference CV: $(abel_result.cv_reference * 100)%")
println("Widened limits: [$(abel_result.lower_limit*100)%, $(abel_result.upper_limit*100)%]")
println("ABEL conclusion: $(abel_result.is_bioequivalent)")

Complete Example

using NeoPKPD

# ===================================
# Bioequivalence Crossover Study
# ===================================

# 1. Design: 2×2 crossover
design = crossover_2x2(
    treatments = ["Test", "Reference"],
    washout_days = 14
)

# 2. Dosing: Single dose 500 mg
regimens = Dict(
    "Test" => dosing_single(dose=500.0),
    "Reference" => dosing_single(dose=500.0)
)

# 3. Population: 24 healthy volunteers
spec = healthy_volunteer_spec()
population = generate_virtual_population(spec, 24)

# 4. PK Model
model = TwoCompOral()
params = TwoCompOralParams(
    Ka = 1.2,
    CL = 8.0,
    V1 = 50.0,
    Q = 3.0,
    V2 = 80.0
)

# 5. Variability
omega = OmegaMatrix([
    0.09 0.02 0.0;    # CL
    0.02 0.04 0.0;    # V1
    0.0  0.0  0.16    # Ka
])

# Formulation effect: Test has 5% higher Ka (faster absorption)
formulation_effects = Dict(
    "Test" => Dict(:Ka => 1.05),
    "Reference" => Dict(:Ka => 1.0)
)

# 6. Trial spec
trial = CrossoverTrialSpec(
    name = "BE Study - Drug X",
    design = design,
    regimens = regimens,
    population = population,
    pk_model = model,
    pk_params = params,
    omega = omega,
    sigma = 0.1,
    formulation_effects = formulation_effects,
    observation_times = [0.0, 0.25, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 12.0, 24.0],
    endpoints = [:cmax, :auc_0_t, :auc_0_inf, :tmax]
)

# 7. Simulate
println("Simulating crossover trial...")
result = simulate_crossover_trial(trial, seed=12345)

# 8. Report
println("\n" * "=" ^ 60)
println("BIOEQUIVALENCE STUDY RESULTS")
println("=" ^ 60)

println("\n--- Study Summary ---")
println("Subjects enrolled: $(result.n_enrolled)")
println("Subjects completed: $(result.n_completed)")
println("Design: $(result.design.n_periods)×$(result.design.n_sequences) crossover")
println("Washout: $(result.design.washout_days) days")

println("\n--- PK Results by Treatment ---")
for treatment in ["Test", "Reference"]
    data = result.treatment_data[treatment]
    println("\n$treatment:")
    println("  Cmax:     $(round(data.mean_cmax, digits=2)) ± $(round(data.sd_cmax, digits=2))")
    println("  Tmax:     $(round(data.mean_tmax, digits=2)) h")
    println("  AUC0-t:   $(round(data.mean_auc_0_t, digits=1)) ± $(round(data.sd_auc_0_t, digits=1))")
    println("  AUC0-inf: $(round(data.mean_auc_0_inf, digits=1)) ± $(round(data.sd_auc_0_inf, digits=1))")
end

# 9. Period/Sequence effects
println("\n--- Effect Tests ---")
period = test_period_effect(result)
sequence = test_sequence_effect(result)
println("Period effect p-value: $(round(period.pvalue, digits=4))")
println("Sequence effect p-value: $(round(sequence.pvalue, digits=4))")

# 10. Within-subject CV
cv_auc = compute_within_subject_cv(result, :auc_0_inf)
cv_cmax = compute_within_subject_cv(result, :cmax)
println("\n--- Within-Subject Variability ---")
println("CV (AUC): $(round(cv_auc * 100, digits=1))%")
println("CV (Cmax): $(round(cv_cmax * 100, digits=1))%")

# 11. Bioequivalence assessment
println("\n--- Bioequivalence Assessment ---")

be_auc = assess_bioequivalence(
    result.treatment_data["Test"].auc_0_inf_values,
    result.treatment_data["Reference"].auc_0_inf_values
)

be_cmax = assess_bioequivalence(
    result.treatment_data["Test"].cmax_values,
    result.treatment_data["Reference"].cmax_values
)

println("\nAUC0-inf:")
println("  GMR: $(round(be_auc.gmr * 100, digits=2))%")
println("  90% CI: [$(round(be_auc.ci_lower * 100, digits=2))%, $(round(be_auc.ci_upper * 100, digits=2))%]")
println("  BE: $(be_auc.is_bioequivalent ? "PASS" : "FAIL")")

println("\nCmax:")
println("  GMR: $(round(be_cmax.gmr * 100, digits=2))%")
println("  90% CI: [$(round(be_cmax.ci_lower * 100, digits=2))%, $(round(be_cmax.ci_upper * 100, digits=2))%]")
println("  BE: $(be_cmax.is_bioequivalent ? "PASS" : "FAIL")")

# 12. Overall conclusion
overall = be_auc.is_bioequivalent && be_cmax.is_bioequivalent
println("\n" * "=" ^ 60)
println("OVERALL CONCLUSION: Bioequivalence $(overall ? "DEMONSTRATED" : "NOT DEMONSTRATED")")
println("=" ^ 60)

See Also