openpharma / crmPack

Object-Oriented Implementation of CRM Designs
https://openpharma.github.io/crmPack/
20 stars 9 forks source link

`stopTrial-StoppingOrdinal` fails when `StoppingOrdinal` rules are nested #859

Open Puzzled-Face opened 6 days ago

Puzzled-Face commented 6 days ago

stopTrial-StoppingOrdinal fails when StoppingOrdinal rules are nested within the overall stopping rule.

No error occurs with the design returned by .DefaultDesignOrdinal() because its stopping rule is given by

my_stopping1 <- StoppingMinCohorts(nCohorts = 3)
my_stopping2 <- StoppingTargetProb(
  target = c(0.2, 0.35),
  prob = 0.5
)
my_stopping3 <- StoppingMinPatients(nPatients = 20)
my_stopping <- StoppingOrdinal(1L, (my_stopping1 & my_stopping2) | my_stopping3)

Note that the overall rule is a StoppingOrdinal, but none of its components are. To demonstrate:

design <- .DefaultDesignOrdinal()
samples <- mcmc(design@data, design@model, .DefaultMcmcOptions())

answer <- stopTrial(
  stopping = design@stopping,
  dose = Inf,
  samples = samples,
  model = design@model,
  data = design@data
)

attributes(answer) <- NULL
answer
[1] FALSE

But an overall stopping rule that is (at least partially) composed of StoppingOrdinals is perfectly possible:

design@stopping <- StoppingOrdinal(
  1L, # Ignored
  StoppingAny(
    list(
      StoppingOrdinal(1L, StoppingTargetProb(target = c(0.2, 0.4), prob = 0.5)),
      StoppingOrdinal(2L, StoppingTargetProb(target = c(0.0, 0.1), prob = 0.9))
    )
  )
)

Here, we stop the trial if, at the next dose, the chance that the probability of grade 1 toxicity is in the range [0.2, 0.4) is at least 0.5 or the chance that the probability of a grade 2 toxicity is less than 0.1 is at least 90%.

After which,

answer <- stopTrial(
  stopping = design@stopping,
  dose = Inf,
  samples = samples,
  model = design@model,
  data = design@data
)
Error in FUN(X[[i]], ...) :
StoppingOrdinal objects can only be used with LogisticLogNormalOrdinal models and DataOrdinal data objects. In this case, the model is a 'LogisticLogNormal' object and the data is in a Data object.

The problem arises because stopTrial-StoppingOrdinal simply converts the model and data objects it is passed to their binary equivalents and then delegates to the appropriate stopTrial method without checking whether any subordinate Stopping rules are themselves StoppingOrdinals. The situation is relatively complicated because the nesting may not be direct. For example:

stoppingParticipantCount <- StoppingMinPatients(
  nPatients = 60, 
  report_label = "Participant count"
)
stoppingToxic <- StoppingMissingDose(report_label = "All toxic")
stoppingFutility <- StoppingAny(
  list(stoppingParticipantCount, stoppingToxic), 
  report_label = "Futility"
)

stoppingPatientsAtMTD <- StoppingPatientsNearDose(
  nPatients = 20L, 
  percentage = 0, 
  report_label = "Patients at MTD"
)
stoppingSafeDLAE <- StoppingOrdinal(
  1L, 
  StoppingTargetProb(
    target = c(0.0, 0.2), 
    prob = 0.8, 
    report_label = "DLAE-safe"
  )
)
stoppingSafeCRS <- StoppingOrdinal(
  2L, 
  StoppingTargetProb(
    target = c(0.0, 0.05), 
    prob = 0.8, 
    report_label = "CRS-safe"
  )
)

stoppingSuccess <- StoppingAll(
  list(
    stoppingPatientsAtMTD,
    stoppingSafeDLAE,
    stoppingSafeCRS
  ),
  report_label = "Stopping success"
)
trialStopping <- StoppingOrdinal(1L, StoppingAny(list(stoppingFutility, stoppingSuccess)))

We should also check that we don't have similar issues with other rules (eg size-CohortSizeMin and size-CohortSizeMax; maxDose-IncrementsMin etc).

Puzzled-Face commented 6 days ago

Possible solution:

danielinteractive commented 1 day ago

Thanks @Puzzled-Face , I think a principled solution could maybe be to add corresponding stopTrial methods with DataOrdinal signature parts to all relevant Stopping classes. Then, the required "conversion" to binary model and data would only happen at the very end when it is needed.