printer.py
1 r""" 2 ANSI Enhanced Text Printing, Text Printer and LaTeX Printer for all Geometric Algebra classes 3 4 :math:`\LaTeX` printing 5 ----------------------- 6 7 .. note:: 8 9 :mod:`galgebra` works out of the box with the usual 10 :ref:`sympy printing <sympy:tutorial-printing>` and will show Latex in 11 IPython by default. In many cases, all that is needed is:: 12 13 sympy.init_printing( 14 use_latex='mathjax', 15 latex_printer=galgebra.printer.latex, 16 # described below in `GaLatexPrinter` 17 omit_function_args=True, 18 omit_partial_derivative_fraction=True, 19 ) 20 21 The rest of this section primarily describes an orthogonal feature for 22 writing out ``.tex`` files with :func:`print`. 23 24 The latex printer is turned on with the :func:`Format` function 25 26 .. function:: Format(Fmode=True, Dmode=True, ipy=False) 27 28 where ``Fmode`` is the function printing mode that suppresses printing arguments, 29 ``Dmode`` is the derivative printing mode that does not use fractions, and 30 ``ipy=True`` is the IPython notebook mode that does not redirect the print output. 31 32 The latex output is post processed and displayed with the function 33 34 .. function:: xpdf(filename='tmplatex.tex', debug=False) 35 36 where ``filename`` is the name of the tex file one would keep for future 37 inclusion in documents and ``debug=True`` would display the tex file 38 immediately. 39 40 There are three options for printing multivectors in latex. They are 41 accessed with the multivector member function 42 43 .. function:: galgebra.mv.Mv.Fmt(self, fmt=1, title=None) 44 45 where ``fmt`` of 1, 2, or 3 determines whether the entire multivector A is 46 printed entirely on one line, or one grade is printed per line, or 47 one base is printed per line. If ``title`` is not None then the latex 48 string generated is of the form:: 49 50 title + ' = ' + str(A) 51 52 where it is assumed that title is a latex math mode string. If title 53 contains '%' it is treated as a pure latex math mode string. If it 54 does not contain '%' then the following character mappings are applied:: 55 56 'grad' -> '\bm{\nabla} ' 57 '*' -> '' 58 '^' -> '\W ' 59 '|' -> '\cdot ' 60 '>' -> '\lfloor ' 61 '<' -> '\rfloor ' 62 63 In the case of a print statement of the form:: 64 65 print(title, A) 66 67 everything in the title processing still applies except that the multivector 68 formatting is one multivector per line. 69 70 For print statements of the form:: 71 72 print(title) 73 74 where no program variables are printed if title contains `#` then title 75 is printed as regular latex line. If title does not contain `#` then 76 title is printed in equation mode. `%` has the same effect in title as 77 in the ``Fmt()`` member function. 78 """ 79 80 import copy 81 import os 82 import sys 83 import io 84 import builtins 85 import functools 86 import inspect 87 import re 88 import shutil 89 import warnings 90 from collections import ChainMap 91 92 from sympy import MatrixBase, Basic, S, Symbol, Function, Derivative, Pow 93 from sympy.printing.str import StrPrinter 94 from sympy.printing.conventions import split_super_sub 95 from sympy.printing.latex import ( 96 LatexPrinter, accepted_latex_functions, other_symbols 97 ) 98 from sympy.core.function import _coeff_isneg 99 from sympy.core.operations import AssocOp 100 from sympy import init_printing 101 from sympy.core.alphabets import greeks 102 103 try: 104 from IPython.display import display, Latex, Math, display_latex 105 except ImportError: 106 pass 107 try: 108 from sympy.interactive import printing 109 except ImportError: 110 pass 111 112 from inspect import getouterframes, currentframe 113 114 from ._utils import parser as _parser 115 from ._utils.printable import Printable as SympyPrintable 116 117 ZERO_STR = ' 0 ' 118 119 Format_cnt = 0 120 121 ip_cmds = r""" 122 $\DeclareMathOperator{\Tr}{Tr} 123 \DeclareMathOperator{\Adj}{Adj} 124 \newcommand{\bfrac}[2]{\displaystyle\frac{#1}{#2}} 125 \newcommand{\lp}{\left (} 126 \newcommand{\rp}{\right )} 127 \newcommand{\paren}[1]{\lp {#1} \rp} 128 \newcommand{\half}{\frac{1}{2}} 129 \newcommand{\llt}{\left <} 130 \newcommand{\rgt}{\right >} 131 \newcommand{\abs}[1]{\left |{#1}\right | } 132 \newcommand{\pdiff}[2]{\bfrac{\partial {#1}}{\partial {#2}}} 133 \newcommand{\npdiff}[3]{\bfrac{\partial^{#3} {#1}}{\partial {#2}^{#3}}} 134 \newcommand{\lbrc}{\left \{} 135 \newcommand{\rbrc}{\right \}} 136 \newcommand{\W}{\wedge} 137 \newcommand{\prm}[1]{{#1}'} 138 \newcommand{\ddt}[1]{\bfrac{d{#1}}{dt}} 139 \newcommand{\R}{\dagger} 140 \newcommand{\deriv}[3]{\bfrac{d^{#3}#1}{d{#2}^{#3}}} 141 \newcommand{\grade}[1]{\left < {#1} \right >} 142 \newcommand{\f}[2]{{#1}\lp {#2} \rp} 143 \newcommand{\eval}[2]{\left . {#1} \right |_{#2}}$ 144 """ 145 146 SYS_CMD = {'linux2': {'rm': 'rm', 'evince': 'evince', 'null': ' > /dev/null', '&': '&'}, 147 'linux': {'rm': 'rm', 'evince': 'evince', 'null': ' > /dev/null', '&': '&'}, 148 'win32': {'rm': 'del', 'evince': 'start', 'null': ' > NUL', '&': ''}, 149 'darwin': {'rm': 'rm', 'evince': 'open', 'null': ' > /dev/null', '&': '&'}} 150 151 152 def isinteractive(): #Is ipython running 153 """ 154 We will assume that if ipython is running then jupyter notebook is 155 running. 156 """ 157 try: 158 __IPYTHON__ 159 return True 160 except NameError: 161 return False 162 163 164 def ostr(obj, dict_mode=False, indent=True): 165 return GaPrinter(dict(dict_mode=dict_mode)).doprint(obj) 166 167 168 def find_functions(expr): 169 f_lst = [] 170 for f in list(expr.atoms(Function)): 171 if str(f) not in GaPrinter.function_names: 172 f_lst.append(f) 173 f_lst += list(expr.atoms(Derivative)) 174 return f_lst 175 176 177 def coef_simplify(expr): 178 # fcts = find_functions(expr) 179 # return expr.collect(fcts) 180 return expr 181 182 183 def oprint(*args, dict_mode=False): 184 """ 185 Debug printing for iterated (list/tuple/dict/set) objects. args is 186 of form ``(title1, object1, title2, object2, ...)`` and prints:: 187 188 title1 = object1 189 title2 = object2 190 ... 191 192 If you only wish to print a title set ``object = None``. 193 """ 194 195 if isinstance(args[0], str) or args[0] is None: 196 titles = args[0::2] 197 objs = args[1::2] 198 strs = [ 199 ostr(obj, dict_mode) if obj is not None else None 200 for obj in objs 201 ] 202 n = max(( 203 len(title) 204 for title, s in zip(titles, strs) 205 if s is not None and '\n' not in s 206 ), default=0) 207 208 for title, s in zip(titles, strs): 209 if s is None: 210 print(title) 211 else: 212 npad = n - len(title) 213 if '\n' in s: 214 print(title + ':\n' + s) 215 else: 216 print(title + npad * ' ' + ' = ' + s) 217 else: 218 for arg in args: 219 print(ostr(arg, dict_mode)) 220 221 222 _ansi_colors = { 223 'black': '\033[0;30m', 'dark gray': '\033[1;30m', 224 'red': '\033[0;31m', 'bright red': '\033[1;31m', 225 'green': '\033[0;32m', 'bright green': '\033[1;32m', 226 'yellow': '\033[0;33m', 'bright yellow': '\033[1;33m', 227 'blue': '\033[0;34m', 'bright blue': '\033[1;34m', 228 'purple': '\033[0;35m', 'bright purple': '\033[1;35m', 229 'cyan': '\033[0;36m', 'bright cyan': '\033[1;36m', 230 'bright gray': '\033[0;37m', 'white': '\033[1;37m', 231 } 232 _ansi_reset = '\033[0m' 233 234 235 def _apply_ansi_color(color: str, text: str) -> str: 236 if color is None: 237 return text 238 else: 239 return _ansi_colors.get(color, color) + text + _ansi_reset 240 241 242 def enhance_print(base='blue', fct='red', deriv='cyan'): 243 """ Enable ansi color codes in plain-text formatting. 244 245 Valid color names are: 246 247 {colors} 248 249 Pass ``None`` to disable coloring. 250 """ 251 GaPrinter.set_global_settings( 252 function_color=fct, 253 derivative_color=deriv, 254 basis_vector_color=base, 255 ) 256 257 258 # patch the docstring using our known color names 259 enhance_print.__doc__ = enhance_print.__doc__.format(colors='\n '.join( 260 " - ``{!r}``".format(k) for k in _ansi_colors 261 )) 262 263 264 def Eprint(*args, **kwargs): 265 """ Alias for :func:`enhance_print` """ 266 return enhance_print(*args, **kwargs) 267 268 269 class GaPrinter(StrPrinter): 270 """ 271 This subclass of the builtin string printer makes some customizations which 272 make output a little more readable for GA usage. 273 274 The customizations are: 275 276 * :class:`~sympy.core.function.Derivative` objects are printed as ``D{x}y`` 277 instead of ``Derivative(y, x)``. 278 * :class:`~sympy.core.function.Function` objects are printed without 279 arguments. This is useful for defining fields over 280 :attr:`~galgebra.ga.Ga.coords`, but sometimes misfires. 281 * A new ``dict_mode`` setting, which when ``True`` prints :class:`dict` 282 objects with ``->`` and one entry per line. 283 * New ANSI color settings: 284 285 * ``derivative_color``, for adjusting the color of ``D{x}``. 286 * ``function_color``, for adjusting the color of argument-less functions. 287 * ``basis_vector_color``, for adjusting the color of basis vector symbols. 288 289 When :mod:`galgebra.printer` is imported, builtin sympy objects are patched 290 to use this printer for their ``__repr__`` instead of the builtin 291 :class:`~sympy.printing.str.StrPrinter`. There is currently no way to 292 disable this patching. 293 """ 294 295 _default_settings = ChainMap({ 296 # if true, print dicts with `->` instead of `:`, one entry per line 297 "dict_mode": False, 298 "derivative_color": None, 299 "function_color": None, 300 "basis_vector_color": None, 301 }, StrPrinter._default_settings) 302 303 function_names = ('acos', 'acosh', 'acot', 'acoth', 'arg', 'asin', 'asinh', 304 'atan', 'atan2', 'atanh', 'ceiling', 'conjugate', 'cos', 305 'cosh', 'cot', 'coth', 'exp', 'floor', 'im', 'log', 're', 306 'root', 'sin', 'sinh', 'sqrt', 'sign', 'tan', 'tanh', 'Abs') 307 308 def _print_Function(self, expr): 309 name = expr.func.__name__ 310 311 if expr.func.nargs is not None: 312 if name in GaPrinter.function_names: 313 return expr.func.__name__ + "(%s)" % self.stringify(expr.args, ", ") 314 315 return _apply_ansi_color( 316 self._settings["function_color"], "%s" % (name,)) 317 318 def _print_BasisVectorSymbol(self, expr): 319 return _apply_ansi_color( 320 self._settings["basis_vector_color"], self._print_Symbol(expr)) 321 322 def _print_Derivative(self, expr): 323 # Break the following to support both py 2 & 3 324 # function, *diff_args = expr.args 325 function = expr.args[0] 326 diff_args = expr.args[1:] 327 328 xi = [] 329 ni = [] 330 for x, n in diff_args: 331 if x in xi: 332 i = xi.index(x) 333 ni[i] += n 334 else: 335 xi.append(self._print(x)) 336 ni.append(n) 337 338 s = 'D' 339 for x, n in zip(xi, ni): 340 s += '{' + str(x) + '}' 341 if n > 1: 342 s += '^' + str(n) 343 s += str(self._print(function)) 344 return _apply_ansi_color(self._settings["derivative_color"], s) 345 346 def _print_dict(self, expr): 347 if not self._settings['dict_mode']: 348 return super()._print_dict(expr) 349 350 return '\n'.join( 351 '{} -> {}'.format(self._print(k), self._print(v)) 352 for k, v in expr.items() 353 ) 354 355 356 # Inheriting from SympyPrintable ensure we take part in interactive printing 357 # customization 358 class GaPrintable(SympyPrintable): 359 """ Mixin class providing default implementations of printing hooks """ 360 def __ga_print_str__(self): 361 if GaLatexPrinter.latex_flg: 362 return GaLatexPrinter().doprint(self) 363 else: 364 return GaPrinter().doprint(self) 365 366 def __repr__(self): 367 return GaPrinter().doprint(self) 368 369 370 # Change sympy builtins to use our printer by default. 371 # We do this because we always have done, and stopping now would break 372 # compatibility. 373 if issubclass(Basic, SympyPrintable): 374 SympyPrintable.__ga_print_str__ = GaPrintable.__ga_print_str__ 375 SympyPrintable.__repr__ = GaPrintable.__repr__ 376 else: 377 # sympy < 1.7 378 Basic.__ga_print_str__ = GaPrintable.__ga_print_str__ 379 Basic.__repr__ = GaPrintable.__repr__ 380 381 MatrixBase.__ga_print_str__ = GaPrintable.__ga_print_str__ 382 MatrixBase.__repr__ = GaPrintable.__repr__ 383 384 385 # This is the lesser of two evils. Previously, we overwrote `Basic.__str__` in 386 # order to customise `print(sympy)`. This broke a bunch of assumptions inside 387 # sympy, so isn't safe. Instead of clobbering `__str__`, we add a 388 # `__ga_print_str__` attribute, and have `print` use it if present. 389 _old_print = builtins.print 390 391 392 @functools.wraps(_old_print) 393 def _print(*values, **kwargs): 394 values_new = [] 395 for v in values: 396 try: 397 f = type(v).__ga_print_str__ 398 except AttributeError: 399 values_new.append(v) 400 else: 401 values_new.append(f(v)) 402 _old_print(*values_new, **kwargs) 403 404 405 builtins.print = _print 406 407 408 class GaLatexPrinter(LatexPrinter): 409 r""" 410 This subclass of the builtin string printer makes some customizations which 411 make output a little more readable for GA usage. 412 413 The customizations are: 414 415 * A new ``omit_partial_derivative_fraction`` setting that affects the 416 printing of :class:`~sympy.core.function.Derivative` objects, with 417 possible values: 418 419 * ``False``, to use the *sympy* default, :math:`\pdiff{f}{x}`. 420 * ``True``, to use a condensed notation, :math:`\partial_{x}f`. 421 422 * A new ``omit_function_args`` setting which affects the printing of 423 :class:`~sympy.core.function.Function` objects, with possible values: 424 425 * ``False``, to use the sympy default, :math:`{{f}\lp {x,y,z} \rp }`. 426 * ``True``, to print as :math:`f`. This is similar to the behavior of 427 :class:`GaPrinter`. 428 429 * A change to function printing to allow function names to contain 430 subscripts and superscripts. 431 432 * Use of ``boldsymbol`` instead of ``mathbf`` for bold symbol names. 433 434 Note that this printer is not *required* for using GA objects, the base 435 class printer will work fine too. 436 """ 437 # overrides of base class settings, and new settings for our printers 438 _default_settings = ChainMap({ 439 "mat_str": "array", 440 "omit_function_args": False, 441 "omit_partial_derivative_fraction": False, 442 }, LatexPrinter._default_settings) 443 444 latex_flg = False 445 latex_str = '' 446 ipy = False 447 448 preamble = \ 449 """ 450 \\pagestyle{empty} 451 \\usepackage[latin1]{inputenc} 452 \\usepackage{amsmath} 453 \\usepackage{amsfonts} 454 \\usepackage{amssymb} 455 \\usepackage{amsbsy} 456 \\usepackage{tensor} 457 \\usepackage{listings} 458 \\usepackage{color} 459 \\usepackage{xcolor} 460 \\usepackage{bm} 461 \\usepackage{breqn} 462 \\definecolor{gray}{rgb}{0.95,0.95,0.95} 463 \\setlength{\\parindent}{0pt} 464 \\DeclareMathOperator{\\Tr}{Tr} 465 \\DeclareMathOperator{\\Adj}{Adj} 466 \\newcommand{\\bfrac}[2]{\\displaystyle\\frac{#1}{#2}} 467 \\newcommand{\\lp}{\\left (} 468 \\newcommand{\\rp}{\\right )} 469 \\newcommand{\\paren}[1]{\\lp {#1} \\rp} 470 \\newcommand{\\half}{\\frac{1}{2}} 471 \\newcommand{\\llt}{\\left <} 472 \\newcommand{\\rgt}{\\right >} 473 \\newcommand{\\abs}[1]{\\left |{#1}\\right | } 474 \\newcommand{\\pdiff}[2]{\\bfrac{\\partial {#1}}{\\partial {#2}}} 475 \\newcommand{\\lbrc}{\\left \\{} 476 \\newcommand{\\rbrc}{\\right \\}} 477 \\newcommand{\\W}{\\wedge} 478 \\newcommand{\\prm}[1]{{#1}'} 479 \\newcommand{\\ddt}[1]{\\bfrac{d{#1}}{dt}} 480 \\newcommand{\\R}{\\dagger} 481 \\newcommand{\\deriv}[3]{\\bfrac{d^{#3}#1}{d{#2}^{#3}}} 482 \\newcommand{\\grade}[1]{\\left < {#1} \\right >} 483 \\newcommand{\\f}[2]{{#1}\\lp{#2}\\rp} 484 \\newcommand{\\eval}[2]{\\left . {#1} \\right |_{#2}} 485 \\newcommand{\\Nabla}{\\boldsymbol{\\nabla}} 486 \\newcommand{\\eb}{\\boldsymbol{e}} 487 \\usepackage{float} 488 \\floatstyle{plain} % optionally change the style of the new float 489 \\newfloat{Code}{H}{myc} 490 \\lstloadlanguages{Python} 491 492 \\begin{document} 493 """ 494 postscript = '\\end{document}\n' 495 macros = '\\newcommand{\\f}[2]{{#1}\\left ({#2}\\right )}' 496 497 # Used by _print_Symbol 498 greek_translated = {'lamda': 'lambda', 'Lamda': 'Lambda'} 499 other = other_symbols | {'infty'} 500 special_alphabet = list(reversed(sorted(list(greeks) + list(other), key=len))) 501 502 @staticmethod 503 def redirect(): 504 GaLatexPrinter.latex_flg = True 505 if GaLatexPrinter.ipy: 506 pass 507 else: 508 GaLatexPrinter.stdout = sys.stdout 509 sys.stdout = io.StringIO() 510 511 @staticmethod 512 def restore(): 513 if GaLatexPrinter.latex_flg: 514 if not GaLatexPrinter.ipy: 515 GaLatexPrinter.latex_str += sys.stdout.getvalue() 516 GaLatexPrinter.latex_flg = False 517 if not GaLatexPrinter.ipy: 518 sys.stdout = GaLatexPrinter.stdout 519 520 def _print_Pow(self, expr): 521 base = self._print(expr.base) 522 if ('_' in base or '^' in base) and 'cdot' not in base: 523 mode = True 524 else: 525 mode = False 526 527 # Treat x**Rational(1, n) as special case 528 if expr.exp.is_Rational and abs(expr.exp.p) == 1 and expr.exp.q != 1: 529 #base = self._print(expr.base) 530 expq = expr.exp.q 531 532 if expq == 2: 533 tex = r"\sqrt{%s}" % base 534 elif self._settings['itex']: 535 tex = r"\root{%d}{%s}" % (expq, base) 536 else: 537 tex = r"\sqrt[%d]{%s}" % (expq, base) 538 539 if expr.exp.is_negative: 540 return r"\frac{1}{%s}" % tex 541 else: 542 return tex 543 elif self._settings['fold_frac_powers'] \ 544 and expr.exp.is_Rational \ 545 and expr.exp.q != 1: 546 base, p, q = self._print(expr.base), expr.exp.p, expr.exp.q 547 if mode: 548 return r"{\left ( %s \right )}^{%s/%s}" % (base, p, q) 549 else: 550 return r"%s^{%s/%s}" % (base, p, q) 551 552 elif expr.exp.is_Rational and expr.exp.is_negative and expr.base.is_Function: 553 # Things like 1/x 554 return r"\frac{%s}{%s}" % \ 555 (1, self._print(Pow(expr.base, -expr.exp))) 556 else: 557 if expr.base.is_Function: 558 return r"{%s}^{%s}" % (self._print(expr.base), self._print(expr.exp)) 559 else: 560 if expr.is_commutative and expr.exp == -1: 561 #solves issue 1030 562 #As Mul always simplify 1/x to x**-1 563 #The objective is achieved with this hack 564 #first we get the latex for -1 * expr, 565 #which is a Mul expression 566 tex = self._print(S.NegativeOne * expr).strip() 567 #the result comes with a minus and a space, so we remove 568 if tex[:1] == "-": 569 return tex[1:].strip() 570 if self._needs_brackets(expr.base): 571 tex = r"\left(%s\right)^{%s}" 572 else: 573 if mode: 574 tex = r"{\left ( %s \right )}^{%s}" 575 else: 576 tex = r"%s^{%s}" 577 578 return tex % (self._print(expr.base), 579 self._print(expr.exp)) 580 581 def _print_Symbol(self, expr, style='plain'): 582 583 def str_symbol(name_str): 584 585 def translate(s): 586 tmp = s 587 588 parse_dict = {} 589 i_sub = 1 590 591 for glyph in GaLatexPrinter.special_alphabet: 592 escaped_glyph = '\\' + glyph 593 if glyph in tmp: 594 parse_sym = '????' + str(i_sub) 595 i_sub += 1 596 # If this glyph is already escaped, avoid escaping again 597 translated_glyph = (escaped_glyph + ' ') if escaped_glyph not in tmp else glyph 598 parse_dict[parse_sym] = translated_glyph 599 tmp = tmp.replace(glyph, parse_sym) 600 601 for parse_sym in parse_dict: 602 tmp = tmp.replace(parse_sym, parse_dict[parse_sym]) 603 604 for glyph in GaLatexPrinter.greek_translated: 605 if glyph in tmp: 606 tmp = tmp.replace(glyph, GaLatexPrinter.greek_translated[glyph]) 607 608 return tmp 609 610 name, supers, subs = split_super_sub(name_str) 611 612 name = translate(name) 613 614 if style == 'bold': 615 name = '\\boldsymbol{' + name + '}' 616 617 supers = list(map(translate, supers)) 618 subs = list(map(translate, subs)) 619 620 # glue all items together: 621 if len(supers) > 0: 622 name += "^{%s}" % " ".join(supers) 623 if len(subs) > 0: 624 name += "_{%s}" % " ".join(subs) 625 626 return name 627 628 if expr in self._settings['symbol_names']: 629 return self._settings['symbol_names'][expr] 630 631 return str_symbol(expr.name) 632 633 def _print_Function(self, expr, exp=None): 634 635 func = expr.func.__name__ 636 name = func 637 if hasattr(self, '_print_' + func): 638 return getattr(self, '_print_' + func)(expr, exp) 639 else: 640 args = [str(self._print(arg)) for arg in expr.args] 641 642 # How inverse trig functions should be displayed, formats are: 643 # abbreviated: asin, full: arcsin, power: sin^-1 644 inv_trig_style = self._settings['inv_trig_style'] 645 # If we are dealing with a power-style inverse trig function 646 inv_trig_power_case = False 647 # If it is applicable to fold the argument brackets 648 can_fold_brackets = self._settings['fold_func_brackets'] and \ 649 len(args) == 1 and not self._needs_function_brackets(expr.args[0]) 650 651 inv_trig_table = ["asin", "acos", "atan", "acot", "acosh", "asinh", "atanh"] 652 653 # If the function is an inverse trig function, handle the style 654 if func in inv_trig_table: 655 if inv_trig_style == "abbreviated": 656 func = func 657 elif inv_trig_style == "full": 658 func = "arc" + func[1:] 659 elif inv_trig_style == "power": 660 func = func[1:] 661 inv_trig_power_case = True 662 663 # Can never fold brackets if we're raised to a power 664 if exp is not None: 665 can_fold_brackets = False 666 667 if inv_trig_power_case: 668 if func in accepted_latex_functions: 669 name = r"\%s^{-1}" % func 670 else: 671 name = r"\operatorname{%s}^{-1}" % func 672 elif exp is not None: 673 if func in accepted_latex_functions: 674 name = r"\%s^{%s}" % (func, exp) 675 else: 676 name = latex(Symbol(func)) + ' ' 677 if '_' in func or '^' in func: 678 name = r'{\left ( ' + name + r'\right ) }^{' + exp + '}' 679 else: 680 name += '^{' + exp + '}' 681 else: 682 if func in accepted_latex_functions: 683 name = r"\%s" % func 684 else: 685 name = latex(Symbol(func)) + ' ' 686 if exp is not None: 687 if '_' in name or '^' in name: 688 name = r'\left ( ' + name + r'\right )^{' + exp + '}' 689 else: 690 name += '^{' + exp + '}' 691 692 if can_fold_brackets: 693 if func in accepted_latex_functions: 694 # Wrap argument safely to avoid parse-time conflicts 695 # with the function name itself 696 name += r" {%s}" 697 else: 698 if not self._settings["omit_function_args"]: 699 name += r"%s" 700 else: 701 if func in accepted_latex_functions or not self._settings["omit_function_args"]: 702 name += r"{\left (%s \right )}" 703 704 if inv_trig_power_case and exp is not None: 705 name += r"^{%s}" % exp 706 707 if func in accepted_latex_functions or not self._settings["omit_function_args"]: 708 if len(args) == 1: 709 name = name % args[0] 710 else: 711 name = name % ",".join(args) 712 713 return name 714 715 def _print_Derivative(self, expr): 716 dim = len(expr.variables) 717 imax = 1 718 if dim == 1: 719 if self._settings["omit_partial_derivative_fraction"]: 720 tex = r"\partial_{%s}" % self._print(expr.variables[0]) 721 else: 722 tex = r"\frac{\partial}{\partial %s}" % self._print(expr.variables[0]) 723 else: 724 multiplicity, i, tex = [], 1, "" 725 current = expr.variables[0] 726 for symbol in expr.variables[1:]: 727 if symbol == current: 728 i = i + 1 729 else: 730 multiplicity.append((current, i)) 731 current, i = symbol, 1 732 else: 733 imax = max(imax, i) 734 multiplicity.append((current, i)) 735 736 if self._settings["omit_partial_derivative_fraction"]: 737 tex = '' 738 for x, i in multiplicity: 739 if i == 1: 740 tex += r"\partial_{%s}" % (self._print(x),) 741 else: 742 tex += r"\partial^{%i}_{%s}" % (i, self._print(x)) 743 else: 744 for x, i in multiplicity: 745 if i == 1: 746 tex += r"\partial %s" % self._print(x) 747 else: 748 tex += r"\partial^{%s} %s" % (i, self._print(x)) 749 tex = r"\frac{\partial^{%s}}{%s} " % (dim, tex) 750 751 if isinstance(expr.expr, AssocOp): 752 s = r"%s\left(%s\right)" % (tex, self._print(expr.expr)) 753 else: 754 s = r"%s %s" % (tex, self._print(expr.expr)) 755 return s 756 757 def _print_Determinant(self, expr): 758 # sympy `uses |X|` by default, we want `det (X)` 759 return r"\det\left ( {}\right )".format(self._print(expr.args[0])) 760 761 @staticmethod 762 def latex(expr, **settings): 763 764 if not isinstance(expr, list): 765 return GaLatexPrinter(settings).doprint(expr) 766 else: 767 s = '\\begin{align*}' 768 for x in expr: 769 s += '\n & ' + latex(x) + ' \\\\' 770 s += '\n\\end{align*}' 771 return s 772 773 774 def latex(expr, **settings) -> str: 775 """ 776 Get the latex representation of expr using :class:`GaLatexPrinter`. 777 778 Takes the same options as :func:`sympy.printing.latex.latex`; see that 779 function for more information. 780 781 This can be used as the ``latex_printer`` argument to 782 :func:`~sympy.interactive.printing.init_printing` to make IPython always 783 use :class:`GaLatexPrinter`. 784 """ 785 return GaLatexPrinter(settings).doprint(expr) 786 787 788 def print_latex(expr, **settings): 789 """Prints LaTeX representation of the given expression. 790 791 Takes the same settings as :func:`latex`.""" 792 print(latex(expr, **settings)) 793 794 795 def Format(Fmode: bool = True, Dmode: bool = True, inverse='full'): 796 r""" 797 Turns on latex printing with configurable options. 798 799 This redirects printer output so that latex compiler can capture it. 800 801 ``Format()`` is also required for printing from *ipython notebook* (note that ``xpdf()`` is not needed to print from *ipython notebook*). 802 803 Parameters 804 ---------- 805 Fmode: 806 Value for the ``omit_function_args`` setting of 807 :class:`GaLatexPrinter`. 808 Dmode: 809 Value for the ``omit_partial_derivative_fraction`` setting of 810 :class:`GaLatexPrinter`. 811 """ 812 global Format_cnt 813 814 GaLatexPrinter.set_global_settings( 815 omit_partial_derivative_fraction=Dmode, 816 omit_function_args=Fmode, 817 inv_trig_style=inverse, 818 ) 819 820 if Format_cnt == 0: 821 Format_cnt += 1 822 823 GaLatexPrinter.latex_flg = True 824 GaLatexPrinter.redirect() 825 826 if isinteractive(): 827 init_printing( 828 use_latex='mathjax', 829 latex_mode='equation*', 830 latex_printer=latex, 831 # Affects only the plaintext printing, and makes our printing 832 # tests easier to maintain 833 wrap_line=False, 834 ) 835 836 return 837 838 839 def _texify(s: str) -> str: 840 """ Convert python GA operator notation to LaTeX """ 841 repl_pairs = [ 842 (r'\|', r'\cdot '), 843 (r'\^(?!{)', r'\W '), 844 (r'\*', ' '), 845 (r'\brgrad\b', r'\bar{\boldsymbol{\nabla}} '), 846 (r'\bgrad\b', r'\boldsymbol{\nabla} '), 847 (r'>>', r' \times '), 848 (r'<<', r' \bar{\times} '), 849 (r'<', r'\rfloor '), 850 (r'>', r'\lfloor '), 851 ] 852 853 def repl_func(m): 854 # only one group will be present, use the corresponding match 855 return next( 856 r 857 for (p, r), g in zip(repl_pairs, m.groups()) 858 if g is not None 859 ) 860 pattern = '|'.join("({})".format(p) for p, _ in repl_pairs) 861 return re.sub(pattern, repl_func, s) 862 863 864 def tex(paper=(14, 11), debug=False, prog=False, pt='10pt'): 865 r""" 866 Post processes LaTeX output (see comments below), adds preamble and 867 postscript. 868 869 This postprocessing has two main behaviors: 870 871 1. Converting strings on the left hand side of the last ``=`` into TeX. 872 This translates the ``*``, ``^``, ``|``, ``>``, ``<``, ``<<``, ``>>``, 873 ``grad``, and ``rgrad`` operators of galgebra into the appropriate latex 874 operators. If there is no ``=`` in the line, no conversion is applied. 875 876 2. Wrapping lines of latex into ``equation*`` environments if they are not 877 already in environments, and moving labels that were prepended outside 878 ``align`` environments inside those environments. 879 880 Both behaviors are applied line by line, unless a line starts with the 881 following text: 882 883 ``#%`` or ``%`` 884 Disables only behavior 1 for the rest of the line. 885 886 ``##`` 887 Disables behaviors 1 and 2 until the end of the next line starting with 888 ``##``. This includes processing any of the other special characters, 889 which will be emitted verbatim. 890 891 ``#`` 892 Disables behaviors 1 and 2 for the rest of the line. 893 894 We assume that if :func:`tex` is called, then :func:`Format` has been called 895 at the beginning of the program. 896 """ 897 898 latex_str = GaLatexPrinter.latex_str + sys.stdout.getvalue() 899 GaLatexPrinter.latex_str = '' 900 GaLatexPrinter.restore() 901 r""" 902 Each line in the latex_str is interpreted to be an equation or align 903 environment. If the line does not begin with '\begin{align*}' then 904 'begin{equation*}' will be added to the beginning of the line and 905 '\end{equation*}' to the end of the line. 906 The latex strings generated by galgebra and sympy expressions for 907 printing must not contain '\n' except as the final character. Thus 908 all '\n' must be removed from a compound (not a simple type) expression 909 and a '\n' added to the end of the string to delimit it when the string 910 is generated. 911 """ 912 latex_lst = latex_str.split('\n') 913 latex_str = '' 914 915 code_flg = False 916 917 for latex_line in latex_lst: 918 if not latex_line: 919 pass 920 elif latex_line.startswith('##'): 921 # a post-processing toggle used by `Print_Function` 922 code_flg = not code_flg 923 latex_line = latex_line[2:] 924 elif code_flg: 925 pass 926 elif latex_line.startswith('#') and not latex_line.startswith('#%'): 927 # do not process this line 928 latex_line = latex_line[1:] 929 else: 930 # two different spellings of "do not process the LHS" 931 if latex_line.startswith('%'): 932 latex_line = latex_line[1:] 933 elif latex_line.startswith('#%'): 934 latex_line = latex_line[2:] 935 # otherwise, process it if we can find it 936 elif '=' in latex_line: 937 lhs, latex_line = latex_line.rsplit('=', 1) 938 latex_line = _texify(lhs) + '=' + latex_line 939 940 # in either case, perform the environment wrapping 941 if r'\begin{align*}' in latex_line: 942 latex_line = r'\begin{align*} ' + latex_line.replace(r'\begin{align*}', '', 1).lstrip() 943 else: 944 latex_line = r'\begin{equation*} ' + latex_line.strip() + r' \end{equation*}' 945 946 latex_str += latex_line + '\n' 947 948 latex_str = latex_str.replace('\n\n', '\n') 949 950 if prog: 951 with open(sys.argv[0], 'r') as prog_file: 952 prog_str = prog_file.read() 953 prog_str = '{\\Large \\bf Program:}\\begin{lstlisting}[language=Python,showspaces=false,' + \ 954 'showstringspaces=false]\n' + \ 955 prog_str + '\n\\end{lstlisting}\n {\\Large \\bf Code Output:} \n' 956 latex_str = prog_str + latex_str 957 958 if debug: 959 print(latex_str) 960 961 if paper == 'letter': 962 paper_size = \ 963 """ 964 \\documentclass[@10pt@,fleqn]{report} 965 """ 966 else: 967 paper_size = \ 968 """ 969 \\documentclass[@10pt@,fleqn]{report} 970 \\usepackage[vcentering]{geometry} 971 """ 972 if paper == 'landscape': 973 paper = [11, 8.5] 974 paper_size += '\\geometry{papersize={' + str(paper[0]) + \ 975 'in,' + str(paper[1]) + 'in},total={' + str(paper[0] - 1) + \ 976 'in,' + str(paper[1] - 1) + 'in}}\n' 977 978 paper_size = paper_size.replace('@10pt@', pt) 979 latex_str = paper_size + GaLatexPrinter.preamble + latex_str + GaLatexPrinter.postscript 980 981 return latex_str 982 983 984 def xpdf(filename=None, paper=(14, 11), crop=False, png=False, prog=False, debug=False, pt='10pt', pdfprog='pdflatex'): 985 986 """ 987 Post processes LaTeX output (see comments below), adds preamble and 988 postscript, generates tex file, inputs file to latex, displays resulting 989 pdf file. 990 991 Arg Value Result 992 pdfprog 'pdflatex' Use pdfprog to generate pdf output, only generate tex if pdfprog is None 993 crop True Use "pdfcrop" to crop output file (pdfcrop must be installed, linux only) 994 png True Use "convert" to produce png output (imagemagick must be installed, linux only) 995 996 We assume that if xpdf() is called then Format() has been called at the beginning of the program. 997 """ 998 999 sys_cmd = SYS_CMD[sys.platform] 1000 1001 latex_str = tex(paper=paper, debug=debug, prog=prog, pt=pt) 1002 1003 if filename is None: 1004 pyfilename = sys.argv[0] 1005 rootfilename = pyfilename.replace('.py', '') 1006 filename = rootfilename + '.tex' 1007 1008 if debug: 1009 print('latex file =', filename) 1010 1011 latex_file = open(filename, 'w') 1012 latex_file.write(latex_str) 1013 latex_file.close() 1014 1015 latex_str = None 1016 1017 if pdfprog is None: 1018 return 1019 1020 pdflatex = shutil.which(pdfprog) 1021 1022 if debug: 1023 print('pdflatex path =', pdflatex) 1024 1025 if pdfprog is not None: 1026 if debug: # Display latex excution output for debugging purposes 1027 os.system(pdfprog + ' ' + filename[:-4]) 1028 else: # Works for Linux don't know about Windows 1029 os.system(pdfprog + ' ' + filename[:-4] + sys_cmd['null']) 1030 1031 print_cmd = sys_cmd['evince'] + ' ' + filename[:-4] + '.pdf ' + sys_cmd['&'] 1032 print(print_cmd) 1033 1034 os.system(print_cmd) 1035 eval(input('!!!!Return to continue!!!!\n')) 1036 1037 if debug: 1038 os.system(sys_cmd['rm'] + ' ' + filename[:-4] + '.aux ' + filename[:-4] + '.log') 1039 else: 1040 os.system(sys_cmd['rm'] + ' ' + filename[:-4] + '.aux ' + filename[:-4] + '.log ' + filename[:-4] + '.tex') 1041 if crop: 1042 os.system('pdfcrop ' + filename[:-4] + '.pdf') 1043 os.remove(filename[:-4] + '.pdf') 1044 os.rename(filename[:-4] + '-crop.pdf', filename[:-4] + '.pdf') 1045 if png: 1046 os.system('Pdf2Png ' + filename[:-4]) 1047 return 1048 1049 1050 def xdvi(filename=None, debug=False, paper=(14, 11)): 1051 xpdf(filename=filename, paper=paper, crop=False, png=False, prog=False, debug=debug, pt='10pt') 1052 return 1053 1054 1055 def LatexFormat(Fmode=True, Dmode=True, ipy=False): 1056 GaLatexPrinter.set_global_settings( 1057 omit_partial_derivative_fraction=Dmode, 1058 omit_function_args=Fmode 1059 ) 1060 GaLatexPrinter.ipy = ipy 1061 GaLatexPrinter.redirect() 1062 return 1063 1064 1065 off_mode = False 1066 1067 1068 def Get_Program(off=False): 1069 global off_mode 1070 off_mode = off 1071 # galgebra 0.5.0 1072 warnings.warn( 1073 "galgebra.printer.Get_Program is deprecated, and exists solely to " 1074 "toggle whether galgebra.printer.Print_Function does anything. If you " 1075 "want to turn off program printing, then just don't call Print_Function!", 1076 DeprecationWarning, stacklevel=2) 1077 1078 1079 def Print_Function(): 1080 """ Print out the source of the current function """ 1081 if off_mode: 1082 return 1083 1084 tmp_str = inspect.getsource(inspect.currentframe().f_back) 1085 if GaLatexPrinter.latex_flg: 1086 #print '#Code for '+fct_name 1087 print(r'##\begin{lstlisting}[language=Python,showspaces=false,' 1088 r'showstringspaces=false,backgroundcolor=\color{gray},frame=single]') 1089 print(tmp_str) 1090 print('##\\end{lstlisting}') 1091 print('#Code Output:') 1092 else: 1093 print('\n' + 80 * '*') 1094 #print '\nCode for '+fct_name 1095 print(tmp_str) 1096 print('Code output:\n') 1097 return 1098 1099 1100 _eval_global_dict = {} 1101 _eval_parse_order = [] 1102 1103 1104 def def_prec(gd: dict, op_ord: str = '<>|,^,*') -> None: 1105 """ 1106 This is used with the ``GAeval()`` function to evaluate a string representing a multivector expression with a revised operator precedence. 1107 1108 Parameters 1109 ---------- 1110 gd : 1111 The ``globals()`` dictionary to lookup variable names in. 1112 op_ord : 1113 The order of operator precedence from high to low with groups of equal precedence separated by commas. 1114 The default precedence, ``'<>|,^,*'``, is that used by Hestenes (:cite:`Hestenes`, p7, :cite:`Doran`, p38). 1115 This means that the ``<``, ``>``, and ``|`` operations have equal 1116 precedence, followed by ``^``, and lastly ``*``. 1117 """ 1118 global _eval_global_dict, _eval_parse_order 1119 op_ord_list = op_ord.split(',') 1120 _parser.validate_op_order(op_ord_list) 1121 _eval_global_dict = gd 1122 _eval_parse_order = op_ord_list 1123 1124 1125 def GAeval(s: str, pstr: bool = False): 1126 """ 1127 Evaluate a multivector expression string ``s``. 1128 1129 The operator precedence and variable values within the string are 1130 controlled by :func:`def_prec`. The documentation for that function 1131 describes the default precedence. 1132 1133 The implementation works by adding parenthesis to the input string ``s`` 1134 according to the requested precedence, and then calling :func:`eval` on the 1135 result. 1136 1137 For example consider where ``X``, ``Y``, ``Z``, and ``W`` are multivectors:: 1138 1139 def_prec(globals()) 1140 V = GAeval('X|Y^Z*W') 1141 1142 The *sympy* variable ``V`` would evaluate to ``((X|Y)^Z)*W``. 1143 1144 Parameters 1145 ---------- 1146 s : 1147 The string to evaluate. 1148 pstr : 1149 If ``True``, the values of ``s`` and ``s`` with parenthesis added to 1150 enforce operator precedence are printed. 1151 """ 1152 1153 seval = _parser.parse_line(s, _eval_parse_order) 1154 if pstr: 1155 print(s) 1156 print(seval) 1157 return eval(seval, _eval_global_dict) 1158 1159 1160 def Fmt(obj, fmt=0): 1161 if isinstance(obj, (list, tuple, dict)): 1162 n = len(obj) 1163 if isinstance(obj, list): 1164 ldelim = '[' 1165 rdelim = ']' 1166 elif isinstance(obj, dict): 1167 ldelim = r'\{' 1168 rdelim = r'\}' 1169 else: 1170 ldelim = '(' 1171 rdelim = ')' 1172 if fmt == 1: 1173 latex_str = r' \left ' + ldelim + r' \begin{array}{' + n*'c' + '} ' 1174 for cell in obj: 1175 if isinstance(obj, dict): 1176 #cell.title = None 1177 latex_cell = latex(cell) + ' : ' + latex(obj[cell]) 1178 else: 1179 #title = cell.title 1180 #cell.title = None 1181 latex_cell = latex(cell) 1182 latex_cell = latex_cell.replace('\n', ' ') 1183 latex_str += latex_cell + ', & ' 1184 #cell.title = title 1185 latex_str = latex_str[:-4] 1186 latex_str += r'\\ \end{array} \right ' + rdelim + ' \n' 1187 else: 1188 latex_str = '' 1189 i = 1 1190 for cell in obj: 1191 #title = cell.title 1192 #cell.title = None 1193 latex_cell = latex(cell) 1194 latex_cell = latex_cell.replace('\n', ' ') 1195 #cell.title = title 1196 if i == 1: 1197 latex_str += r'\begin{array}{c} \left ' + ldelim + r' ' + latex_cell + r', \right. \\ ' 1198 elif i == n: 1199 latex_str += r' \left. ' + latex_cell + r'\right ' + rdelim + r' \\ \end{array}' 1200 else: 1201 latex_str += r' ' + latex_cell + r', \\' 1202 i += 1 1203 if isinteractive(): # For Ipython notebook 1204 latex_str = r'\begin{equation*} ' + latex_str + r'\end{equation*}' 1205 return latex_str 1206 else: 1207 return latex_str 1208 1209 elif isinstance(obj, int): 1210 LatexPrinter.set_global_settings(galgebra_mv_fmt=obj) 1211 return 1212 else: 1213 raise TypeError(str(type(obj)) + ' not allowed arg type in Fmt') 1214 1215 1216 class _WithSettings(GaPrintable): 1217 """ Helper class to attach print settings to an object """ 1218 def __init__(self, obj, settings: dict = {}): 1219 self._obj = obj 1220 self._settings = settings 1221 1222 def __do_print(self, printer): 1223 # make a copy of the printer with the specified setting applied 1224 new_printer = copy.copy(printer) 1225 new_printer._settings = copy.copy(new_printer._settings) 1226 new_printer._settings.update(self._settings) 1227 return new_printer._print(self._obj) 1228 1229 _latex = _pretty = _sympystr = __do_print 1230 1231 1232 class _FmtResult(GaPrintable): 1233 """ Object returned from .Fmt methods, which can be printed as latex """ 1234 def __new__(cls, obj, label: str) -> GaPrintable: 1235 if label is None: 1236 return obj 1237 self = super().__new__(cls) 1238 self._obj = obj 1239 self._label = label 1240 return self 1241 1242 def _latex(self, printer): 1243 return self._label + ' = ' + printer._print(self._obj) 1244 1245 def _sympystr(self, printer): 1246 return self._label + ' = ' + printer._print(self._obj)