Can an optimizer recover a recognizable map of Europe from a table of
road distances? We will use that problem to explore three parts of the
mize callback interface: capturing fixed data in a closure,
checking an analytic gradient, and sharing intermediate calculations
between the objective and gradient.
datasets::eurodist contains road distances in kilometres
among 21 European cities. We will fit a two-dimensional configuration
whose Euclidean distances approximate those values. Roads need not
follow straight lines, and the cities lie on the curved surface of the
Earth, so an exact two-dimensional Euclidean fit should not be
expected.
This example minimizes raw stress directly.
stats::cmdscale() follows the classical-scaling route and
uses a different criterion.
Stress and its gradient
Let \(R = (r_{ij})\) be the target distances and let \(D = (d_{ij})\) be the Euclidean distances among candidate coordinates \(Y\). We minimize raw stress:
\[ C(Y) = \sum_{i < j} (r_{ij} - d_{ij})^2. \]
The condition \(i < j\) counts each unordered pair once. The matrix implementation below instead sums both symmetric triangles, so it multiplies that sum by one half. With \(n\) objects, the corresponding distance RMSE is \(\sqrt{C / {n \choose 2}}\) and has the same units as the input distances.
For distinct points, the derivative with respect to the coordinates of object \(i\) is
\[ \frac{\partial C}{\partial \mathbf{y}_i} = -2 \sum_{j \ne i} \frac{r_{ij} - d_{ij}}{d_{ij}} (\mathbf{y}_i - \mathbf{y}_j). \]
Implementation note. Euclidean distance is nondifferentiable when two candidate points coincide.
distance_epsilonprevents division by zero in the code, although the mathematical derivative remains undefined at an exactly coincident configuration. The gradient check therefore uses a seeded, nondegenerate point.
The callback factory captures the fixed target matrix in a closure.
Its prepare() helper performs the shared conversion from a
parameter vector to coordinates and pairwise distances. A counter
records how often that work is performed.
distance_epsilon <- 1e-10
make_mmds_callbacks <- function(
distances,
combined = FALSE,
epsilon = distance_epsilon
) {
target <- as.matrix(distances)
preparation_count <- 0L
prepare <- function(par) {
preparation_count <<- preparation_count + 1L
coordinates <- matrix(par, ncol = 2, byrow = TRUE)
list(
coordinates = coordinates,
distances = as.matrix(stats::dist(coordinates))
)
}
stress <- function(prepared) {
0.5 * sum((target - prepared$distances)^2)
}
gradient <- function(prepared) {
weights <- (target - prepared$distances) /
(prepared$distances + epsilon)
coordinates <- prepared$coordinates
result <- matrix(
nrow = nrow(coordinates),
ncol = ncol(coordinates)
)
for (i in seq_len(nrow(coordinates))) {
differences <- sweep(-coordinates, 2, -coordinates[i, ])
result[i, ] <- colSums(differences * weights[, i])
}
as.vector(t(result)) * -2
}
callbacks <- list(
fn = function(par) stress(prepare(par)),
gr = function(par) gradient(prepare(par))
)
if (combined) {
callbacks$fg <- function(par) {
prepared <- prepare(par)
list(
fn = stress(prepared),
gr = gradient(prepared)
)
}
}
list(
fg = callbacks,
preparations = function() preparation_count,
reset_preparations = function() {
preparation_count <<- 0L
invisible(NULL)
}
)
}The separate variant supplies fn and gr.
The combined variant also supplies fg, which lets
mize request an objective and gradient from one prepared
configuration.
Validate the gradient
check_mize_gradient() compares the analytic gradient
with central finite differences. Because the combined callback is
present, it also checks that fg agrees with the separate
fn and gr callbacks.
gradient_check <- check_mize_gradient(
combined_callbacks$fg,
initial_par
)
gradient_summary <- data.frame(
max_abs_error = gradient_check$max_abs_error,
max_rel_error = gradient_check$max_rel_error,
fg_fn_abs_error = gradient_check$fg_consistency$fn$abs_error,
fg_gr_max_abs_error =
gradient_check$fg_consistency$gr$max_abs_error
)
knitr::kable(
signif(gradient_summary, digits = 4),
row.names = FALSE,
col.names = c(
"Maximum absolute error",
"Maximum relative error",
"fg/fn absolute error",
"fg/gr maximum absolute error"
)
)| Maximum absolute error | Maximum relative error | fg/fn absolute error | fg/gr maximum absolute error |
|---|---|---|---|
| 0.008302 | 1.7e-06 | 0 | 0 |
The analytic gradient agrees with the finite-difference approximation to about 1.7e-06 in relative terms. At the checked point, the combined callback also reproduces the separate objective and gradient to the displayed precision.
Optimize with separate and combined callbacks
For this example, we stop when the relative change in stress falls below \(10^{-8}\). Stress depends on the scale of the supplied distances, which makes a relative criterion convenient here. The Convergence article discusses the other controls.
mmds_controls <- list(
method = "L-BFGS",
max_iter = 200,
abs_tol = NULL,
rel_tol = 1e-8,
grad_tol = NULL,
step_tol = NULL
)
separate_callbacks$reset_preparations()
combined_callbacks$reset_preparations()
separate_result <- do.call(
mize,
c(
list(par = initial_par, fg = separate_callbacks$fg),
mmds_controls
)
)
combined_result <- do.call(
mize,
c(
list(par = initial_par, fg = combined_callbacks$fg),
mmds_controls
)
)
results <- list(
separate = separate_result,
combined = combined_result
)
initial_stress <- gradient_check$fn
pair_count <- choose(attr(target_distances, "Size"), 2)
preparation_counts <- c(
separate = separate_callbacks$preparations(),
combined = combined_callbacks$preparations()
)
initial_rmse_km <- sqrt(initial_stress / pair_count)
final_rmse_km <- sqrt(combined_result$f / pair_count)
callback_comparison <- data.frame(
callbacks = c("separate fn/gr", "combined fg"),
iterations = vapply(results, `[[`, numeric(1), "iter"),
objective_calls = vapply(results, `[[`, numeric(1), "nf"),
gradient_calls = vapply(results, `[[`, numeric(1), "ng"),
distance_preparations = unname(preparation_counts),
check.names = FALSE
)
knitr::kable(
callback_comparison,
row.names = FALSE,
col.names = c(
"Callbacks",
"Iterations",
"Objective calls",
"Gradient calls",
"Distance preparations"
)
)| Callbacks | Iterations | Objective calls | Gradient calls | Distance preparations |
|---|---|---|---|---|
| separate fn/gr | 37 | 57 | 57 | 114 |
| combined fg | 37 | 57 | 57 | 58 |
Both callback forms converge on rel_tol from the fixed
starting point and return the same configuration. They take 37
iterations and reduce pairwise-distance RMSE from 1,750 km to 126
km.
mize requests the same logical information in both runs.
The combined callback performs 58 distance-matrix preparations instead
of 114 because a joint request can reuse one prepared configuration. On
this small problem, that deterministic count is more informative than a
wall-clock comparison. Exact callback totals may change with line-search
details.
Raw stress is nonconvex, so this seeded run demonstrates a reproducible fit without claiming that it found the global minimum.
Put the configuration on the map
Distances leave absolute position and orientation unidentified. Adding one vector to every point leaves all distances unchanged, as do rotations and reflections. A coordinate-only plot therefore cannot tell us where north is, but it also leaves a more useful question unanswered: after accounting for those arbitrary choices, where does the recovered shape disagree with the actual geography?
To make that comparison, we use a small geographic reference bundled with the article. The map outline and 20 city coordinates come from Natural Earth; the Hook of Holland coordinate comes from GeoNames. The source, license, and reproduction details are recorded with the data files.
The helper below uses an equirectangular projection centred on Europe. Its coordinates are approximate kilometres, which is sufficient for a regional diagnostic rather than a precision map.
geography_path <- system.file(
"extdata",
"eurodist-geography.csv",
package = "mize"
)
map_path <- system.file(
"extdata",
"europe-map.csv",
package = "mize"
)
city_geography <- utils::read.csv(geography_path)
europe_map <- utils::read.csv(map_path)
project_geography <- function(longitude, latitude) {
earth_radius_km <- 6371.009
reference_longitude <- 9
reference_latitude <- 48
degrees_to_radians <- pi / 180
cbind(
x = earth_radius_km * cos(reference_latitude * degrees_to_radians) *
(longitude - reference_longitude) * degrees_to_radians,
y = earth_radius_km *
(latitude - reference_latitude) * degrees_to_radians
)
}The alignment is fitted rather than chosen by eye. It removes translation, allows rotation or reflection because handedness is unidentified, and uses one uniform scale factor. The scale separates the overall inflation of road distances relative to straight-line geography from differences in the configuration’s shape. It is used only for this geographic comparison; the optimizer’s returned configuration and raw stress remain unchanged. The singular value decomposition finds the least-squares similarity alignment.
center_coordinates <- function(par) {
coordinates <- matrix(par, ncol = 2, byrow = TRUE)
sweep(coordinates, 2, colMeans(coordinates))
}
align_to_reference <- function(configuration, reference) {
configuration <- scale(
configuration,
center = TRUE,
scale = FALSE
)
reference_center <- colMeans(reference)
centered_reference <- sweep(reference, 2, reference_center)
decomposition <- svd(crossprod(configuration, centered_reference))
orthogonal_transform <- decomposition$u %*% t(decomposition$v)
scale_factor <- sum(decomposition$d) / sum(configuration^2)
list(
coordinates = sweep(
scale_factor * configuration %*% orthogonal_transform,
2,
reference_center,
"+"
),
scale = scale_factor,
determinant = det(orthogonal_transform)
)
}
centered_coordinates <- center_coordinates(combined_result$par)
stopifnot(identical(city_geography$city, labels(target_distances)))
geographic_coordinates <- project_geography(
city_geography$longitude,
city_geography$latitude
)
alignment <- align_to_reference(
centered_coordinates,
geographic_coordinates
)
aligned_coordinates <- alignment$coordinates
geographic_residual_km <- sqrt(rowSums(
(aligned_coordinates - geographic_coordinates)^2
))
geographic_rmse_km <- sqrt(mean(geographic_residual_km^2))The fitted scale factor is 0.694, and this fit includes a reflection because the optimized configuration’s handedness is arbitrary. Each orange segment in the map joins a city’s geographic location to its aligned MDS location. The triangle pattern retains the recovered distance geometry, while the segments make its local errors visible. Their RMSE is about 269 km after the global similarity alignment. Long segments at Athens and Stockholm, for example, are information that a polished but unreferenced coordinate plot would hide.

The invariance holds at the objective level. The hidden check below translates, rotates, and reflects the returned configuration and verifies that stress is unchanged to numerical tolerance.
Starting from road distances alone, the optimizer recovers a
recognizable geometry even though its coordinate frame is arbitrary. The
closure and combined-callback pattern used here also applies whenever an
objective and gradient share fixed data or an expensive intermediate
calculation; ?mize describes the full callback
contract.
See also
- Getting started introduces callback lists and gradient checks.
- Convergence explains the relative tolerance used here.
- Stateful optimization shows step-by-step integration with retained optimizer state.