Skip to content

Feature/issue 3249 minmax rev#3344

Draft
JmieTng wants to merge 2 commits into
stan-dev:developfrom
JmieTng:feature/issue-3249-minmax-rev
Draft

Feature/issue 3249 minmax rev#3344
JmieTng wants to merge 2 commits into
stan-dev:developfrom
JmieTng:feature/issue-3249-minmax-rev

Conversation

@JmieTng

@JmieTng JmieTng commented Jul 11, 2026

Copy link
Copy Markdown

Summary

This PR implements reverse-mode automatic differentiation for the max() and min() function inside stan::math. It introduces the binary scalar overload max(x, y) and min(x, y) as well as container/matrix overloads.

Gradient Propagation ($dx$ and $dy$)

During the backward pass, the upstream adjoint (vi.adj()) is distributed based on the comparative values of the inputs:

For max(x, y):

  • $x > y$: $x$ is the unique maximum. $dx \text{ += } \text{vi.adj()}$ and $dy \text{ += } 0$.
  • $y > x$: $y$ is the unique maximum. $dx \text{ += } 0$ and $dy \text{ += } \text{vi.adj()}$.
  • $x = y$: A tied maximum. The subgradient is split equally: $dx \text{ += } \text{vi.adj()} \times 0.5$ and $dy \text{ += } \text{vi.adj()} \times 0.5$.

For min(x, y):

  • $x < y$: $x$ is the unique minimum. $dx \text{ += } \text{vi.adj()}$ and $dy \text{ += } 0$.
  • $y < x$: $y$ is the unique minimum. $dx \text{ += } 0$ and $dy \text{ += } \text{vi.adj()}$.
  • $x = y$: A tied minimum. The subgradient is split equally: $dx \text{ += } \text{vi.adj()} \times 0.5$ and $dy \text{ += } \text{vi.adj()} \times 0.5$.

NaN Check & Propagation

The binary max(x, y) and min(x, y) functions handle NaN identically:

  • Both $x$ and $y$ are NaN: Returns NaN. Both $dx$ and $dy$ are explicitly set to NOT_A_NUMBER ($NaN$) in the reverse pass.
  • Only $x$ is NaN: The function ignores the $NaN$ and returns $y$.
  • Only $y$ is NaN: The function ignores the $NaN$ and returns $x$.

Tests

1. Matrix and Vector Variates (mix)

To guarantee that the functions handle Stan’s specialized memory layouts efficiently, the matrix tests wrap execution using both standard types and specialized matrix-variate structures:

  • Overload Coverage: Every shape profile is mapped and tested across Eigen::VectorXd, Eigen::RowVectorXd, Eigen::MatrixXd, and std::vector<double>.
  • expect_ad_matvar: Explicitly validates the code path for var_value<Matrix> structures to prevent regressions in Stan’s vectorized expressions.

2. Edge Case & Boundary Verification

  • Empty Containers: Replicating Stan's mathematical contracts, empty container evaluations are strictly verified to return structural boundary limits:
    • max(empty) $\rightarrow$ NEGATIVE_INFTY
    • min(empty) $\rightarrow$ INFTY
  • Infinite Limits: Validates structural bounds handling when processing inputs containing combinations of $\infty$ and $-\infty$.

3. Subgradient Tie Resolution

Because max and min contain non-differentiable where values tie, the tests verifies the subgradient approach:

  • Scalar & Container Ties: Tests include inputs with uniform values (e.g., matching elements across a vector or matrix).
  • Tolerance Adjustments: Because finite-differencing struggles inherently at exact kinks, stan::test::ad_tolerances are tuned (gradient_grad_ = 1e-3) to cleanly verify that the gradient is distributed symmetrically and equally ($0.5 / 0.5$) across tied elements.

4. Comprehensive NaN Framework Diagnostics

NaN handling is tested in multiple arrangements:

  • Asymmetric NaNs: Validates that isolated NaN values are ignored during the forward pass, allowing standard gradient flow through the valid numbers.
  • Complete NaNs: Validates that global NaN states safely resolve to NaN and correctly apply structural NOT_A_NUMBER adjustments to the input adjoints during the backward pass without triggering execution faults.

Side Effects

Release notes

Checklist

  • Copyright holder: (fill in copyright holder information)

    The copyright holder is typically you or your assignee, such as a university or company. By submitting this pull request, the copyright holder is agreeing to the license the submitted work under the following licenses:
    - Code: BSD 3-clause (https://opensource.org/licenses/BSD-3-Clause)
    - Documentation: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)

  • the basic tests are passing

    • unit tests pass (to run, use: ./runTests.py test/unit)
    • header checks pass, (make test-headers)
    • dependencies checks pass, (make test-math-dependencies)
    • docs build, (make doxygen)
    • code passes the built in C++ standards checks (make cpplint)
  • the code is written in idiomatic C++ and changes are documented in the doxygen

  • the new changes are tested

@JmieTng JmieTng marked this pull request as draft July 11, 2026 20:23
@JmieTng

JmieTng commented Jul 11, 2026

Copy link
Copy Markdown
Author

@SteveBronder Apologies for the delay! Reviving the conversation, I'm mainly concerned about the templates. This has led me to temporarily create tests in two places: mix and rev.

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.

1 participant