Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added naming based on input types #660

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

GeorgeR227
Copy link

This closes #659.

@ChrisRackauckas
Copy link
Member

Needs docs and tests

@jpfairbanks
Copy link
Contributor

I guess a good test would be to define a function that we want to have a different name based on Real vs Complex argument. Like

sqrt_R+ vs sqrt_C

Is the same function object, but we want to have different printing to make it easier to understand how the types are flowing through the formulas.

Still need tests for decorated case.
@ChrisRackauckas
Copy link
Member

Should this instead be related to Julia's compact printing modes?

I still don't understand what you mean by "based on the input types" here: what are the types and how are they involved?

@jpfairbanks
Copy link
Contributor

Compact printing modes is a new feature to me. What are you thinking?

In the exterior calculus context, the operations are graded by dimension, so for every operation, there is a subscript that corresponds to that dimension. We have implemented a bunch of functions and types to represent the types and operations of the exterior calculus that have the dimension as part of the variable type. Our variables have types like PrimalForm{i,n} where i and n are integers representing the dimension of the form and the dimension of the manifold over which it is defined. But then when you have an operations like d the exterior derivative, it is actually a family of operations indexed by i and n. This is commonly written as d_0 or d_1 depending on the dimension of the argument. Thus in order to derive the name of the operation, we need to know the symtype of the arguments.

If we define a fallback, where you ignore the arguments, then the existing behavior is the same for everyone except for those that opt in to names that depend on argument types.

@GeorgeR227
Copy link
Author

I've just added a test that might explain more what I mean by the "naming based on input types ".

In this case, I've defined a Julia function that acts like SymbolicUtils.FnType. The reason for the function is so that I can dispatch on Julia's Base.nameof and add extra information to the printed output based on the symtypes of the arguments passed into the function. So even though the function itself is simply called sq, I can augment that name to provide more information. This is useful particularily for code-gen, if we let the user only input a generic name, like sq, but under the hood we have different implementations based on the input types.

sq(x) = return SymbolicUtils.Term{Number}(sq, [x])

function Base.nameof(::typeof(sq), arg)
    if arg <: Real
        return :sqrt_R
    elseif arg <: Complex
        return :sqrt_C
    else
        return :sqrt
    end
end

x,y,z = @syms x::Real y::Complex z

@test get_print(sq(x)) == "sqrt_R(x)"
@test get_print(sq(y)) == "sqrt_C(y)"
@test get_print(sq(z)) == "sqrt(z)"

@ChrisRackauckas
Copy link
Member

I still don't understand. Not trying to be adversarial here, but this is really confusing to me.

This is useful particularily for code-gen, if we let the user only input a generic name, like sq, but under the hood we have different implementations based on the input types.

Isn't that just multiple dispatch? If someone has a function that acts differently dependent on the type, you'd just make different dispatches and that would happen automatically. The sqrt example is exactly a case where one doesn't need to specify the types because it has multiple dispatch? Is there an example where multiple dispatch does not cover this?

Compact printing modes is a new feature to me. What are you thinking?

Julia's Base printing has a compact mode, which is how arrays do that .... I think it would make sense to do something like, if compact mode then don't print the module and instead just show the function. That seems to be a more general way to solve the problem that printing sometimes is verbose with module showing.

@jpfairbanks
Copy link
Contributor

I guess that means there are two features here

  1. If printing is in compact mode, we should suppress module names. How do we do that?

  2. If you want to use a different symbol for the name based on the argument type of a call expression, we would also like to do that.

We definitely need (1). We can put (2) in our own codebase for a different purpose since it isn't critical if we have (1)

How do you add a check for compact mode? And are the repl and jupyter notebook cells in compact mode?

@ChrisRackauckas
Copy link
Member

If printing is in compact mode, we should suppress module names. How do we do that?

I always forget. https://discourse.julialang.org/t/show-and-showcompact-on-custom-types/8493 seems like it has it.

If you want to use a different symbol for the name based on the argument type of a call expression, we would also like to do that.

I guess what I don't understand is if this is just a display thing? A symbolic is defined by matching not just its name but also its type and metadata, so they can have the same name but still be different in a sense. This is already checked in equality matching. So is this just about allowing a system to allow for uniqueness in display? Is it odd if the displayed name does not match the given name of the symbolic?

Pinging @bowenszhu @0x0f0f0f for some discussion.

@jpfairbanks
Copy link
Contributor

A symbolic is defined by matching not just its name but also its type and metadata, so they can have the same name but still be different in a sense.

It would be helpful if users could opt in to having the printing of the symbolic term also depend on the type and metadata. My understanding of SymbolicUtils types is that the type parameter of a symbolic term is the type that the term will evaluate to.

I agree that if the printed name doesn't match a constructor you can use to construct it, people will get confused. We are defining aliases so that Δ₀, Δ₁ both create terms with Δ as the head. But then when we want to print them, we want to have the subscript determined based on the argument type. So in our usage pattern, you can write either x::Form{0}; y == Δ(x) or x::Form{0}; y == Δ₀(x) and these should mean the same thing and both should print as y == Δ₀(x).

@ChrisRackauckas
Copy link
Member

Maybe related is JuliaSymbolics/Symbolics.jl#1305 with customization options, @hersle

@hersle
Copy link

hersle commented Nov 5, 2024

It would be helpful if users could opt in to having the printing of the symbolic term also depend on the type and metadata.

I also think being able to hook into the printing/latexifying of expressions sounds like a useful feature.

If printing is in compact mode, we should suppress module names. How do we do that?

This sounds somewhat "dishonest" to me, since it really changes the name of the printed variable. For consistency, I would prefer scoping variables instead (e.g. GlobalScope()), if possible.

But this application is far away from my domain, so I am sure there are good reasons for why you want to do this. Ideally, I think the customization of expression printing/latexifying should be done through a function in which the user can freely process the normal/Latex string, instead of only choosing between a set of predetermined "modes". Then this function could be used to e.g. suppress module names, customize the font style of module/variable names, turn "a_1" into "a₁", and anything else one can think of.

@ChrisRackauckas
Copy link
Member

@AayushSabharwal thoughts?

@AayushSabharwal
Copy link
Contributor

AayushSabharwal commented Nov 8, 2024

EDIT at the bottom 😅

There's definitely a case to be made either way. But we already lie to the user when printing time-dependent variables (you can't actually type x(t) most of the time, even if that is what it displays as), so I don't see the harm in allowing users to customize display even if it results in output that can't just be copy-pasted into a REPL.

But then when you have an operations like d the exterior derivative, it is actually a family of operations indexed by i and n. This is commonly written as d_0 or d_1 depending on the dimension of the argument. Thus in order to derive the name of the operation, we need to know the symtype of the arguments.

I agree that if the printed name doesn't match a constructor you can use to construct it, people will get confused. We are defining aliases so that Δ₀, Δ₁ both create terms with Δ as the head.

The "name" of the operation defines what it does, not how it does it. The name of the function is effectively julia-compatible shorthand for the name of the operation. If "exterior derivative" isn't enough to describe what is happening, are they not either different operations, or operations which accept an additional argument 0/1, etc?

If you're going to these lengths to make sure that A) a user can type Δ(x) and see Δ₀(x) and B) also be able to type Δ₀(x) to get the same result, they really look like the same operation with the subscript being more for user clarity. I'm not opposed to this, but we shouldn't get the motives mixed up.

I'm in support of better hooks into our display API, but this implementation feels like a quick and dirty approach. It would be good to list the sort of hooks we want and have at least a quick discussion around how to expose them, otherwise we might have to rework this functionality later to accommodate other requests and be stuck having to add edge cases to handle old infrastructure. Two things that occur to me right off the bat:

  1. We should make sure display customization doesn't affect codegen.
  2. I really don't like Base.nameof(f, arg, args...). It's type piracy, even if Base doesn't actually define a multi-argument nameof method. SymbolicUtils doesn't own the method, or the types (all of which are Any). This should instead be a function defined in SymbolicUtils which falls back to nameof.

EDIT: I just realized this is SymbolicUtils not Symbolics. In this case, up until now I believe our printed expressions do directly reflect runnable code. The above comment is then hinged upon whether we decide this should continue to be the case or not and where we draw the line. Symbolics expressions are not necessarily runnable as they are printed, because it adds [1:3] for array expressions, @variables x(t) is printed as x(t) but input as x and so on. However, all of this is a difference in printing variables created through Symbolics, not through SU, and none of it modifies how expressions are printed.

@AayushSabharwal
Copy link
Contributor

AayushSabharwal commented Nov 8, 2024

I'm leaving that comment up despite writing it under a bit of a misconception 😅. Realizing that this is SymbolicUtils I'll give this some more thought before writing much more, but:

The motive isn't fully clear here. Are the subscripts and change in printing for user clarity or because they are semantically different operations? Why is the solution here to also define functions with subscripts, since manually defining all of this only goes so far and the user can always have a big enough i/n that SU would print a function that you haven't defined. The function in the BasicSymbolic should always be callable with values whose type is the symtype of the arguments. If this is not possible, then that's the wrong function to put in there.

@AayushSabharwal
Copy link
Contributor

@jpfairbanks if you have subscript functions defined, why not create the symbolic variable with the subscript functions instead of with Δ?

@jpfairbanks
Copy link
Contributor

We do that too. They convert to the unsubscripted versions as part of our implementation of dispatch.

We have a layer of defining these operators that supports dispatching on dimension so that you can use the generic versions without subscripts or the specific versions with subscripts. You get the same terms either way. The subscripts are there just for readability in our implementation

@AayushSabharwal
Copy link
Contributor

I'm not sure what you mean by your version of dispatch?

Regardless, though, I think it's worth exposing some well-defined hooks into our printing. The major win I see here is better readability when the function in the BasicSymbolic is a callable struct.

A few notes on how we can go about this:

  1. The custom printing should be defined for show(::IO, ::MIME"text/plain", ::BasicSymbolic) and show(::IO, ::BasicSymbolic) should be parseable Julia code (as defined in the docstring of show)
  2. We can have a special hook (at the very least) for functions, and for Syms. I can see this being useful for e.g. a Geometric Algebra CAS which might annotate variables with the grade of blades e.t.c
  3. Base.nameof(f, arg, args...) is type piracy, as I mentioned earlier. In general, SU should export its own display hooks which fall back to ones in Base

@GeorgeR227
Copy link
Author

The first attempt to fix the printing issue on our side was to just overload the show_call method but doing so felt very clunky. I can get behind introducing defined hooks that are meant to be overloaded. I imagine the type piracy here can also be fixed by directly converting this Base.nameof use into a hook.

@jpfairbanks
Copy link
Contributor

jpfairbanks commented Nov 8, 2024

The major win I see here is better readability when the function in the BasicSymbolic is a callable struct.

whoa whoa whoa, we didn't even think to try and make the head of a BasicSymbolic a callable struct. Are there examples of that in the wild somewhere that we can learn from?

We could put our grade and space information into the fields of a callable struct and do

X=Space(:X, 2)
p=Form(X,0)
u = Form(X, 1)
d(0)(p) +  Δ(1)(u)

Then we could use d(p::Form) = d(grade(p))(p) to allow users to write the generic formulas and have the grades be inferred by regular julia dispatch.

We can have a special hook (at the very least) for functions, and for Syms. I can see this being useful for e.g. a Geometric Algebra CAS which might annotate variables with the grade of blades e.t.c

This is exactly what we are doing. We are working with forms on a de Rahm complex and are trying to work with the grade of the forms, which corresponds to the grade of a blade.

@AayushSabharwal
Copy link
Contributor

It's possible using Symbolics.jl to do something like

struct Foo
  # ...
end
(foo::Foo)(arg1, arg2) = (...)

@register_symbolic (foo::Foo)(arg1, arg2)

Now if you do

@variables x y
foo = Foo(...)
foo(x, y)

you get a symbolic variable whose head is the callable struct.

Then we could use d(p::Form) = d(grade(p))(p) to allow users to write the generic formulas and have the grades be inferred by regular julia dispatch.

I thought the \Delta function already did this? If not, then yeah pretty useful

This is exactly what we are doing. We are working with forms on a de Rahm complex and are trying to work with the grade of the forms, which corresponds to the grade of a blade.

Oh, neat. I have some experience with GA and trying to use it for physical simulations, but never used exterior calculus.

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.

Argument type-aware printing for Symbolic terms
6 participants