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