2

Note: This is a duplicate of this question in Math Stack Exchange. I had to first pose the question in Math StackExchange because StackOverflow doesn't have MathJax. However, almost all SymPy questions are on StackOverflow. Please refer to the Math Stack Exchange version for the typesetting of the desired output. An editor on Math StackExchange suggested I cross-post it here.

In Jupyter notebook if we execute this code:

import sympy as sp
sp.init_printing()
x,y=sp.symbols('x,y')
x**2+sp.sin(y)

We will get a nice output, with no further coding, due to SymPy's pretty printing process, that looks like

Pretty formula

Now suppose we do:

class MetricSpace:
    def __init__(self, M, d):
        self.M = M
        self.d = d
        
    def __repr__(self):
        return f"$({self.M}, {self.d})$ {self.__class__.__name__}"

Re,d=sp.symbols(r'\Re,d')

MetricSpace(Re,d)

Then the output we get is

ugly formula

If we do instead

from IPython.core.display import Markdown
Markdown(repr(MetricSpace(Re,d)))

then I get the desired output, which looks like

Desired formatting

How do we code the above so that SymPy's pretty printer provides the desired output in Jupyter notebook without having to wrap it in Markdown(repr(...))?

Eric
  • 95,302
  • 53
  • 242
  • 374
Lars Ericson
  • 1,952
  • 4
  • 32
  • 45

3 Answers3

1

Here is a snippet that functions correctly. It might not do quite all the things you want yet, but hopefully it will start you off.

import sympy as sp
sp.init_printing()

class MetricSpace(sp.Expr):
    def __init__(self, M, d):
        self.M = M
        self.d = d

    def _latex(self, printer=None):
        return f"({printer.doprint(self.M)}, {printer.doprint(self.d)})\\ \\text{{{self.__class__.__name__}}}"

Re, d = sp.symbols(r'\Re,d')

MetricSpace(Re, d)

See here for more information on the Sympy printing system and how to hook into it. The key things to note is that you should subclass Expr if you want your object to come into the domain of Sympy's pretty printing, and that the latex printer calls the _latex method to get its LaTeX. It doesn't need to be wrapped in dollar signs, and you should use printer.doprint to get the LaTeX of nested expressions.

Here is a screenshot of what this produces on my notebook:

screenshot

PS: I'm intrigued by a metric space with underlying set denoted by "\Re". If you mean the reals, might I suggest using sp.Reals?

Izaak van Dongen
  • 2,450
  • 13
  • 23
  • 1
    Izaak, the other side of the coin is how to do dynamic checking of types in Python. "typen" package solves that: https://stackoverflow.com/questions/63343360/dynamic-checking-of-type-hints-in-python-3-5/63344681 – Lars Ericson Aug 10 '20 at 17:54
  • 1
    For a metric space, I want to say that M is a nonempty set. So I need a way of annotating sp.Reals to say that it is a nonempty set. To do that I have to at least wrap sp.Reals with a "Reals" subclass which is also a subclass of a made-up class I call NonemptySet. – Lars Ericson Aug 10 '20 at 17:54
  • 1
    So something like this: `class MetricSpace: @strict_type_hints def __init__(self, M: NonemptySet, d: Metric): self.M = M self.d = d def _latex(self, printer=None): ...`. The third side of the coin is that where applicable I want to start using the Category Theory features in sympy. – Lars Ericson Aug 10 '20 at 17:54
  • Subclassing `Basic` would make more sense, `Expr` gives you operator overloads you don't want. – Eric Aug 10 '20 at 19:48
  • In sympy 1.7, you can subclass `sympy.printing.defaults.Printable` – Eric Aug 10 '20 at 19:52
  • _"and you should use `printer.doprint` to get the LaTeX of nested expressions."_ - the docs [specifically call out](https://docs.sympy.org/dev/modules/printing.html#common-mistakes) not to do this. – Eric Aug 10 '20 at 20:42
  • Eric thanks for this extra info. I am on SymPy 1.6.1 now but will remember this advice when I get to SymPy 1.7. – Lars Ericson Aug 11 '20 at 00:26
  • @Eric can you post an additional answer which revises the first answer for SymPy 1.7 new features? – Lars Ericson Aug 11 '20 at 11:52
1

Note: I have already accepted the answer from Izaak van Dongen above. This answer is to provide context for the question for those that may be interested.

I was reading a book on SDEs and I asked a question on the measure theory presentation of probability space. There are a large number of structural definitions involved. I wanted to use SymPy to organize them and present the type signatures in something mimicking Axiom style.

To do this pleasantly in Python 3 and SymPy I needed a few things:

  • A way of dynamically enforcing function signatures.
  • A way of pretty-printing complex algebraic type signatures (this question).

I started implementing the definitions. To check that they were organized correctly, I asked

With that in hand, and the above solution for the pretty-printing, the following few definitions give the style of my solution (without quoting the whole thing which is about 85 definitions):

import sympy as sp  # I am at version 1.6.1
from typen import strict_type_hints, enforce_type_hints
from traits.api import Array, Either, Enum, Instance, Int, Str, Tuple

class Concept(sp.Expr):
    def __init__(self, name, value):
        self.name = name
        self.value = value
        
    def _latex(self, printer=None):
        return f"{self.name}:\\ \\text{{{self.__class__.__name__}}}"

class NonemptySet(Concept):
    def __init__(self, name, value):
        if value==sp.S.EmptySet:
            raise ValueError("Set must not be empty")
        super().__init__(name, value)
        
    def _latex(self, printer=None):
        return self.name

class Reals(NonemptySet):
    
    @strict_type_hints
    def __init__(self):
        self.name = sp.symbols('\\Re')
        super().__init__(self.name,sp.Reals)
        
    def _latex(self, printer=None):
        return self.name

class SampleSpace (NonemptySet):
    pass

class Algebra(NonemptySet):
    
    @strict_type_hints
    def __init__(self, 
                 name: sp.Symbol, 
                 Ω: SampleSpace, 
                 A: Either(NonemptySet, sp.Symbol)):
        self.Ω=Ω
        super().__init__(name, A)

    def _latex(self, printer=None):
        math=str(self.name).replace('$','')
        math2 = self.Ω._latex(printer)
        return f"{math}:\\ \\text{{{self.__class__.__name__} on }} ({math2})"

class Algebra(Algebra):
    
    @strict_type_hints
    def __init__(self, name: sp.Symbol, Ω: SampleSpace, A: Algebra):
        self.Ω=Ω
        super().__init__(name, Ω, A)

class EventSpace(Algebra):
    
    @strict_type_hints
    def __init__(self, name: sp.Symbol, Ω: SampleSpace, A: Algebra):
        super().__init__(name, Ω, A)

class AdditiveFunction(Concept):
    
    @strict_type_hints
    def __init__(self, 
                 name: sp.core.function.UndefinedFunction, 
                 Ω: SampleSpace, 
                 A: Algebra, 
                 f: sp.core.function.UndefinedFunction):
        self.Ω = Ω
        self.A = A
        super().__init__(name, f)

    def _latex(self, printer=None):
        math2 = self.A._latex(printer)
        return f"{self.name}: {self.A.name} \\to \\Re \\ \\text{{{self.__class__.__name__} on }} {math2}"

and so on. Any comments or suggestions on a more "SymPy-thonic" way of improving the above sketch would be greatly appreciated.

Lars Ericson
  • 1,952
  • 4
  • 32
  • 45
1

If all you care about is integration with Jupyter notebook, then you want the IPython _repr_latex_ hook:

class MetricSpace:
   def _repr_latex_(self):
       # this is text-mode latex, so needs $ to enter math mode
       return f"$({self.M}, {self.d})$ {self.__class__.__name__}"

This will make MetricSpace(...) show as latex in a jupter notebook, and is completely independent of sympy.


Separately, sympy has a latex function. If you want to support that, you need to implement _latex:

class MetricSpace:
   def _latex(self, printer):
       # this is math-mode latex, so needs \text to enter text mode
       # if the class members support this hook, you can use `printer._print` to recurse
       return f"{printer._print(self.M)}, {printer._print(self.d)}$ \text{{{self.__class__.__name__}}}"

This will make sympy.latex(MetricSpace(...)) work.


Finally, there's the sympy integration that connects these two modes together, after init_printing(use_latex='mathjax') is called. This has changed between Sympy 1.6 and 1.7.

  • In Sympy 1.6, an ipython latex formatter is provided for
    • subclasses of sympy.Basic (and some others)
    • lists, sets, etc of those objects
  • In Sympy 1.7, an ipython latex formatter is provided for
    • subclasses of sympy.printing.defaults.Printable
    • lists, sets, etc of any object that implements _latex or subclasses Printable

Subclassing Expr is a bad idea, as that adds tens of possibly meaningless methods and operator overloads to your class. Subclassing Basic is less bad, but suffers from a lesser version of the same problem if your class is not intended to be used within sympy

Eric
  • 95,302
  • 53
  • 242
  • 374