Skip to content

Fix bugs from a worked-example sweep: core, images, optimization, graphics, graphs/ML, notebook ; v0.231 - #81

Merged
stblake merged 8 commits into
mainfrom
worked-example-fixes
Sep 29, 2026
Merged

stblake merged 8 commits into
mainfrom
worked-example-fixes

Conversation

@msollami

Copy link
Copy Markdown
Collaborator

Summary

This PR fixes a batch of bugs that turned up while running several hundred worked examples through the binary, covering graphs, optimization, machine learning, image processing, graphics export and the notebook front end. Each fix has a regression test, and the full ctest run has no new failures against main (v0.230). Bumps $VersionNumber to 0.231.

Changes

Core language

  • a === b === c, =!= chains and != chains now parse to one variadic call (SameQ[a, b, c]), so 1 === 1 === 1 is True. != also joins mixed Inequality chains.
  • Equal and Unequal on Lists compare element by element with the inexact tolerance, recursively: {0.1 + 0.2} == {0.3} is True.
  • Normal[SparseArray[...]] produces the dense array. New src/sparsearray.c handles position rules, pattern rules, Band, defaults and Mathematica's internal CSR form.
  • Table, Range, Do, Sum and Product accept symbolic numeric bounds with a step (Table[x, {x, 0, 2 Pi, 1.}]). This also fixes a Do loop over a symbolic lattice that never ended, and Sum's closed form using the raw bound.
  • New Log10 and Log2, with NDArray/packed kernels and Compile lowering (scalar and rank-1 both report Compiled -> True). Log[b, z] gives the exact rational when b and z are powers of one rational.

Image processing

  • ImageRotate now turns counterclockwise on both the quarter-turn and free-angle paths, as documented. Adds the side and side1 -> side2 forms, and fixes a leak of the N[angle] wrapper.
  • A stated type coerces by rounding and clipping, as Mathematica documents: Image[{{0, 300}}, "Byte"] stores {0, 255}. Adds "Bit16". Image[img, type] converts between types. Invalid data leaves the call unevaluated and ImageQ false.
  • Morphology keeps a "Bit" image "Bit". ImageCorrelate accepts a single-channel Image template.
  • Fixes test_ncc_finds_the_patch_it_was_given, which fails on main and aborted image_tests.

Numerical optimization

  • NMaximize and FindMaximum with {f, c1, c2, ...} negate only the objective. They used to return garbage.
  • FindMinimum[f, {x, x0, x1}] treats the two values as starting points and searches. It used to return x0.
  • Starting values and options are evaluated, so the spec works inside Table.
  • MPFR and machine bracket searches step back from points where the objective is complex. WorkingPrecision -> 30 on x^2 - 2 Log[x] now works.
  • Powell switches to a rotated direction set when stalled at a kink, so it moves on non-smooth objectives.
  • A FindMinimum::cvmit warning is emitted by all local solvers at MaxIterations.
  • Conjugate gradient uses a strong-Wolfe line search: Rosenbrock goes from 6e-4 to 1e-23.
  • COBYLA keeps its spacing and trust radius separate and takes round steps.
  • Box-bound active sets for BFGS, CG and Newton: HS71 reaches 17.014 with the default method.
  • SLSQP no longer quits while the constraint violation is still falling.
  • Nelder–Mead returns the polished feasible point.
  • The NMinimize refpage now matches the source (NMinimize is not HoldAll, and Or constraints are supported).

Graphics

  • Show combines several graphics, nested lists and trailing options. PlotRange is merged and each input keeps its own PlotStyle.
  • PDF export honours PlotStyle, Epilog/Prolog, AxesLabel, PlotLabel and a simple PlotLegends.
  • New directives in both renderers: Dashing, AbsoluteDashing, AbsoluteThickness, AbsolutePointSize, Directive, named sizes, and Dashed/Dotted/DotDashed/Thick/Thin. Multi-curve plots apply per-curve PlotStyle.
  • Exact coordinates (Rational, and other exact values via N) are drawn in every primitive. They used to be dropped.
  • PointSize and Thickness mean the same thing in the PNG and PDF renderers (a fraction of plot width). This fixes points that disappeared over large coordinate ranges.
  • Fixes a leak in the Plot/ParametricPlot option handling.

Graphs and machine learning

  • FindSpanningTree on weighted graphs returns a minimum spanning forest (Kruskal) or, for directed graphs, a minimum arborescence (Chu–Liu/Edmonds). Weights are compared exactly and carried over to the result. The {g, v} form is supported.
  • Graph[edges, EdgeWeight -> w] and a stored EdgeCapacity work in both constructor forms, with one canonical form. FindMaximumFlow uses the graph's own capacities.
  • ConnectedComponents on directed graphs returns strongly connected components in Mathematica's order. ConnectedGraphQ requires strong connectivity. Undirected components are listed largest first.
  • The LearnDistribution Gaussian-mixture variance floor is measured between distinct points, so on the rounded iris data it finds 2 components (as Mathematica does) instead of 9 degenerate ones.

Notebook front end

  • The Cell menu's "Convert to" actually converts the cell. There is now one shared convertCell.
  • One z-index scale for the whole window, so the properties panel and the find bar sit above the notebook in focused mode.
  • Enter in the find bar walks through matches with focus kept in the field. It used to replace the match with a newline.
  • Print output and messages reach the cell. There is an opt-in "cell": true pipe request, with stream/message lines; requests without the flag behave as before.
  • .mathilda files open and Save As through the existing Rust stanza parser.
  • Several statements per cell are evaluated in turn. The kernel's 10 KB request limit is gone.

Testing

  • Full ctest against a clean main build: no new failures. image_tests now passes; it fails on main. The remaining failures are the same as on main: corpus suites that assume tests/build, plus the iter and series suites that were already failing.
  • New or extended suites: test_parse, test_comparisons, test_logexp, test_iter, test_sparsearray (new), test_image, test_findmin, test_nminimize, test_findmin_powell, test_findmin_cobyla, test_findmin_slsqp, test_graphics, test_graph, test_hypergraph, test_ml_dist. Each new regression test fails on the parent commit.
  • Where the answer is unique, the graph and ML results were checked against Mathematica 15 (wolframscript).
  • make check-c99, check-messages, check-packed-aware, check-array-exactness and check-nd-surfaces pass. check-compile-coverage reports only heads not touched here (the image heads and PackedArrayQ).
  • leaks --atExit on a script that exercises every fix: 0 leaks.
  • Frontend: npm run check has 0 errors. New check:notebook passes 26/26; check:search passes 24/24. cargo test passes 16/16. New make check-pipe-protocol passes 22/22. The desktop app was not launched to check the UI changes by eye.
  • Known and left alone: integer data given to Image without a type is stored as "Byte", as on main. PNG export crashes inside GLFW when no display is available; this happens on main too, so the two raster tests are opt-in (MATHILDA_TEST_RASTER=1).

JIRA Ticket

N/A

…sages in cells, .mathilda files

Five front-end bugs, plus the two cell limits found alongside them.

Cell > Convert to: the three menu items called retypeActiveCell, which only
relabels the toolbar's record of the active cell. Menu and toolbar now share
convertCell(store, id, type) in cellCommands.ts, which retypes the cell in the
store (source and output kept) and refuses reference-page cells.

Stacking: the properties panel (40) and find bar (45) sat under the focused
view (50). All window-level z-indexes now come from one --z-* scale in app.css;
overlays sit above the notebook view and status dock, menus above everything.

Find bar: a jump selected the match and focused the cell, so the next Enter
replaced the match with a newline. The current match is now painted (a
CodeMirror mark decoration in code cells, the CSS Custom Highlight API in prose)
and scrolled into view while the find field keeps focus; the first Enter lands
on the shown match and Escape places the caret on it.

Print and messages: pipe mode sent Print text raw to stdout (dropped by the
Rust side) and messages to stderr (only logged). A request may now carry
"cell": true; the kernel then captures each statement's Print output and
messages and sends them as "stream" and "message" lines before its result.
Requests without the flag are unchanged (raw Print, stderr, "Null" payloads),
which site/generate.py and the audit tools depend on. The Rust side also
forwards stray non-JSON stdout and stderr to the cell for older kernels.

.mathilda files: the Open dialog offered them but parsed every file as library
JSON. They now open through the Rust stanza parser (moved to notebook_format.rs,
stricter marker matching, preamble kept, CRLF accepted) as a new notebook, and
Save As can export the current notebook in that format.

Cells: statements on separate lines are each evaluated, as in Mathematica
(parse_next_expression, whole cell syntax-checked first; ';' and Null suppress
a result). Requests are read at any length instead of into a 10 KB buffer, and
all JSON escapes including \uXXXX are decoded.

Also fixes the two existing svelte-check errors (untyped plotly import, rename
field typed as an input).

Tests: make check-pipe-protocol (new, both request modes), npm run
check:notebook (new), extended check:search, 16 Rust unit tests including a
round trip through the real kernel.
…logy

ImageRotate turned clockwise on both the quarter-turn permutation path
(rot90_run had its odd turns swapped) and the free-angle resampler
(rot_free_run used the y-up inverse matrix on y-down rows). Both now
turn counterclockwise for a positive angle, as the docstring and
Mathematica say. Adds Mathematica's side forms, ImageRotate[img, side]
and ImageRotate[img, side1 -> side2]. Also frees the N[angle] wrapper,
which leaked on every call with an angle argument.

Image[data, type] now coerces the data to the stated type by rounding
and clipping, as Mathematica documents: Image[{{0, 300}}, "Byte"]
stores {{0, 255}}. Adds the "Bit16" type, accepts "Real32" and "Real64"
as synonyms for "Real", and adds Image[image, type] / Image3D[vol, type]
conversion. The old path had two root causes. A declined call stayed
unevaluated in exactly the canonical shape, and image_info checked only
the shape, so ImageQ gave True and ImageData divided 300 by 255. And an
auto-packed int64 buffer passed the fixed-point early return without its
values being checked. image_info now checks every element of nested
data, and the fixed point checks the range of integer buffers. Data
that cannot be represented (non-numeric, complex, ragged) is still left
unevaluated.

Dilation/Erosion/Opening/Closing keep a "Bit" image "Bit" (planar and
volumetric). ImageCorrelate accepts a single-channel Image as the
kernel or template.

Tests: new regression rows in test_image.c for rotation direction (both
paths, the side forms, colour images, packed images), type coercion
and conversion (planar, volumetric, packed, and with packing disabled),
Bit morphology, and image templates. Fixes
test_ncc_finds_the_patch_it_was_given: its test image contains a
second copy of the template shifted by a constant brightness, which NCC
scores as an exact tie. That test failed on main and aborted the suite.
Refpage docs/spec/builtins/image-processing.md updated.
…edComponents; robust mixture floor

FindSpanningTree ignored EdgeWeight. It returned a BFS tree, total weight 13 where the
minimum is 11. It now follows Mathematica case by case, checked output for output against
Mathematica 15:
- weighted undirected: Kruskal with union-find, giving a minimum spanning forest; ties
  are broken by vertex position, which matches Mathematica's choice;
- weighted directed: Chu-Liu/Edmonds, O(E log V), with leftist heaps and a rollback
  union-find. It gives the minimum arborescence, or the lightest branching with the
  fewest roots;
- unweighted: a BFS forest, or a forward BFS branching rooted in decreasing DFS
  finishing time.
Weights are compared exactly as GMP rationals, and each tree edge keeps its own weight
and capacity. {g, v} gives the tree grown from v, an invalid v emits
FindSpanningTree::inv, and options are accepted. Mixed graphs stay unevaluated, as in
Mathematica.

Graph[edges, opts] now takes options: EdgeWeight, and a new stored EdgeCapacity, in
either constructor form and in any order. The canonical graph lists them after the edges
in a fixed order. All consumers read them through graph_edge_weight_list and
graph_edge_capacity_list instead of assuming argument 3. FindMaximumFlow falls back to
the graph's own EdgeCapacity when no option is given.

ConnectedComponents on a graph with directed edges now gives the strongly connected
components, in Tarjan completion order, which is exactly Mathematica's order. On an
undirected graph the largest component comes first. The {v...} form is new.
WeaklyConnectedComponents stays weak and is ordered largest first. ConnectedGraphQ
requires strong connectivity on directed graphs. The refpage deviation note is removed.

LearnDistribution with Method -> "GaussianMixture" measured its variance floor as the
median nearest-neighbour distance over all points. On rounded data most points have an
exact duplicate, so the median was 0 and the floor fell to 1e-300. The spacing is now
measured between distinct points, with a floor of at least 1e-4 of the average variance,
and k is capped by the number of distinct points. On 45 iris petal lengths the fit goes
from 9 components, some of a single point, to 2, the model Mathematica learns.

Regression tests in graph_tests, hypergraph_tests and ml_dist_tests. Refpages updated in
graphs.md, hypergraphs.md and machine-learning.md.
…ch, held starts, MPFR bracket, Powell kinks, silent non-convergence

NMaximize[{f, c1, c2, ...}] negated the whole list (Times[-1, {...}] threads
over the constraints) and returned {-1e+300, garbage). NMaximize and
FindMaximum now negate only the objective; FindMinimum/FindMaximum also accept
the {f, c1, c2, ...} form.

FindMinimum[f, {x, x0, x1}] treated x0, x1 as bounds and returned x0. They are
now the two starting values: the interval is the initial bracket only when an
interior golden-section point lies below both ends, otherwise the bracket is
expanded downhill (new fm_bracket2 and a two-start branch in fm_bracket_mpfr).

HoldAll kept starting values unevaluated: {x, s} with s a Table iterator was
read as two variables. A trailing symbol that carries a value (or a numeric
constant such as Pi) is now a starting value (fm_symbol_is_free); a symbol
bound to a spec list is resolved; scalar option values (Method, MaxIterations,
WorkingPrecision, AccuracyGoal, PrecisionGoal) are evaluated.

The 1-D bracket expansion (machine and MPFR) backs off toward the last good
point when a step leaves the objective's real domain, fixing "MPFR
bracket-finding failed" on x^2 - 2 Log[x] from x = 3.

Powell restarts a stalled cycle from a deterministic rotated orthonormal basis
and accepts convergence only when that also fails, so it leaves Abs/Max kinks
where every axis direction is non-descent.

Every local method emits FindMinimum::cvmit when MaxIterations is reached
(silent inside the penalty wrapper and NMinimize's polish). ConjugateGradient
uses a strong-Wolfe line search with Powell restarts on its unconstrained
path. COBYLA separates the monotone resolution rho from a ratio-controlled
trust radius and takes the Euclidean trust-region step instead of a box corner.

The projected BFGS, CG and Newton solvers hold binding box bounds fixed
(fm_box_binding_mask), so the default method reaches 17.0140 on HS071 (was
19.84) and the NMinimize fixed-charge-flow polish returns a feasible flow; that
test asserts a feasible optimum again. SLSQP abandons feasibility only after 30
non-improving iterations (was any 8 infeasible ones), so a 10-link hanging
chain converges. The NelderMead restart polish no longer keeps a fixed-penalty
vertex 1e-6 outside an active constraint; the NM_FEAS_RETURN comment now states
its units and measurements.

Docs: numerical-calculus.md describes NMinimize as non-HoldAll throughout,
drops Or from its unsupported list, and documents the two-start form, argument
evaluation, cvmit, and the Powell/CG/COBYLA changes; docstrings updated.

Tests: new regressions in test_findmin.c, test_findmin_powell.c,
test_findmin_cobyla.c, test_findmin_slsqp.c and test_nminimize.c (each fails on
the parent commit); all 23 optimization-related suites pass.
…s, exact coordinates

Show[g1, g2, ...] and Show[{g1, g2, ...}] used to stay unevaluated. They now
overlay their inputs, as in Mathematica. Each input's primitives become one
List directive scope, and the input's own PlotStyle (the style a single-curve
Plot/ListPlot is drawn in) is baked into that scope. Options come from g1 unless
trailing options override them. PlotRange is the union of the inputs' ranges
(an input without one contributes its primitive extent) and stays automatic
when no input fixes one. Legends are concatenated, $PlotResample is dropped,
and 2D and 3D inputs do not mix.

Style directives: add Dashing, AbsoluteDashing, AbsoluteThickness,
AbsolutePointSize, Directive, the named sizes Tiny/Small/Medium/Large, and
the OwnValues Dashed, Dotted, DotDashed, Thick and Thin, which evaluate to
their long forms. The Raylib renderer (screen, PNG, JPEG) and the PDF writer
resolve all of them through shared helpers in plot_common.c: Thickness,
PointSize (a diameter) and Dashing are fractions of the plot width; the
Absolute forms are in points. A List is now a directive scope in both back
ends.

PlotStyle -> {s1, s2, ...} was ignored by multi-curve Plot, ListPlot and
ParametricPlot. Each style is now baked into its curve and cycles. A
non-colour style gets its own List scope, and legend swatches use the styled
colours.

PDF export: honour PlotStyle, Epilog/Prolog, PlotLabel, AxesLabel,
PlotLegends (a swatch legend), Dashing and the absolute sizes. Read every
coordinate through gfx_coerce_double, so primitives with exact coordinates
(Line[{{0,0},{1/2,1}}], Disk[{Pi/4,1/4},1/10], Point, Polygon, Circle,
Arrow, Text) are no longer dropped or misplaced. Circle[] and Disk[] draw the
unit circle.

PNG export: the rasteriser read PointSize[d] as a radius in plot coordinates,
so over a large coordinate range the dot became sub-pixel and vanished.
PointSize, and likewise Thickness, now use the PDF's fraction-of-width
meaning. Prolog/Epilog start from black, and a thick Circle outline is drawn
at its thickness. Mesh dots use PointSize[0.005].

Fix a leak in Plot/ParametricPlot option handling:
evaluate(expr_copy(rhs)) leaked the copy of every held option value.

Tests (graphics_tests): Show combination, option precedence, PlotRange union,
per-input styles and legends; directive symbols and size resolution;
per-curve PlotStyle; the PDF content stream for exact coordinates, PlotStyle,
dashes, Epilog/Prolog order, labels, legend and list scoping. There are also
opt-in raster checks (MATHILDA_TEST_RASTER=1) for PointSize and Dashing in
PNG.
Output of make docs for the pages the graphics change touched: Show's multi-graphic forms and examples, Export's PDF/PNG option and directive support, and ParametricPlot's test link. The other regenerated pages differed only in run-dependent noise and are left out.
…ator bounds; add Log10/Log2

Variadic comparison chains. The parser nested a run of === as
SameQ[SameQ[a, b], c], comparing True with c, so 1 === 1 === 1 was False;
=!= and != had the same defect (1 != 2 != 1 evaluated True != 1). A bare
run of SameQ / UnsameQ / Unequal now collects into one variadic call, as in
Mathematica; a parenthesised operand still nests. Unequal mixed with an
ordering operator joins the chain's Inequality (a < b != c), which
Inequality now evaluates and prints. Inequality decides its == and != pairs
through the Equal / Unequal builtins, so list tolerance and the exact zero
test apply inside chains.

Equal on Lists. Lists that were not structurally identical never reached a
numeric comparison, so {0.1 + 0.2} == {0.3} stayed unevaluated. Two Lists
are now compared elementwise with Equal, recursively: True when every
position is, False for different lengths or one decidably unequal position,
otherwise unevaluated. Unequal uses the same verdict.

Normal[SparseArray[...]]. Mathilda has no sparse storage and Normal handed
the SparseArray back unchanged. New src/sparsearray.c builds the dense
nested List for explicit positions (with or without dims and default),
{p1, p2} -> {v1, v2}, pattern rules, Band[...], dense lists and the
InputForm of Mathematica's internal SparseArray[Automatic, ...] form, at any
depth of Normal's argument.

Numeric symbolic iterator bounds. Table[x, {x, 0., 2 Pi, 1.}] and
Range[0, 2 Pi, 1.] stayed unevaluated, Do over {x, 0, 2 Pi, Pi/2} never
terminated, and Sum[i, {i, 1, Pi}] took its closed form over the raw bound
(Pi (1 + Pi)/2 instead of 6). iter_spec_parse_lattice (Table, Do, Sum,
Product) and Range now replace a real NumericQ, non-explicit upper bound by
the exact last lattice point imin + Floor[(imax - imin)/di] di; the
plotters keep the plain parser, whose ranges are continuous. Loops refresh
their termination value from a symbolic running value. Range with BigInt
bounds is counted exactly in GMP.

Log10 and Log2. Neither existed. Both are now Listable NumericFunction
builtins defined as Log[10, z] / Log[2, z] (Log10[x] -> Log[x]/Log[10]),
with libm log10/log2 for positive machine reals so exact powers stay exact,
NDArray kernels for the packed and visible-array surfaces, and Compile[]
lowering at scalar and rank-1 shape. Log[b, z] now returns the exact
rational whenever b and z are powers of one common rational
(Log[4, 8] = 3/2, Log[10, 1/100] = -2, Log[10, 10^30] = 30).

Tests: new sparsearray_tests suite; regressions added to parse_tests,
comparisons_tests, iter_tests and logexp_tests. Refpages updated in
docs/spec/builtins (comparisons, elementary-functions, lists-and-iteration,
control-flow, calculus, power-series, data-structures).
Record the batch of fixes on this branch in the weekly changelog and bump
$VersionNumber to 0.231. It covers comparison chains, list Equal,
Normal[SparseArray], symbolic iterator bounds and Log10/Log2; image rotation
direction and stated-type coercion; the FindMinimum/NMinimize family;
Show and PDF/PNG export styling; weighted spanning trees, Graph[edges, opts]
and strong components; the LearnDistribution mixture floor; and five
notebook front-end bugs.
@stblake
stblake merged commit 771c31a into main Sep 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants