---
title: "Metric MDS: SGD, SMACOF, and stress weighting"
output:
rmarkdown::html_vignette:
toc: true
vignette: >
%\VignetteIndexEntry{Metric MDS: SGD, SMACOF, and stress weighting}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r setup, include=FALSE}
library(grip)
knitr::opts_chunk$set(collapse=TRUE,comment='#>',fig.width=9,fig.height=5,out.width='100%')
source(system.file('scripts','vignette-views.R',package='grip'),local=TRUE)
```
`metric.mds()` fits Euclidean coordinates to graph shortest-path distances.
The default `backend='sgd'` moves pairs of vertices in shuffled order, following
[Zheng, Pawar and Goodman (2018)](https://doi.org/10.1109/TVCG.2018.2859997),
with a selectable learning-rate schedule. Legacy graph calls default to the
hybrid schedule adapted from the implementation of
[Hangan, Kobourov and Miller (2026)](https://arxiv.org/abs/2605.00641);
`prepare.sparse.mds()` inputs default to Zheng's weight-dependent fixed-budget
exponential schedule (`scheduler="zheng"`).
`backend='smacof'` updates the full configuration using the optional
[smacof package](https://doi.org/10.18637/jss.v102.i10). Its majorization steps
are designed to reduce total stress; individual SGD pair adjustments need not.
This guide illustrates the differences using existing saved synthetic examples.
For a broader comparison of the methods, see Hangan, Kobourov and Miller (2026).
The [complete experiment](metric-mds-results.html) retains all 420 original
fits, the separate 60-fit weighting comparison, failed outcomes, numerical
results, and reproduction instructions.
For higher-dimensional fitting followed by PCA and optional free 3D refinement,
see [Higher-dimensional metric MDS for 3D visualization](high-dimensional-mds.html).
To record and animate actual SGD pair updates, see
[Animating metric MDS: paraboloid and saddle](metric-mds-animation.html).
## Fit a graph with either backend
```{r recipe, eval=FALSE}
g <- suitesparse.graph('dwt_307')
sgd <- metric.mds(edges=g$edges,n=g$n,dim=3,seed=1,
init='random',max.iter=100,diagnostics=FALSE)
smacof.fit <- metric.mds(edges=g$edges,n=g$n,dim=3,seed=1,
init='random',max.iter=100,diagnostics=FALSE,
backend='smacof',eps=1e-8)
```
This is a usage recipe, not a new benchmark on dwt_307. Both backends support
uniform and inverse-squared stress weights. The default full mode uses uniform
weights. An SGD pass and a SMACOF iteration are different amounts of work;
compare achieved error and elapsed time, not iteration counts alone. SGD's
schedule completion reports `iteration_limit`, which does not certify
convergence. The hybrid tuning defaults remain provisional calibration choices.
The saved backend comparisons do not isolate the learning-rate schedule and
therefore do not establish that hybrid is better than `"zheng"`. Hangan et al.'s
fixed-switch rationale concerns the usual uniform-weight MDS setting; it is not
evidence of superiority for inverse-squared regional sparse stress. See
`?metric.mds`, under “Learning-rate schedules and evidence,” for the provenance,
precise defaults and published-schedule controls.
## Two distance targets on the same saddle
The saved experiment uses 64- and 144-point clouds, five paired random starts,
and separate allowances of 10, 30, or 100 passes/iterations. Both methods receive
the same starting coordinates within each pair. Here we show the 144-point
saddle at seed 1 and allowance 100, with uniform stress weights. These displayed
runs were selected in advance, not chosen for their appearance.
**Ambient distances** are straight-line distances between generating points;
an exact 3D realization exists. **Local graph paths** sum edge lengths along
a finite grid with horizontal, vertical, and diagonal edges. They are not
exact geodesics on the continuous saddle, and their best 3D drawing need not
reproduce the generating surface.
```{r data, include=FALSE}
bundle <- readRDS(system.file('extdata','metric-mds-comparison','benchmark.rds',package='grip'))
```
```{r selected-saddles, results='asis', echo=FALSE}
for(id in c('saddle-144','saddle_graph-144')) {
case <- bundle$cases[[id]]; fits <- comparison_representative(bundle,id)
cat('\n\n### ',case$target,'\n\n',sep='')
child <- c(sprintf('```{r selected-%s, echo=FALSE}',id),
'documentation.full.view(case,fits)','```')
cat(knitr::knit_child(text=child,envir=environment(),quiet=TRUE),sep='\n')
tab <- do.call(rbind,lapply(fits,function(f) f$row[,c('backend','error','seconds','iterations','termination')]))
cat(knitr::kable(tab,digits=4,row.names=FALSE,
col.names=c('Backend','Normalized error','Fit seconds','Passes/iterations','Termination')),sep='\n')
}
```
Gray shows the generating reference; blue is SGD and orange is SMACOF.
The ambient case uses matched-vertex alignment. The local-graph case uses
surface-shape alignment: translation, rotation, and reflection match nearby
surface locations without requiring vertex labels to agree. Neither alignment
resizes or deforms the layouts. Scores use the original returned coordinates.
**Normalized error** is the square root of raw stress divided by the sum of
squared target distances; lower is better. The figures below summarize both
cloud sizes across five starts, with median errors and fitting times and
interquartile ranges. Each point is a separate fit restarted with its own
iteration allowance, not a checkpoint along one run. Fitting times exclude
shared graph preparation, alignment, and rendering.
```{r tradeoff, echo=FALSE}
comparison_tradeoff(bundle,c('saddle_graph-64','saddle_graph-144'))
```
## The effect of stress weighting
Uniform weighting minimizes squared errors in distance units. The paper's
inverse-squared weighting minimizes squared **relative** distance errors:
\[
\sum_{i