Source code for sherpa.models.op

#
#  Copyright (C) 2024
#  Smithsonian Astrophysical Observatory
#
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License along
#  with this program; if not, write to the Free Software Foundation, Inc.,
#  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

"""
Operator-related routines for the model and parameter classes.
"""

from typing import Any, Callable

import numpy as np


__all__ = ('get_precedences_op', 'get_precedence_expr',
           'get_precedence_lhs', 'get_precedence_rhs',
           )


# We make remainder and floor_divide have the same precedence
# as power (aka **) just to make things clear.
#
lprec : dict[Callable, int]
lprec = {np.power: 4,
         np.remainder: 4,
         np.floor_divide: 4,
         np.multiply: 3,
         np.divide: 3,
         np.true_divide: 3,
         np.add: 2,
         np.subtract: 2
         }
"""Represent the precedence for the left-hand side of a binary operator.

All values must be below the `sherpa.models.op.NO_PRECEDENCE` value.
"""


rprec : dict[Callable, bool]
rprec = {np.power: True,
         np.remainder: True,
         np.floor_divide: True
         }
"""Should the left-hand side of a binary operator be wrapped by brackets?"""


NO_PRECEDENCE : int
NO_PRECEDENCE = 9
"""Represent "no precedence"."""


# Expression terms (used to remove excess brackets from model and
# parameter expressions).
#
[docs] def get_precedences_op(op: Callable) -> tuple[int, bool]: """Return precedences for the operation. Unrecognized parameters are mapped to (`sherpa.models.op.NO_PRECEDENCE`, False). Parameters ---------- op : callable The operator (e.g. np.multiply or np.power). Returns ------- lprec, rprec : (int, bool) The left and right "precedences" (the right case only cares about being set or not hence we use a boolean). See Also -------- get_precedence_expr, get_precedence_lhs, get_precedence_rhs Notes ----- This uses the module-level symbols: `sherpa.models.op.lprec` and `sherpa.models.op.rprec`. """ return lprec.get(op, NO_PRECEDENCE), rprec.get(op, False)
# Typing is hard to get right here given that we want to allow this # for both models and parameters. #
[docs] def get_precedence_expr(expr: Any) -> int: """Return precedence for the expression. This is the precedence of the operator in the expression, if one exists. Parameters ---------- expr : Model or Parameter The expression. It may have a .opprec field. Returns ------- prec : int The "precedence" value `sherpa.models.op.NO_PRECEDENCE` is returned if expr has an unknown operator or it does not contain an operator. See Also -------- get_precedences_op, get_precedence_lhs, get_precedence_rhs """ try: return expr.opprec except AttributeError: return NO_PRECEDENCE
[docs] def get_precedence_lhs(lstr: str, lp: int, p: int, a: bool) -> str: """Return the string to use for the left side of a binary operator. This is only needed when the operator (represented by the p value) is written in infix form - such as 'a + b'- rather than prefix form like 'np.add(a, b)'. Parameters ---------- lstr : str The term to the left of the operator. lp : int Precedences of any operator in lstr. p : int Precedences of the current operator. a : bool Do we care about power-like terms. Returns ------- term : str Either lstr or (lstr). See Also -------- get_precedences_op, get_precedence_expr, get_precedence_rhs Examples -------- >>> get_precedence_lhs("a", NO_PRECEDENCE, lprec[np.divide], False) 'a' >>> get_precedence_lhs("a + b", lprec[np.add], lprec[np.divide], False) '(a + b)' >>> get_precedence_lhs("a * b", lprec[np.multiply], lprec[np.add], False) 'a * b' """ if lp < p: return f"({lstr})" if not a: return lstr # We could combine all these into one, but for now keep # them separate. # if lp == p: # For now ensure the left term is bracketed just to be # clear. # return f"({lstr})" if lstr[0] == "-": # If the term is negative then ensure it is included # in a bracket before passed through to the power # term. Python has "-a ** 2" actually mapping to "-(a # ** 2)" so we need to say "(-a) ** 2" if we really # want the unary operator before the power term. # return f"({lstr})" return lstr
[docs] def get_precedence_rhs(rstr: str, opstr: str, rp: int, p: int) -> str: """Return the string to use for the right side of a binary operator. This is only needed when the operator (represented by the p value) is written in infix form - such as 'a + b'- rather than prefix form like 'np.add(a, b)'. Parameters ---------- rstr : str The term to the right of the operator. opstr : str The string representing the operator. rp : int Precedences of any operator in rstr. p : int Precedences of the current operator. Returns ------- term : str Either rstr or (rstr). See Also -------- get_precedences_op, get_precedence_expr, get_precedence_lhs Examples -------- >>> get_precedence_rhs("a", "/", NO_PRECEDENCE, lprec[np.divide]) 'a' >>> get_precedence_rhs("a + b", "/", lprec[np.add], lprec[np.divide]) '(a + b)' """ if opstr in ["+", "*"]: condition = rp < p else: condition = rp <= p if condition: return f"({rstr})" return rstr