@@ -1526,55 +1526,105 @@ def replace_atan2(values_map: dict[str, Value], node: fx.Node, loc: Location) ->
15261526 """Lower atan2(y, x) using atan(y/x) with quadrant correction.
15271527
15281528 CoreAI has no native atan2, so it is decomposed as:
1529- - x != 0: atan(y/x) adjusted by ±π to place the result in the correct quadrant.
1530- - x == 0: ±π/2 or 0 based on sign of y.
1529+ - x != 0, finite: atan(y/x) adjusted by ±π for the correct quadrant.
1530+ - x == +0: ±π/2 for non-zero y, 0 for y = 0.
1531+ - x == -0: ±π for all y (including ±0 → ±π per IEEE-754).
1532+ - both infinite: ±π/4 or ±3π/4 per IEEE-754.
1533+
1534+ Signed-zero handling: IEEE-754 treats -0.0 as distinct from +0.0 for atan2
1535+ (e.g. atan2(-0, -1) = -π, not +π). The 1/v trick — 1/-0.0 = -inf — is used
1536+ to detect the sign bit of zero inputs so that y_neg and x_neg are correct
1537+ for -0.0 inputs without misclassifying ±inf (which use the strict > path).
15311538
15321539 When x=0, x is replaced with 1 before the divide solely to avoid NaN/inf; that
1533- intermediate result is discarded by the final where-select in favour of the x=0 branch .
1540+ intermediate result is discarded by the final where-select.
15341541 atan2(0, 0) = 0 by convention.
1535-
1536- IEEE-754 limitations:
1537- - Signed zeros: ``-0.0`` is treated the same as ``+0.0`` because the
1538- ``0 > y`` predicate is false for ``y = -0.0``. Results are numerically
1539- equal to PyTorch for finite inputs but the sign bit may differ
1540- (e.g. ``atan2(-0.0, -1.0)`` returns ``+π`` here, ``-π`` in PyTorch).
1541- - Infinities: ``atan2(±inf, ±inf)`` returns NaN because ``inf/inf``
1542- produces NaN before ``atan`` is applied. PyTorch returns ``±π/4``
1543- or ``±3π/4`` per IEEE-754. Do not pass infinite inputs to this op.
15441542 """
15451543 y , x = _get_operands (values_map , node , [0 , 1 ])
15461544 ele_type = x .type .element_type
15471545
15481546 zero = coreai .constant (0.0 , dtype = ele_type )
1547+ one = coreai .constant (1.0 , dtype = ele_type )
15491548 pi = coreai .constant (np .pi , dtype = ele_type )
1549+ neg_pi = coreai .constant (- np .pi , dtype = ele_type )
15501550 half_pi = coreai .constant (np .pi / 2.0 , dtype = ele_type )
15511551 neg_half_pi = coreai .constant (- np .pi / 2.0 , dtype = ele_type )
1552-
1553- # Avoid division by zero when x = 0 by substituting x = 1 for the ratio.
1552+ quarter_pi = coreai .constant (np .pi / 4.0 , dtype = ele_type )
1553+ neg_quarter_pi = coreai .constant (- np .pi / 4.0 , dtype = ele_type )
1554+ three_quarter_pi = coreai .constant (3.0 * np .pi / 4.0 , dtype = ele_type )
1555+ neg_three_quarter_pi = coreai .constant (- 3.0 * np .pi / 4.0 , dtype = ele_type )
1556+
1557+ # ── signed-zero-aware sign predicates ─────────────────────────────────────
1558+ # 1 / -0.0 = -inf (IEEE-754), so (0 > 1/v) is True iff v = -0.0. Combine with
1559+ # the strict > predicate (handles ±inf and non-zero finites) via OR.
1560+ y_is_zero = coreai .broadcasting_equal (y , zero )
15541561 x_is_zero = coreai .broadcasting_equal (x , zero )
1555- x_safe = coreai .broadcasting_where (
1556- x_is_zero , coreai .constant (1.0 , dtype = ele_type ), x
1562+ y_neg = coreai .broadcasting_or (
1563+ coreai .broadcasting_greater (zero , y ),
1564+ coreai .broadcasting_and (
1565+ y_is_zero ,
1566+ coreai .broadcasting_greater (zero , coreai .broadcasting_divide (one , y )),
1567+ ),
1568+ )
1569+ x_neg = coreai .broadcasting_or (
1570+ coreai .broadcasting_greater (zero , x ),
1571+ coreai .broadcasting_and (
1572+ x_is_zero ,
1573+ coreai .broadcasting_greater (zero , coreai .broadcasting_divide (one , x )),
1574+ ),
1575+ )
1576+ x_is_neg_zero = coreai .broadcasting_and (
1577+ x_is_zero ,
1578+ coreai .broadcasting_greater (zero , coreai .broadcasting_divide (one , x )),
1579+ )
1580+
1581+ # ── both-infinite branch ──────────────────────────────────────────────────
1582+ # atan(inf/inf) = atan(NaN) = NaN; handle before the divide.
1583+ pos_inf = coreai .constant (float ("inf" ), dtype = ele_type )
1584+ neg_inf = coreai .constant (float ("-inf" ), dtype = ele_type )
1585+ x_is_inf = coreai .broadcasting_or (
1586+ coreai .broadcasting_equal (x , pos_inf ), coreai .broadcasting_equal (x , neg_inf )
1587+ )
1588+ y_is_inf = coreai .broadcasting_or (
1589+ coreai .broadcasting_equal (y , pos_inf ), coreai .broadcasting_equal (y , neg_inf )
1590+ )
1591+ both_inf = coreai .broadcasting_and (x_is_inf , y_is_inf )
1592+ inf_result = coreai .broadcasting_where (
1593+ y_neg ,
1594+ coreai .broadcasting_where (x_neg , neg_three_quarter_pi , neg_quarter_pi ),
1595+ coreai .broadcasting_where (x_neg , three_quarter_pi , quarter_pi ),
15571596 )
1558- base = coreai .atan (coreai .broadcasting_divide (y , x_safe ))
15591597
1560- # Quadrant correction: x < 0 shifts the result by ±π.
1561- x_neg = coreai .broadcasting_greater (zero , x )
1562- y_neg = coreai .broadcasting_greater (zero , y )
1563- y_pos = coreai .broadcasting_greater (y , zero )
1598+ # ── x = 0 branch ──────────────────────────────────────────────────────────
1599+ # x = +0: ±π/2 for strictly ±y, 0 when y = 0.
1600+ # x = -0: ±π for all y (y_neg covers y = -0.0 via the 1/y trick above).
1601+ y_pos_strict = coreai .broadcasting_greater (y , zero )
1602+ y_neg_strict = coreai .broadcasting_greater (zero , y )
1603+ pos_x_zero_result = coreai .broadcasting_where (
1604+ y_pos_strict ,
1605+ half_pi ,
1606+ coreai .broadcasting_where (y_neg_strict , neg_half_pi , zero ),
1607+ )
1608+ neg_x_zero_result = coreai .broadcasting_where (y_neg , neg_pi , pi )
1609+ zero_result = coreai .broadcasting_where (
1610+ x_is_neg_zero , neg_x_zero_result , pos_x_zero_result
1611+ )
1612+
1613+ # ── finite nonzero x branch ────────────────────────────────────────────────
1614+ # Avoid division by zero: substitute x = 1 when x = 0; result discarded by
1615+ # the outer where-select.
1616+ x_safe = coreai .broadcasting_where (x_is_zero , one , x )
1617+ base = coreai .atan (coreai .broadcasting_divide (y , x_safe ))
15641618 correction = coreai .broadcasting_where (
15651619 y_neg ,
15661620 coreai .broadcasting_sub (base , pi ),
15671621 coreai .broadcasting_add (base , pi ),
15681622 )
15691623 nonzero_result = coreai .broadcasting_where (x_neg , correction , base )
15701624
1571- # x = 0: result is π/2, −π/2, or 0 based on sign of y.
1572- zero_result = coreai .broadcasting_where (
1573- y_pos ,
1574- half_pi ,
1575- coreai .broadcasting_where (y_neg , neg_half_pi , zero ),
1576- )
1577- return coreai .broadcasting_where (x_is_zero , zero_result , nonzero_result )
1625+ # ── combine ────────────────────────────────────────────────────────────────
1626+ result = coreai .broadcasting_where (x_is_zero , zero_result , nonzero_result )
1627+ return coreai .broadcasting_where (both_inf , inf_result , result )
15781628
15791629
15801630def replace_gather (values_map : dict [str , Value ], node : fx .Node , loc : Location ) -> Value :
0 commit comments