From 21d74f50e6f3b4ecac214dc4bd791a05d16e4c49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:37:31 +0000 Subject: [PATCH 1/7] Initial plan From 5f5b6bbe3b43823472bd6dc1a619cf0fb4dabdcf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:46:26 +0000 Subject: [PATCH 2/7] Add optimizer control settings for classical LMM and GLMM Co-authored-by: FBartos <38475991+FBartos@users.noreply.github.com> --- R/MixedModelsCommon.R | 90 +++++++++++++++++++++++--- R/MixedModelsMessages.R | 2 +- inst/qml/common/MixedModelsOptions.qml | 69 ++++++++++++++++++++ 3 files changed, 152 insertions(+), 9 deletions(-) diff --git a/R/MixedModelsCommon.R b/R/MixedModelsCommon.R index c8bc4f77..24f0e3e2 100644 --- a/R/MixedModelsCommon.R +++ b/R/MixedModelsCommon.R @@ -411,20 +411,89 @@ return(dataset) } -.mixedInterceptML <- function(formula, dataset, type, family = NULL) { +.mmCreateOptimizerControl <- function(type, options) { + # Create optimizer control objects based on user settings + # Returns default controls if options is NULL or optimizer settings not specified + + if (is.null(options) || is.null(options$optimizerMethod) || options$optimizerMethod == "default") { + # Return default controls + if (type == "LMM") { + return(lmerTest::lmerControl()) + } else if (type == "GLMM") { + return(lme4::glmerControl()) + } + } + + # Build control arguments from user options + control_args <- list() + + # Set optimizer method + if (!is.null(options$optimizerMethod) && options$optimizerMethod != "default") { + control_args$optimizer <- options$optimizerMethod + } + + # Set convergence checking + if (!is.null(options$optimizerCheckConv)) { + control_args$check.conv.singular <- options$optimizerCheckConv + control_args$check.conv.grad <- options$optimizerCheckConv + control_args$check.conv.hess <- options$optimizerCheckConv + } + + # Build optimizer control list + optCtrl <- list() + + if (!is.null(options$optimizerMaxIter)) { + optCtrl$maxfun <- options$optimizerMaxIter + optCtrl$maxit <- options$optimizerMaxIter + } + + if (!is.null(options$optimizerMaxFunEvals)) { + optCtrl$maxfun <- options$optimizerMaxFunEvals + } + + if (!is.null(options$optimizerTolerance)) { + optCtrl$ftol_abs <- options$optimizerTolerance + optCtrl$xtol_abs <- options$optimizerTolerance + optCtrl$reltol <- options$optimizerTolerance + } + + if (length(optCtrl) > 0) { + control_args$optCtrl <- optCtrl + } + + # Create appropriate control object + if (type == "LMM") { + return(do.call(lmerTest::lmerControl, control_args)) + } else if (type == "GLMM") { + return(do.call(lme4::glmerControl, control_args)) + } + + # Fallback to default + if (type == "LMM") { + return(lmerTest::lmerControl()) + } else { + return(lme4::glmerControl()) + } +} + +.mixedInterceptML <- function(formula, dataset, type, family = NULL, options = NULL) { # this is a simple function to fit a mixed-effects model with a fixed intercept only # because afex does not allow those models for GLMMs (or LMMs with LRT/PB) if (type == "LMM") { + control <- .mmCreateOptimizerControl(type, options) fit <- lmerTest::lmer( formula = formula, data = dataset, - REML = FALSE + REML = FALSE, + control = control ) } else if (type == "GLMM") { + control <- .mmCreateOptimizerControl(type, options) fit <- lme4::glmer( formula = formula, data = dataset, - family = family + family = family, + control = control ) } @@ -504,7 +573,8 @@ .mixedInterceptML( formula = as.formula(modelFormula$modelFormula), data = dataset, - type = "LMM" + type = "LMM", + options = options )) else model <- try( @@ -515,7 +585,8 @@ method = .mmGetTestMethod(options), test_intercept = .mmGetTestIntercept(options), args_test = list(nsim = options$bootstrapSamples), - check_contrasts = FALSE + check_contrasts = FALSE, + control = .mmCreateOptimizerControl("LMM", options) )) } else if (type == "GLMM") { # needs to be evaluated in the global environment @@ -540,7 +611,8 @@ args_test = list(nsim = options$bootstrapSamples), check_contrasts = FALSE, family = glmmFamily, - weights = glmmWeight + weights = glmmWeight, + control = .mmCreateOptimizerControl("GLMM", options) )) } else { if (.isInterceptML(options)) @@ -549,7 +621,8 @@ formula = as.formula(modelFormula$modelFormula), data = dataset, family = glmmFamily, - type = "GLMM" + type = "GLMM", + options = options )) else model <- try( @@ -562,7 +635,8 @@ args_test = list(nsim = options$bootstrapSamples), check_contrasts = FALSE, #start = start, - family = glmmFamily + family = glmmFamily, + control = .mmCreateOptimizerControl("GLMM", options) )) } } diff --git a/R/MixedModelsMessages.R b/R/MixedModelsMessages.R index 721809fb..8b7d5f8a 100644 --- a/R/MixedModelsMessages.R +++ b/R/MixedModelsMessages.R @@ -237,7 +237,7 @@ else if (grepl("Downdated VtV is not positive definite", error)) return(gettext("The optimizer failed to find a solution. Probably due to scaling issues quasi-separation in the data. Try rescaling or removing some of the predictors.")) else if (grepl("did not converge in (maxit) iterations", error)) - return(gettext("The optimizer failed to find a solution in the specified number of iterations. (JASP currently does not support modifying the optimizer settings.)")) + return(gettext("The optimizer failed to find a solution in the specified number of iterations. Try adjusting the optimizer settings in the Advanced Options section.")) else if (grepl("unexpected symbol", error)) # triggered by right hand side formula larger than 500 characters -- the maximum length return(gettext("The model formula is probably too long. Try shortening variable names.")) else diff --git a/inst/qml/common/MixedModelsOptions.qml b/inst/qml/common/MixedModelsOptions.qml index 3edeeb97..d937e932 100644 --- a/inst/qml/common/MixedModelsOptions.qml +++ b/inst/qml/common/MixedModelsOptions.qml @@ -121,4 +121,73 @@ Section label: qsTr("Vovk-Sellke maximum p-ratio") } + Section + { + title: qsTr("Advanced Options") + expanded: false + visible: allMethodOptions // Only show for classical models (LMM/GLMM) + + Group + { + title: qsTr("Optimizer Settings") + + DropDown + { + name: "optimizerMethod" + label: qsTr("Optimizer") + info: qsTr("Optimization algorithm used for parameter estimation") + values: + [ + { label: qsTr("Default"), value: "default"}, + { label: "nlminb", value: "nlminb"}, + { label: "BFGS", value: "BFGS"}, + { label: "Nelder-Mead", value: "Nelder_Mead"}, + { label: "bobyqa", value: "bobyqa"} + ] + } + + IntegerField + { + name: "optimizerMaxIter" + label: qsTr("Maximum iterations") + info: qsTr("Maximum number of iterations for the optimizer") + defaultValue: 10000 + min: 100 + max: 1000000 + fieldWidth: 80 * jaspTheme.uiScale + } + + IntegerField + { + name: "optimizerMaxFunEvals" + label: qsTr("Maximum function evaluations") + info: qsTr("Maximum number of function evaluations") + defaultValue: 100000 + min: 1000 + max: 10000000 + fieldWidth: 80 * jaspTheme.uiScale + } + + DoubleField + { + name: "optimizerTolerance" + label: qsTr("Convergence tolerance") + info: qsTr("Convergence tolerance for parameter estimates") + defaultValue: 1e-6 + min: 1e-12 + max: 1e-3 + decimals: 12 + fieldWidth: 100 * jaspTheme.uiScale + } + + CheckBox + { + name: "optimizerCheckConv" + label: qsTr("Check convergence") + info: qsTr("Check for convergence warnings and errors") + checked: true + } + } + } + } From 45ed33dca29726166cadcf1105e1f7888321f310 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:51:59 +0000 Subject: [PATCH 3/7] Add tests for optimizer settings and finalize implementation Co-authored-by: FBartos <38475991+FBartos@users.noreply.github.com> --- R/MixedModelsCommon.R | 6 ++--- inst/qml/common/MixedModelsOptions.qml | 2 +- tests/testthat/test-mixedmodelsglmm.R | 35 ++++++++++++++++++++++++++ tests/testthat/test-mixedmodelslmm.R | 32 +++++++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/R/MixedModelsCommon.R b/R/MixedModelsCommon.R index 24f0e3e2..25b40aa1 100644 --- a/R/MixedModelsCommon.R +++ b/R/MixedModelsCommon.R @@ -418,7 +418,7 @@ if (is.null(options) || is.null(options$optimizerMethod) || options$optimizerMethod == "default") { # Return default controls if (type == "LMM") { - return(lmerTest::lmerControl()) + return(lme4::lmerControl()) } else if (type == "GLMM") { return(lme4::glmerControl()) } @@ -463,14 +463,14 @@ # Create appropriate control object if (type == "LMM") { - return(do.call(lmerTest::lmerControl, control_args)) + return(do.call(lme4::lmerControl, control_args)) } else if (type == "GLMM") { return(do.call(lme4::glmerControl, control_args)) } # Fallback to default if (type == "LMM") { - return(lmerTest::lmerControl()) + return(lme4::lmerControl()) } else { return(lme4::glmerControl()) } diff --git a/inst/qml/common/MixedModelsOptions.qml b/inst/qml/common/MixedModelsOptions.qml index d937e932..8864fae2 100644 --- a/inst/qml/common/MixedModelsOptions.qml +++ b/inst/qml/common/MixedModelsOptions.qml @@ -125,7 +125,7 @@ Section { title: qsTr("Advanced Options") expanded: false - visible: allMethodOptions // Only show for classical models (LMM/GLMM) + // Show optimizer settings for all classical models (LMM and GLMM) Group { diff --git a/tests/testthat/test-mixedmodelsglmm.R b/tests/testthat/test-mixedmodelsglmm.R index cc160fdb..e2e2109d 100644 --- a/tests/testthat/test-mixedmodelsglmm.R +++ b/tests/testthat/test-mixedmodelsglmm.R @@ -973,3 +973,38 @@ context("Generalized Linear Mixed Models") jaspTools::expect_equal_plots(testPlot, "plot-glmm-5") }) } + +### Test optimizer options for GLMM +{ + test_that("GLMM optimizer options can be set without errors", { + options <- jaspTools::analysisOptions("MixedModelsGLMM") + options$dependent <- "dependent" + options$fixedEffects <- list(list(components = "factor1")) + options$randomEffects <- list(list( + randomComponents = list(list(randomSlopes = FALSE, value = "grouping")) + )) + options$family <- "binomial" + options$link <- "logit" + + # Test custom optimizer settings + options$optimizerMethod <- "bobyqa" + options$optimizerMaxIter <- 5000 + options$optimizerMaxFunEvals <- 50000 + options$optimizerTolerance <- 1e-8 + options$optimizerCheckConv <- TRUE + + # Create simple test dataset for binomial GLMM + dataset <- data.frame( + dependent = rbinom(40, 1, 0.5), + factor1 = factor(rep(c("A", "B"), each = 20)), + grouping = factor(rep(1:4, each = 10)) + ) + + # Should not error during options processing + results <- jaspTools::runAnalysis("MixedModelsGLMM", dataset = dataset, options) + + # Basic check that analysis ran and produced some output + expect_true(!is.null(results)) + expect_true(length(results$results) > 0) + }) +} diff --git a/tests/testthat/test-mixedmodelslmm.R b/tests/testthat/test-mixedmodelslmm.R index 81edf48d..9975ff13 100644 --- a/tests/testthat/test-mixedmodelslmm.R +++ b/tests/testthat/test-mixedmodelslmm.R @@ -1067,3 +1067,35 @@ context("Linear Mixed Models") }) } +### Test optimizer options +{ + test_that("Optimizer options can be set without errors", { + options <- jaspTools::analysisOptions("MixedModelsLMM") + options$dependent <- "Variable4" + options$fixedEffects <- list(list(components = "Variable1")) + options$randomEffects <- list(list( + randomComponents = list(list(randomSlopes = FALSE, value = "Variable2")) + )) + + # Test custom optimizer settings + options$optimizerMethod <- "nlminb" + options$optimizerMaxIter <- 5000 + options$optimizerMaxFunEvals <- 50000 + options$optimizerTolerance <- 1e-8 + options$optimizerCheckConv <- TRUE + + dataset <- data.frame( + Variable1 = factor(rep(c("A", "B"), each = 20)), + Variable2 = factor(rep(1:4, each = 10)), + Variable4 = rnorm(40) + ) + + # Should not error during options processing + results <- jaspTools::runAnalysis("MixedModelsLMM", dataset = dataset, options) + + # Basic check that analysis ran and produced some output + expect_true(!is.null(results)) + expect_true(length(results$results) > 0) + }) +} + From 72562215c4eef62da2c7acd22890269d212fa9e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 12:03:09 +0000 Subject: [PATCH 4/7] Refactor optimizer settings into separate Advanced.qml component Co-authored-by: FBartos <38475991+FBartos@users.noreply.github.com> --- inst/qml/MixedModelsGLMM.qml | 2 + inst/qml/MixedModelsLMM.qml | 2 + inst/qml/common/Advanced.qml | 89 ++++++++++++++++++++++++++ inst/qml/common/MixedModelsOptions.qml | 69 -------------------- 4 files changed, 93 insertions(+), 69 deletions(-) create mode 100644 inst/qml/common/Advanced.qml diff --git a/inst/qml/MixedModelsGLMM.qml b/inst/qml/MixedModelsGLMM.qml index 98de4c3e..a3c8412c 100644 --- a/inst/qml/MixedModelsGLMM.qml +++ b/inst/qml/MixedModelsGLMM.qml @@ -419,4 +419,6 @@ Form { } } + MM.Advanced {} + } diff --git a/inst/qml/MixedModelsLMM.qml b/inst/qml/MixedModelsLMM.qml index f081e783..098e623f 100755 --- a/inst/qml/MixedModelsLMM.qml +++ b/inst/qml/MixedModelsLMM.qml @@ -317,4 +317,6 @@ Form { } } + MM.Advanced {} + } diff --git a/inst/qml/common/Advanced.qml b/inst/qml/common/Advanced.qml new file mode 100644 index 00000000..2ef8fa2e --- /dev/null +++ b/inst/qml/common/Advanced.qml @@ -0,0 +1,89 @@ +// +// Copyright (C) 2013-2020 University of Amsterdam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public +// License along with this program. If not, see +// . +// +import QtQuick +import QtQuick.Layouts +import JASP +import JASP.Controls + +Section +{ + title: qsTr("Advanced Options") + expanded: false + + Group + { + title: qsTr("Optimizer Settings") + + DropDown + { + name: "optimizerMethod" + label: qsTr("Optimizer") + info: qsTr("Optimization algorithm used for parameter estimation") + values: + [ + { label: qsTr("Default"), value: "default"}, + { label: "nlminb", value: "nlminb"}, + { label: "BFGS", value: "BFGS"}, + { label: "Nelder-Mead", value: "Nelder_Mead"}, + { label: "bobyqa", value: "bobyqa"} + ] + } + + IntegerField + { + name: "optimizerMaxIter" + label: qsTr("Maximum iterations") + info: qsTr("Maximum number of iterations for the optimizer") + defaultValue: 10000 + min: 100 + max: 1000000 + fieldWidth: 80 * jaspTheme.uiScale + } + + IntegerField + { + name: "optimizerMaxFunEvals" + label: qsTr("Maximum function evaluations") + info: qsTr("Maximum number of function evaluations") + defaultValue: 100000 + min: 1000 + max: 10000000 + fieldWidth: 80 * jaspTheme.uiScale + } + + DoubleField + { + name: "optimizerTolerance" + label: qsTr("Convergence tolerance") + info: qsTr("Convergence tolerance for parameter estimates") + defaultValue: 1e-6 + min: 1e-12 + max: 1e-3 + decimals: 12 + fieldWidth: 100 * jaspTheme.uiScale + } + + CheckBox + { + name: "optimizerCheckConv" + label: qsTr("Check convergence") + info: qsTr("Check for convergence warnings and errors") + checked: true + } + } +} \ No newline at end of file diff --git a/inst/qml/common/MixedModelsOptions.qml b/inst/qml/common/MixedModelsOptions.qml index 8864fae2..3edeeb97 100644 --- a/inst/qml/common/MixedModelsOptions.qml +++ b/inst/qml/common/MixedModelsOptions.qml @@ -121,73 +121,4 @@ Section label: qsTr("Vovk-Sellke maximum p-ratio") } - Section - { - title: qsTr("Advanced Options") - expanded: false - // Show optimizer settings for all classical models (LMM and GLMM) - - Group - { - title: qsTr("Optimizer Settings") - - DropDown - { - name: "optimizerMethod" - label: qsTr("Optimizer") - info: qsTr("Optimization algorithm used for parameter estimation") - values: - [ - { label: qsTr("Default"), value: "default"}, - { label: "nlminb", value: "nlminb"}, - { label: "BFGS", value: "BFGS"}, - { label: "Nelder-Mead", value: "Nelder_Mead"}, - { label: "bobyqa", value: "bobyqa"} - ] - } - - IntegerField - { - name: "optimizerMaxIter" - label: qsTr("Maximum iterations") - info: qsTr("Maximum number of iterations for the optimizer") - defaultValue: 10000 - min: 100 - max: 1000000 - fieldWidth: 80 * jaspTheme.uiScale - } - - IntegerField - { - name: "optimizerMaxFunEvals" - label: qsTr("Maximum function evaluations") - info: qsTr("Maximum number of function evaluations") - defaultValue: 100000 - min: 1000 - max: 10000000 - fieldWidth: 80 * jaspTheme.uiScale - } - - DoubleField - { - name: "optimizerTolerance" - label: qsTr("Convergence tolerance") - info: qsTr("Convergence tolerance for parameter estimates") - defaultValue: 1e-6 - min: 1e-12 - max: 1e-3 - decimals: 12 - fieldWidth: 100 * jaspTheme.uiScale - } - - CheckBox - { - name: "optimizerCheckConv" - label: qsTr("Check convergence") - info: qsTr("Check for convergence warnings and errors") - checked: true - } - } - } - } From 736148fe4de76c0737fc4d1aa8a595ab9891bc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Wed, 10 Sep 2025 15:35:17 +0200 Subject: [PATCH 5/7] fix basic issues --- R/MixedModelsCommon.R | 56 ++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/R/MixedModelsCommon.R b/R/MixedModelsCommon.R index 25b40aa1..db020bc2 100644 --- a/R/MixedModelsCommon.R +++ b/R/MixedModelsCommon.R @@ -413,67 +413,50 @@ } .mmCreateOptimizerControl <- function(type, options) { # Create optimizer control objects based on user settings - # Returns default controls if options is NULL or optimizer settings not specified - - if (is.null(options) || is.null(options$optimizerMethod) || options$optimizerMethod == "default") { - # Return default controls - if (type == "LMM") { - return(lme4::lmerControl()) - } else if (type == "GLMM") { - return(lme4::glmerControl()) - } - } - + # Build control arguments from user options control_args <- list() - + # Set optimizer method - if (!is.null(options$optimizerMethod) && options$optimizerMethod != "default") { + if (options$optimizerMethod != "default") { control_args$optimizer <- options$optimizerMethod } - + # Set convergence checking if (!is.null(options$optimizerCheckConv)) { control_args$check.conv.singular <- options$optimizerCheckConv control_args$check.conv.grad <- options$optimizerCheckConv control_args$check.conv.hess <- options$optimizerCheckConv } - + # Build optimizer control list optCtrl <- list() - + if (!is.null(options$optimizerMaxIter)) { optCtrl$maxfun <- options$optimizerMaxIter optCtrl$maxit <- options$optimizerMaxIter } - + if (!is.null(options$optimizerMaxFunEvals)) { optCtrl$maxfun <- options$optimizerMaxFunEvals } - + if (!is.null(options$optimizerTolerance)) { optCtrl$ftol_abs <- options$optimizerTolerance optCtrl$xtol_abs <- options$optimizerTolerance optCtrl$reltol <- options$optimizerTolerance } - + if (length(optCtrl) > 0) { control_args$optCtrl <- optCtrl } - + # Create appropriate control object if (type == "LMM") { return(do.call(lme4::lmerControl, control_args)) } else if (type == "GLMM") { return(do.call(lme4::glmerControl, control_args)) } - - # Fallback to default - if (type == "LMM") { - return(lme4::lmerControl()) - } else { - return(lme4::glmerControl()) - } } .mixedInterceptML <- function(formula, dataset, type, family = NULL, options = NULL) { @@ -543,7 +526,8 @@ return(added) } .mmFitModel <- function(jaspResults, dataset, options, type = "LMM") { - +saveRDS(options, file = "C:/JASP-Packages/options.RDS") +saveRDS(dataset, file = "C:/JASP-Packages/dataset.RDS") if (!is.null(jaspResults[["mmModel"]])) return() @@ -567,6 +551,10 @@ # specify contrasts dataset <- .mmSetContrasts(dataset, options) + # the control arguments needs to be assigned outside of the call because + # forwarding the call crashes afex + lmControl <- .mmCreateOptimizerControl(type, options) + if (type == "LMM") { if (.isInterceptML(options)) model <- try( @@ -574,7 +562,8 @@ formula = as.formula(modelFormula$modelFormula), data = dataset, type = "LMM", - options = options + options = options, + control = lmControl )) else model <- try( @@ -586,7 +575,7 @@ test_intercept = .mmGetTestIntercept(options), args_test = list(nsim = options$bootstrapSamples), check_contrasts = FALSE, - control = .mmCreateOptimizerControl("LMM", options) + control = lmControl )) } else if (type == "GLMM") { # needs to be evaluated in the global environment @@ -612,7 +601,7 @@ check_contrasts = FALSE, family = glmmFamily, weights = glmmWeight, - control = .mmCreateOptimizerControl("GLMM", options) + control = lmControl )) } else { if (.isInterceptML(options)) @@ -622,7 +611,8 @@ data = dataset, family = glmmFamily, type = "GLMM", - options = options + options = options, + control = lmControl )) else model <- try( @@ -636,7 +626,7 @@ check_contrasts = FALSE, #start = start, family = glmmFamily, - control = .mmCreateOptimizerControl("GLMM", options) + control = lmControl )) } } From f347a0d04c14c1c22959c5a2af4308729e6f20b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:54:16 +0000 Subject: [PATCH 6/7] Implement optimizer-specific settings and fix title Co-authored-by: FBartos <38475991+FBartos@users.noreply.github.com> --- R/MixedModelsCommon.R | 64 +++++++++++---- inst/qml/common/Advanced.qml | 153 ++++++++++++++++++++++++++++++++--- 2 files changed, 192 insertions(+), 25 deletions(-) diff --git a/R/MixedModelsCommon.R b/R/MixedModelsCommon.R index db020bc2..8c9354e7 100644 --- a/R/MixedModelsCommon.R +++ b/R/MixedModelsCommon.R @@ -429,22 +429,56 @@ control_args$check.conv.hess <- options$optimizerCheckConv } - # Build optimizer control list + # Build optimizer-specific control list optCtrl <- list() - if (!is.null(options$optimizerMaxIter)) { - optCtrl$maxfun <- options$optimizerMaxIter - optCtrl$maxit <- options$optimizerMaxIter - } - - if (!is.null(options$optimizerMaxFunEvals)) { - optCtrl$maxfun <- options$optimizerMaxFunEvals - } - - if (!is.null(options$optimizerTolerance)) { - optCtrl$ftol_abs <- options$optimizerTolerance - optCtrl$xtol_abs <- options$optimizerTolerance - optCtrl$reltol <- options$optimizerTolerance + if (options$optimizerMethod == "Nelder_Mead") { + # Nelder-Mead specific options + if (!is.null(options$nelderMeadMaxfun) && options$nelderMeadMaxfun > 0) { + optCtrl$maxfun <- options$nelderMeadMaxfun + } + if (!is.null(options$nelderMeadFtolAbs) && options$nelderMeadFtolAbs > 0) { + optCtrl$FtolAbs <- options$nelderMeadFtolAbs + } + if (!is.null(options$nelderMeadFtolRel) && options$nelderMeadFtolRel > 0) { + optCtrl$FtolRel <- options$nelderMeadFtolRel + } + if (!is.null(options$nelderMeadXtolRel) && options$nelderMeadXtolRel > 0) { + optCtrl$XtolRel <- options$nelderMeadXtolRel + } + } else if (options$optimizerMethod == "bobyqa") { + # bobyqa specific options + if (!is.null(options$bobyqaNpt) && options$bobyqaNpt > 0) { + optCtrl$npt <- options$bobyqaNpt + } + if (!is.null(options$bobyqaRhobeg) && options$bobyqaRhobeg > 0) { + optCtrl$rhobeg <- options$bobyqaRhobeg + } + if (!is.null(options$bobyqaRhoend) && options$bobyqaRhoend > 0) { + optCtrl$rhoend <- options$bobyqaRhoend + } + if (!is.null(options$bobyqaMaxfun) && options$bobyqaMaxfun > 0) { + optCtrl$maxfun <- options$bobyqaMaxfun + } + } else if (options$optimizerMethod == "nlminb") { + # nlminb specific options + if (!is.null(options$nlminbTol) && options$nlminbTol > 0) { + optCtrl$tol <- options$nlminbTol + } + if (!is.null(options$nlminbRelTol) && options$nlminbRelTol > 0) { + optCtrl$relTol <- options$nlminbRelTol + } + if (!is.null(options$nlminbMaxit) && options$nlminbMaxit > 0) { + optCtrl$maxit <- options$nlminbMaxit + } + } else { + # Default and BFGS: use generic options + if (!is.null(options$optimizerMaxIter) && options$optimizerMaxIter > 0) { + optCtrl$maxit <- options$optimizerMaxIter + } + if (!is.null(options$optimizerTolerance) && options$optimizerTolerance > 0) { + optCtrl$reltol <- options$optimizerTolerance + } } if (length(optCtrl) > 0) { @@ -526,8 +560,6 @@ return(added) } .mmFitModel <- function(jaspResults, dataset, options, type = "LMM") { -saveRDS(options, file = "C:/JASP-Packages/options.RDS") -saveRDS(dataset, file = "C:/JASP-Packages/dataset.RDS") if (!is.null(jaspResults[["mmModel"]])) return() diff --git a/inst/qml/common/Advanced.qml b/inst/qml/common/Advanced.qml index 2ef8fa2e..e1a40a01 100644 --- a/inst/qml/common/Advanced.qml +++ b/inst/qml/common/Advanced.qml @@ -22,7 +22,7 @@ import JASP.Controls Section { - title: qsTr("Advanced Options") + title: qsTr("Advanced") expanded: false Group @@ -31,6 +31,7 @@ Section DropDown { + id: optimizerDropdown name: "optimizerMethod" label: qsTr("Optimizer") info: qsTr("Optimization algorithm used for parameter estimation") @@ -44,26 +45,159 @@ Section ] } + // Nelder-Mead specific options IntegerField { - name: "optimizerMaxIter" - label: qsTr("Maximum iterations") - info: qsTr("Maximum number of iterations for the optimizer") + name: "nelderMeadMaxfun" + label: qsTr("Maximum function evaluations") + info: qsTr("Maximum number of function evaluations allowed") defaultValue: 10000 min: 100 max: 1000000 fieldWidth: 80 * jaspTheme.uiScale + visible: optimizerDropdown.currentValue === "Nelder_Mead" + } + + DoubleField + { + name: "nelderMeadFtolAbs" + label: qsTr("Absolute function tolerance") + info: qsTr("Absolute tolerance on change in function values") + defaultValue: 1e-5 + min: 1e-12 + max: 1e-1 + decimals: 12 + fieldWidth: 100 * jaspTheme.uiScale + visible: optimizerDropdown.currentValue === "Nelder_Mead" + } + + DoubleField + { + name: "nelderMeadFtolRel" + label: qsTr("Relative function tolerance") + info: qsTr("Relative tolerance on change in function values") + defaultValue: 1e-15 + min: 1e-20 + max: 1e-5 + decimals: 20 + fieldWidth: 100 * jaspTheme.uiScale + visible: optimizerDropdown.currentValue === "Nelder_Mead" + } + + DoubleField + { + name: "nelderMeadXtolRel" + label: qsTr("Relative parameter tolerance") + info: qsTr("Relative tolerance on change in parameter values") + defaultValue: 1e-7 + min: 1e-15 + max: 1e-3 + decimals: 15 + fieldWidth: 100 * jaspTheme.uiScale + visible: optimizerDropdown.currentValue === "Nelder_Mead" + } + + // bobyqa specific options + IntegerField + { + name: "bobyqaNpt" + label: qsTr("Number of interpolation points") + info: qsTr("Number of points used to approximate the objective function") + defaultValue: 0 + min: 0 + max: 100 + fieldWidth: 80 * jaspTheme.uiScale + visible: optimizerDropdown.currentValue === "bobyqa" + } + + DoubleField + { + name: "bobyqaRhobeg" + label: qsTr("Initial trust region radius") + info: qsTr("Initial value of the trust region radius") + defaultValue: 0 + min: 0 + max: 10 + decimals: 6 + fieldWidth: 100 * jaspTheme.uiScale + visible: optimizerDropdown.currentValue === "bobyqa" + } + + DoubleField + { + name: "bobyqaRhoend" + label: qsTr("Final trust region radius") + info: qsTr("Final value of the trust region radius") + defaultValue: 0 + min: 0 + max: 1 + decimals: 10 + fieldWidth: 100 * jaspTheme.uiScale + visible: optimizerDropdown.currentValue === "bobyqa" } IntegerField { - name: "optimizerMaxFunEvals" + name: "bobyqaMaxfun" label: qsTr("Maximum function evaluations") - info: qsTr("Maximum number of function evaluations") - defaultValue: 100000 - min: 1000 - max: 10000000 + info: qsTr("Maximum number of function evaluations allowed") + defaultValue: 10000 + min: 100 + max: 1000000 + fieldWidth: 80 * jaspTheme.uiScale + visible: optimizerDropdown.currentValue === "bobyqa" + } + + // nlminb specific options + DoubleField + { + name: "nlminbTol" + label: qsTr("Tolerance") + info: qsTr("Convergence tolerance") + defaultValue: 1e-6 + min: 1e-12 + max: 1e-3 + decimals: 12 + fieldWidth: 100 * jaspTheme.uiScale + visible: optimizerDropdown.currentValue === "nlminb" + } + + DoubleField + { + name: "nlminbRelTol" + label: qsTr("Relative tolerance") + info: qsTr("Relative convergence tolerance") + defaultValue: 1e-10 + min: 1e-20 + max: 1e-5 + decimals: 20 + fieldWidth: 100 * jaspTheme.uiScale + visible: optimizerDropdown.currentValue === "nlminb" + } + + IntegerField + { + name: "nlminbMaxit" + label: qsTr("Maximum iterations") + info: qsTr("Maximum number of iterations") + defaultValue: 10000 + min: 100 + max: 1000000 + fieldWidth: 80 * jaspTheme.uiScale + visible: optimizerDropdown.currentValue === "nlminb" + } + + // Default and BFGS options (generic fallback) + IntegerField + { + name: "optimizerMaxIter" + label: qsTr("Maximum iterations") + info: qsTr("Maximum number of iterations for the optimizer") + defaultValue: 10000 + min: 100 + max: 1000000 fieldWidth: 80 * jaspTheme.uiScale + visible: optimizerDropdown.currentValue === "default" || optimizerDropdown.currentValue === "BFGS" } DoubleField @@ -76,6 +210,7 @@ Section max: 1e-3 decimals: 12 fieldWidth: 100 * jaspTheme.uiScale + visible: optimizerDropdown.currentValue === "default" || optimizerDropdown.currentValue === "BFGS" } CheckBox From ca0a2f8a5103835db3214d8a061fa2c32265716b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Wed, 10 Sep 2025 16:31:03 +0200 Subject: [PATCH 7/7] additional fixes --- R/MixedModelsCommon.R | 18 ++++---------- inst/qml/MixedModelsGLMM.qml | 2 +- inst/qml/MixedModelsLMM.qml | 2 +- .../{Advanced.qml => MixedModelsAdvanced.qml} | 24 +++++++------------ 4 files changed, 15 insertions(+), 31 deletions(-) rename inst/qml/common/{Advanced.qml => MixedModelsAdvanced.qml} (91%) diff --git a/R/MixedModelsCommon.R b/R/MixedModelsCommon.R index 8c9354e7..aac3df89 100644 --- a/R/MixedModelsCommon.R +++ b/R/MixedModelsCommon.R @@ -422,13 +422,6 @@ control_args$optimizer <- options$optimizerMethod } - # Set convergence checking - if (!is.null(options$optimizerCheckConv)) { - control_args$check.conv.singular <- options$optimizerCheckConv - control_args$check.conv.grad <- options$optimizerCheckConv - control_args$check.conv.hess <- options$optimizerCheckConv - } - # Build optimizer-specific control list optCtrl <- list() @@ -496,21 +489,20 @@ .mixedInterceptML <- function(formula, dataset, type, family = NULL, options = NULL) { # this is a simple function to fit a mixed-effects model with a fixed intercept only # because afex does not allow those models for GLMMs (or LMMs with LRT/PB) + lmControl <<- .mmCreateOptimizerControl(type, options) + if (type == "LMM") { - control <- .mmCreateOptimizerControl(type, options) fit <- lmerTest::lmer( formula = formula, data = dataset, REML = FALSE, - control = control + control = lmControl ) } else if (type == "GLMM") { - control <- .mmCreateOptimizerControl(type, options) fit <- lme4::glmer( formula = formula, data = dataset, - family = family, - control = control + family = family ) } @@ -585,7 +577,7 @@ # the control arguments needs to be assigned outside of the call because # forwarding the call crashes afex - lmControl <- .mmCreateOptimizerControl(type, options) + lmControl <<- .mmCreateOptimizerControl(type, options) if (type == "LMM") { if (.isInterceptML(options)) diff --git a/inst/qml/MixedModelsGLMM.qml b/inst/qml/MixedModelsGLMM.qml index a3c8412c..012bde9d 100644 --- a/inst/qml/MixedModelsGLMM.qml +++ b/inst/qml/MixedModelsGLMM.qml @@ -419,6 +419,6 @@ Form { } } - MM.Advanced {} + MM.MixedModelsAdvanced {} } diff --git a/inst/qml/MixedModelsLMM.qml b/inst/qml/MixedModelsLMM.qml index 098e623f..74ddcd1b 100755 --- a/inst/qml/MixedModelsLMM.qml +++ b/inst/qml/MixedModelsLMM.qml @@ -317,6 +317,6 @@ Form { } } - MM.Advanced {} + MM.MixedModelsAdvanced {} } diff --git a/inst/qml/common/Advanced.qml b/inst/qml/common/MixedModelsAdvanced.qml similarity index 91% rename from inst/qml/common/Advanced.qml rename to inst/qml/common/MixedModelsAdvanced.qml index e1a40a01..bb34f646 100644 --- a/inst/qml/common/Advanced.qml +++ b/inst/qml/common/MixedModelsAdvanced.qml @@ -67,7 +67,7 @@ Section min: 1e-12 max: 1e-1 decimals: 12 - fieldWidth: 100 * jaspTheme.uiScale + fieldWidth: 80 * jaspTheme.uiScale visible: optimizerDropdown.currentValue === "Nelder_Mead" } @@ -80,7 +80,7 @@ Section min: 1e-20 max: 1e-5 decimals: 20 - fieldWidth: 100 * jaspTheme.uiScale + fieldWidth: 80 * jaspTheme.uiScale visible: optimizerDropdown.currentValue === "Nelder_Mead" } @@ -93,7 +93,7 @@ Section min: 1e-15 max: 1e-3 decimals: 15 - fieldWidth: 100 * jaspTheme.uiScale + fieldWidth: 80 * jaspTheme.uiScale visible: optimizerDropdown.currentValue === "Nelder_Mead" } @@ -119,7 +119,7 @@ Section min: 0 max: 10 decimals: 6 - fieldWidth: 100 * jaspTheme.uiScale + fieldWidth: 80 * jaspTheme.uiScale visible: optimizerDropdown.currentValue === "bobyqa" } @@ -132,7 +132,7 @@ Section min: 0 max: 1 decimals: 10 - fieldWidth: 100 * jaspTheme.uiScale + fieldWidth: 80 * jaspTheme.uiScale visible: optimizerDropdown.currentValue === "bobyqa" } @@ -158,7 +158,7 @@ Section min: 1e-12 max: 1e-3 decimals: 12 - fieldWidth: 100 * jaspTheme.uiScale + fieldWidth: 80 * jaspTheme.uiScale visible: optimizerDropdown.currentValue === "nlminb" } @@ -171,7 +171,7 @@ Section min: 1e-20 max: 1e-5 decimals: 20 - fieldWidth: 100 * jaspTheme.uiScale + fieldWidth: 80 * jaspTheme.uiScale visible: optimizerDropdown.currentValue === "nlminb" } @@ -209,16 +209,8 @@ Section min: 1e-12 max: 1e-3 decimals: 12 - fieldWidth: 100 * jaspTheme.uiScale + fieldWidth: 80 * jaspTheme.uiScale visible: optimizerDropdown.currentValue === "default" || optimizerDropdown.currentValue === "BFGS" } - - CheckBox - { - name: "optimizerCheckConv" - label: qsTr("Check convergence") - info: qsTr("Check for convergence warnings and errors") - checked: true - } } } \ No newline at end of file