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

Let user hook into variable Latexifying #1350

Closed
wants to merge 1 commit into from

Conversation

hersle
Copy link
Contributor

@hersle hersle commented Nov 6, 2024

There has been some changes/discussion about how variables are Latexified in JuliaSymbolics/SymbolicUtils.jl#659, JuliaSymbolics/SymbolicUtils.jl#660, #1314 and #1305.

This is a suggestion to provide a hook Symbolics.latexify_variables that users (e.g. ModelingToolkit, Catalyst, other packages or humans) can redefine to customize this. In a fresh Julia session, it could work like this:

# 1) Default with Symbolics only (pure output)
using Symbolics, Latexify
@variables t car₊wheel₊ω(t) # angular velocity
latexify(car₊wheel₊ω) # -> car.wheel.\omega\left( t \right)

# 2) Inside ModelingToolkit?
function Symbolics.latexify_variable(name)
    return "\\mathtt{$name}" # treat subsystem and variable names like code (word-like names are very common in hierarchical modeling)  
end
latexify(car₊wheel₊ω) # -> \mathtt{car.wheel.\omega}\left( \mathtt{t} \right)

# 3) Inside another user's package?
function Symbolics.latexify_variable(name)
    names = split(name, ".") # get subsystem and variable names
    names[1:end-1] = map(name -> "\\mathrm{$name}", names[1:end-1]) # treat subsystem names like words
    names[end] = length(names[end]) > 1 ? "\\mathrm{$(names[end])}" : names[end] # treat one-letter variables like symbols and multi-letter names like words
    name = join(names, ".") # reassemble
    return name
end
latexify(car₊wheel₊ω) # -> \mathrm{car}.\mathrm{wheel}.\omega\left( t \right)

If ModelingToolkit gets something like a LatexDisplayOptions object in the future, my idea is that it could use this hook to do its thing. But for many packages, maybe it is sufficient to just override this function, based on what types of equations and variables it has.

Any thoughts on this approach and potential issues? cc @ChrisRackauckas @isaacsas

@codecov-commenter
Copy link

codecov-commenter commented Nov 6, 2024

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

Attention: Patch coverage is 0% with 6 lines in your changes missing coverage. Please review.

Project coverage is 6.47%. Comparing base (5af597a) to head (cfd3689).
Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/latexify_recipes.jl 0.00% 6 Missing ⚠️

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff            @@
##           master   #1350      +/-   ##
=========================================
+ Coverage    3.98%   6.47%   +2.49%     
=========================================
  Files          50      50              
  Lines        4771    4772       +1     
=========================================
+ Hits          190     309     +119     
+ Misses       4581    4463     -118     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@isaacsas
Copy link
Contributor

isaacsas commented Nov 6, 2024

I had been thinking we could just add a kwarg to latexify to pass a configuration struct, but it appears there would be no automatic way to get a custom instance of the struct passed into show. i.e. to make show here

Base.show(io::IO, ::MIME"text/latex", x::RCNum) = print(io, "\$\$ " * latexify(x) * " \$\$")
Base.show(io::IO, ::MIME"text/latex", x::Symbolic) = print(io, "\$\$ " * latexify(x) * " \$\$")
Base.show(io::IO, ::MIME"text/latex", x::Equation) = print(io, "\$\$ " * latexify(x) * " \$\$")
Base.show(io::IO, ::MIME"text/latex", x::Vector{Equation}) = print(io, "\$\$ " * latexify(x) * " \$\$")
Base.show(io::IO, ::MIME"text/latex", x::AbstractArray{<:RCNum}) = print(io, "\$\$ " * latexify(x) * " \$\$")

work, one would need to effectively change a global variable.

But I think that even then it makes sense to have a Symbolics.LatexifyConfiguration struct and a global Symbolics.latexify_config = LatexifyConfiguration() variable. (Or have Symbolics.latexify_config be a Dict() that stores some configuration settings that can be modified.)

That struct/Dict could, for example, store the latexify_variable function, and via changing the value of latexify_config to a different instance of the struct, a user could change the default behavior. This would also allow for adding future config options by just adding more fields with default values to LatexifyConfiguration.

@ChrisRackauckas
Copy link
Member

This is a suggestion to provide a hook Symbolics.latexify_variables that users (e.g. ModelingToolkit, Catalyst, other packages or humans) can redefine to customize this. In a fresh Julia session, it could work like this:

I don't think that would work because if you all pirate the same method, then you just get the behavior of the last package imported. Piracy is also not recommended.

@hersle
Copy link
Contributor Author

hersle commented Nov 7, 2024

I don't think that would work because if you all pirate the same method, then you just get the behavior of the last package imported.

That makes sense. Thanks for mentioning it quickly.

@isaacsas
Copy link
Contributor

isaacsas commented Nov 7, 2024

What if we add a symbolic metadata that when set stores the Symbolics.LatexConfiguration object to use for a given symbolic variable? (If not set, the default is used.)

@isaacsas
Copy link
Contributor

isaacsas commented Nov 7, 2024

That avoids needing a global variable and potentially allows per-variable control on formatting. If we make the struct mutable then it should just be storing a pointer right, so not be too heavy?

@hersle
Copy link
Contributor Author

hersle commented Nov 7, 2024

Something like @variables money [latex = raw"\mathrm{money}"]? That resembles SageMath's latex_name = ..., for example.

@isaacsas
Copy link
Contributor

isaacsas commented Nov 7, 2024

# in Symbolics.jl we have
struct LatexConfiguration{F}
   latexify_variables::F
end

# in your code
lc = Symbolics.LatexConfiguration(pass_your_custom_function)
@variables money [latex_config = lc]

@hersle
Copy link
Contributor Author

hersle commented Nov 7, 2024

I could see that working. But if we tie Latex formatting to each variable, is there any gain in using a function? Would not an (optional) per-variable string be simpler and accomplish the same? Or should performance be significantly better by only storing a function? If formatting is not tied to each variable, I fully see the need for a function, though.

I'm torn on this 😅 Catch-all formatting functions are great at making systematic adjustments to all variables. And per-variable formatting is great for tuning specific variables only. Maybe both options could even be used in conjunction?

I almost have something working that uses @latexrecipe with a keyword argument, similar to what you suggested. That could be combined with system-specific latexify() dispatches. I will see if I can finalize it, just to explore some options.

@isaacsas
Copy link
Contributor

isaacsas commented Nov 7, 2024

The problem is that ModelingToolkit can change variable names under the hood when it namespaces. Then the attached metadata would no longer be the right latex string i.e. a getting turned within MTK into sys1.sys2.a. Having a configuration function and/or struct means we can have options for handling namespace separators too that get applied to derived variables (assuming MTK propagates the metadata to derived variables).

Dispatches on the recipes is what I thought originally too, but I don't see how to get them to work with show where I linked above. That means in certain contexts (notebook environments?) the default formatting would start getting applied again.

@hersle
Copy link
Contributor Author

hersle commented Nov 7, 2024

but I don't see how to get them to work with show where I linked above

Could you not define a @latexrecipe for the system type, which calls latexify() with the desired keyword argument? For example here in MTK. Then that recipe would be called by latexify() in show().

@hersle
Copy link
Contributor Author

hersle commented Nov 7, 2024

The problem is that ModelingToolkit can change variable names under the hood when it namespaces.

Indeed. I guess MTK could respect the Latex strings by concatenating them when namespacing variables. But a function would also handle it, as you say.

@hersle hersle marked this pull request as draft November 7, 2024 14:02
@isaacsas
Copy link
Contributor

isaacsas commented Nov 7, 2024

Could you not define a @latexrecipe for the system type, which calls latexify() with the desired keyword argument? For example here in MTK. Then that recipe would be called by latexify() in show().

But that will only give correct displays when at some top level one is calling latexify(sys). It won't be hooked into anywhere that latexify(symbolic_variable) or latexify(equation) are called directly (like a Pluto or Jupyter notebook when you are manipulating an individual symbolic variable/expression and not trying to display a full system). For example, how do you pass kwargs to latexify in cases where show gets called by something:

Base.show(io::IO, ::MIME"text/latex", x::RCNum) = print(io, "\$\$ " * latexify(x) * " \$\$")
Base.show(io::IO, ::MIME"text/latex", x::Symbolic) = print(io, "\$\$ " * latexify(x) * " \$\$")
Base.show(io::IO, ::MIME"text/latex", x::Equation) = print(io, "\$\$ " * latexify(x) * " \$\$")
Base.show(io::IO, ::MIME"text/latex", x::Vector{Equation}) = print(io, "\$\$ " * latexify(x) * " \$\$")
Base.show(io::IO, ::MIME"text/latex", x::AbstractArray{<:RCNum}) = print(io, "\$\$ " * latexify(x) * " \$\$")

@hersle
Copy link
Contributor Author

hersle commented Nov 7, 2024

You are right. I think that is a killer argument.

TL;DR: one way or another, formatting must be tied to each variable.

@hersle hersle closed this Nov 7, 2024
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.

4 participants