Reference/API

SymPy.SymPy β€” Module

SymPy package to interface with Python's SymPy library through PyCall.

The basic idea is that a new type – Sym – is made to hold symbolic objects. For this type, the basic functions from SymPy and appropriate functions of Julia are overloaded for Sym objects so that the expressions are treated symbolically and not evaluated immediately. Instances of this type are created by the constructor Sym, the function symbols or the macro @vars.

On loading, a priviledged set of the functions from the sympy module are defined as generic functions with their first argument narrowed to symbolic types. Others may be accessed by qualification, as in sympy.trigsimp. Calling import_from(sympy) will import the rest. SymPy methods are called through Python's dot-call syntax. To find documentation on SymPy functions and methods, one should refer to SymPy's website.

Plotting is provided through Plots recipes. For details, see the help page for sympy_plotting.

The package documentation provides many examples.

SymPy.𝑄 β€” Module
𝑄

Exported symbol for SymPy.Q, a Julia module implementing sympy.Q. "Questions" can be asked through the patterns 𝑄.query(value) (𝑄 is entered as [slash]itQ[tab]) or SymPy.Q.query(value)but not as sympy.Q.query(value)

Note

At one time, the symbol Q was exported for this. To avoid namespace clutter, the unicode alternative is now used. Legacy code would need a definition like const Q = SymPy.Q to work.

SymPy.IM β€” Constant

IM is a symbolic im

SymPy.PI β€” Constant

PI is symbolic pi

SymPy.oo β€” Constant

oo is a symbolic infinity. Example: integrate(exp(-x), x, 0, oo).

SymPy.sympy β€” Constant
sympy

Variable from pyimport("sympy"). Numerous methods are available through Python's dot-call syntax.

SymPy.sympy_plotting β€” Constant

Plotting of symbolic objects.

The Plots package provide a uniform interface to many of Julia's plotting packages. SymPy plugs into Plots' "recipes."

The basic goal is that when Plots provides an interface for function objects, this package extends the interface to symbolic expressions.

In particular:

  • plot(ex::Sym, a, b; kwargs...) will plot a function evaluating ex over [a,b]

Example. Here we use the default backend for Plots to make a plot:

using Plots
@vars x
plot(x^2 - 2x, 0, 4)
  • plot(ex1, ex2, a, b; kwargs...) will plot the two expressions in a parametric plot over the interval [a,b].

Example:

@vars x
plot(sin(2x), cos(3x), 0, 4pi) ## also

For a few backends (those that support :path3d) a third symbolic expression may be added to have a 3d parametric plot rendered:

plot(sin(x), cos(x), x, 0, 4pi) # helix in 3d
  • plot(xs, ys, expression) will make a contour plot (for many backends).
@vars x y
plot(range(0,stop=5, length=50), range(0,stop=5, length=50), x*y)
  • To plot the surface z=ex(x,y) over a region we have Plots.surface. For example,
@vars x y
surface(-5:5, -5:5, 25 - x^2 - y^2)
  • a vectorfield plot can (inefficiently but directly) be produced following this example:
function vfieldplot(fx, fy; xlim=(-5,5), ylim=(-5,5), n=8)
    xs = range(xlim[1], stop=xlim[2], length=n)
    ys = range(ylim[1], stop=ylim[2], length=n)

    us = vec([x for x in xs, y in ys])
    vs = vec([y for x in xs, y in ys])
    fxs = vec([fx(x,y) for x in xs, y in ys])
    fys = vec([fy(x,y) for x in xs, y in ys])

    mxs = maximum(abs.(filter(!isinf, filter(!isnan, fxs))))
    mys = maximum(abs.(filter(!isinf, filter(!isnan, fys))))
    d = 1/2 * max((xlim[2]-xlim[1])/mxs, (ylim[2]-ylim[1])/mys) / n

    quiver(us, vs, quiver=(fxs.*d, fys.*d))

end
fx = (x + y) / sqrt(x^2 + y^2)
fy = (x - y) / sqrt(x^2 + y^2)
vfieldplot(fx, fy)

  • To plot two or more functions at once, the style plot([ex1, ex2], a, b) does not work. Rather, use plot(ex1, a, b); plot!(ex2), as in:
@vars x
plot(sin(x), 0, 2pi)
plot!(cos(x))

Some graphics provided by SymPy are available if PyPlot is installed, such as:

  • sympy.plotting.plot3d_parametric_surface
  • sympy.plotting.plot_implicit

Plot the parametrically defined surface [exs[1](u,v), exs[2](u,v), exs[3](u,v)] over [a0,a1] x [b0,b1]. The specification of the variables uses a tuple of the form (Sym, Real, Real) following the style of SymPy in integrate, say, where disambiguation of variable names is needed.

@vars theta, phi
r = 1
sympy.plotting.plot3d_parametric_surface((r*sin(theta)*sin(phi), r*sin(theta)*cos(phi), r*cos(theta)),
                        (theta, 0, pi), (phi, 0, pi/2))
  • sympy.plotting.plot_implicit(equation, (xvar, x0, x1), (yvar, y0, y1)) will plot implicitly the equation.
@syms x y
sympy.plotting.plot_implicit(Eq(x^2+ y^2,3), (x, -2, 2), (y, -2, 2))  # draw a circle
SymPy.zoo β€” Constant

zoo complex inifinity

SymPy.SymFunction β€” Type

Create a symbolic function. These can be used for specifying differential equations. For these objects we can specify derivatives with the transpose operator (e.g., u'') as opposed to, say diff(u(x), x, 2).

Example:

u = SymFunction("u")
u'

Alternatively, we can pass a comma separated string of variable names to create more than one at a time. (The cls=symfunction is no longer supported):

F,G,H = SymFunction("F, G, H")

This is just a thin wrapper around sympy.Functioni for symbolic functions that allows prime notation in place of using diff.

The macro @symfuns is also available for constructing symbolic functions.

SymPy.SymMatrix β€” Type
SymMatrix

Type to store a SymPy matrix, as created by sympy.ImmutableMatrix.

These have 0-based indexing defined for them to match SymPy

The traditional infix mathmatical operations are defined, but no dot broadcasting.

The convert(Matrix{Sym}, M) call is useful to covert to a Julia matrix

SymPy.VectorField β€” Type

VectorField(fx, fy): create an object that can be plotted as a vector field.

A vectorfield plot draws arrows at grid points proportional to [fx(x_i,y_i), fy(x_i,y_i)] to visualize the field generated by [fx, fy].

The plot command: plot(VectorField(fx, fy), xlims=(-5,5), ylims=(-5,5), n=8) will draw the vectorfield. This uses the default values, so the same graph would be rendered by plot(VectorField(fx,fy)).

To faciliate the visualization of solution to the ODE y' = F(x, y(x)), the call plot(VectorField(F)) will work. (The order is x then y, though often this is written as F(y(x),x).)

SymPy objects can be passed to VectorField, but this is a bit fragile, as they must each have two variables so that they can be called with two variables. (E.g., y(1,2) will be 1 not 2, as might be intended.)

Examples:

using Plots

fx(x,y) = sin(y); fy(x,y) = cos(y)
plot(VectorField(fx, fy), xlims=(-2pi, 2pi), ylims=(-2pi,2pi))

# plot field of y' = 3y*x over (-5,5) x (-5,5)
F(x,y) = 3*y*x
plot(VectorField(F))

# plot field and solution u' = u*(1-u)
u = SymFunction("u"); @vars x
F(x,y) = y*(1-y)
out = dsolve(u'(x) - F(x, u(x)), x, (u, 0, 1))
plot(VectorField(F), xlims=(0,5), ylims=(0,2))
plot!(rhs(out))
SymPy.:≦ β€” Method

This is \leqq<tab> mapped as an infix operator to Le

SymPy.:≧ β€” Method

This is \geqq<tab> mapped as an infix operator to Ge

SymPy.:β‰ͺ β€” Method

This is \ll<tab> mapped as an infix operator to Lt

SymPy.:≫ β€” Method

This is \gg<tab> mapped as an infix operator to Gt

SymPy.:β©΅ β€” Method

For infix Eq one can use \Equal<tab> unicode operator

SymPy.N β€” Method
N(x::Sym, digits::Int)

N can take a precision argument, whichm when given as an integer greater than 16, we try to match the digits of accuracy using BigFloat precision on conversions to floating point.

SymPy.N β€” Method
N(ex)

Convert a Sym value to a numeric Julian value.

In SymPy, N(ex, options...) is identifcal to ex.evalf(options...) and is used to convert expressions into floating-point approximations. A positional precision argument indicates the number of digits, keyword arguments chop can be used to trim floating point roundoff errors and subs for free variable substitution prior to conversions.

For example, symbolic roots can be computed numerically, even if not available symbolically, by calling N on the values.

Using SymPy within Julia makes having two such functions useful:

  • one to do the equivalent of SymPy's evalf call
  • one to convert these expressions back into Julia objects (like convert(T, ex))

We use N to return a Julia object and evalf to return a symbolic object. The type of Julia object is heurisitically identified.

Examples:

julia> using SymPy

julia> x = Sym("x")
x

julia> p = subs(x, x, pi)
Ο€

julia> N(p)                            # float version of pi
Ο€ = 3.1415926535897...

julia> p.evalf(60)                     # 60 digits of pi, as a symbolic value
3.14159265358979323846264338327950288419716939937510582097494

julia> N(p, 60)                        # when a precision is given, "Big" values are returned
3.141592653589793238462643383279502884197169399375105820974939

julia> r = subs(x,x,1.2)
1.20000000000000

julia> N(r)                            # float
1.2

julia> q = subs(x, x, 1//2)
1/2

julia> N(q)                            # 1//2
1//2

julia> z = solve(x^2 + 1)[1]           # -β…ˆ
-β…ˆ

julia> N(z)                            # 0 - 1im
0 - 1im

julia> z.evalf()
-1.0β‹…β…ˆ

julia> rts = solve(x^5 - x + 1)
5-element Array{Sym,1}:
 CRootOf(x^5 - x + 1, 0)
 CRootOf(x^5 - x + 1, 1)
 CRootOf(x^5 - x + 1, 2)
 CRootOf(x^5 - x + 1, 3)
 CRootOf(x^5 - x + 1, 4)

julia> [r.evalf() for r in rts]          # numeric solutions to quintic
5-element Array{Sym,1}:
                       -1.16730397826142
 -0.181232444469875 - 1.08395410131771*I
 -0.181232444469875 + 1.08395410131771*I
 0.764884433600585 - 0.352471546031726*I
 0.764884433600585 + 0.352471546031726*I

julia> [N(r) for r in rts]             
5-element Array{Number,1}:
                     -1.167303978261418684256045899854842180720560371525489039140082449275651903429536
 -0.18123244446987538 - 1.0839541013177107im
 -0.18123244446987538 + 1.0839541013177107im
   0.7648844336005847 - 0.35247154603172626im
   0.7648844336005847 + 0.35247154603172626im

N returns the value unchanged when it has free symbols.

SymPy.Permutation β€” Method
Permutation(args...)

This module mostly implements SymPy's Permutation module.

A permuation can be represented in different ways. Here a permutation is a reaarangment of the values 0, 1, ..., n. For example, the mapping 0->2, 1->3, 2->0, 3->1 can be presented by a vector: sigma = [2,3,0,1] where sigma[i] = j when i -> j. Otheriwse, it can be presented as a product of cycles: (0 2)(1 3) which reads 0 goes to 2 which goes to 0 (wrapping) and 1 goes to 3 and 3 goes to 1.

Either representation can be passed through the Permutation constructor.

For the vector notation – 0-based – it is passed directly to the constructor:

julia> using SymPy

julia> p = Permutation([2,3,0,1])
(0 2)(1 3)

If a range describes a permutation, it can be used as well:

julia> id = Permutation(10:-1:0)
(0 10)(1 9)(2 8)(3 7)(4 6)

Cycle notation can more compactly describe a permuation, it can be passed in as a container of cycles specified through tuples or vectors:

julia>  p = Permutation([(0,2), (1,3)])
(0 2)(1 3)

The latter can be be expresed more quickly as

julia> p = Permutation(0,2)(1,3)
(0 2)(1 3)

This works as a single cycle can be passed to the Permutation constructor with values separated by commas and the "call" method for Permuation objects is overloaded: for a single argument, the mapping i -> j is created (also the notation i^p returns this) but if more than one argument is given, a cycle is created and multiplied on the right by p, so that the above becomes (0,2) * (1,3).

Here are two permutations forming the symmetries of square, naturally represented in the two ways:

julia> flip = Permutation([[0,1],[2,3]])  # or Permutation(0,1)(2,3)
(0 1)(2 3)

julia> rotate = Permutation([1,2,3,0])    # or Permutation(0,1,2,3) in cycle notation
(0 1 2 3)

Operations on permutations include:

  • a function call, p(i) to recover j where i -> j, also i^p.
  • * for multiplication. The convention is (p*q)(i) = q(p(i)) or with the ^ notation: i^(p*q) = (i^p)^q.
  • + for multiplication when p and q commute, where a check on commuting is performed.
  • inv for the inverse permutation.
  • /, where p/q is p * inv(q).
  • p^n for powers. We have inv(p) = p^(-1) and p^order(p) is the identity.
  • p^q for conjugate, defined by inv(q) * p * q.

We can see that a flip is an involution through:

julia> flip^2  # the identity
(3)

whereas a rotation is not (as it has order 4)

julia> rotate * rotate
(0 2)(1 3)

julia> rotate.order()
4

These two operations do not commute:

julia> flip * rotate  # (3)(0 2) -- note (n) is the identity
(3)(0 2)
julia> rotate * flip  # (1 3)
(1 3)

We can see this is the correct mapping 1 -> 3 with

julia> (1^rotate)^flip, 1^(rotate*flip), flip(rotate(1))
(3, 3, 3)

We can check that flip and rotate^2 do commute:

julia> id = Permutation(3)   # (n) is the identify
(3)

julia> flip.commutator(rotate^2) == id
true

The conjugate for flip and rotate does the inverse of the flip, then rotates, then flips:

julia> rotate^flip
(0 3 2 1)

This is different than flip^rotate. As flip commutes with rotate^2 this will return rotate^2:

julia> (rotate^2)^flip
(0 2)(1 3)
Differences

There is no support currently for the Cycle class

SymPy.PermutationGroup β€” Method
PermutationGroup()

Create Permutation group from group generators

A PermutationGroup is one generated by a collection of permutations.

Some pre-defined groups are built-in:

  • SymmetricgGroup(n): S_n or all symmetries of an n-gon
  • CyclicGroup: the group Z_n
  • DihedralGroup: Group formed by a flip and rotation
  • AlternativeGroup: Subgroup of S_n of even elements
  • AbelianGroup: Returns the direct product of cyclic groups with the given orders.

Differences:

  • use collect(generate(G)) in place of list(G.generate())
SymPy.Wild β€” Method
Wild(x)

create a "wild card" for pattern matching

SymPy.ask β€” Method
ask(query)

Returns true, false, or nothing; ask

Example:

julia> using SymPy

julia> @vars x y integer=true
(x, y)

julia> ask(𝑄.integer(x*y), And(𝑄.integer(x), 𝑄.integer(y)))
true

julia> ## really slow isprime:
       filter(x -> ask(𝑄.prime(x)), 1:10)
4-element Array{Int64,1}:
 2
 3
 5
 7
SymPy.dsolve β€” Method

dsolve(eqn, var, args..,; ics=nothing, kwargs...)

Call sympy.dsolve with possible difference for initial condition specification.

For problems with an initial condition, the ics argument may be specified. This is different from the ics argument of sympy.dsolve. (Call directly if that is preferred.)

Here ics allows the specification of a term like f(x0) = y0 as a tuple (f, x0, y0). Similarly, a term like f''(x0)=y0 is specified through (f'', x0, y0). If more than one initial condition is needed, a tuple of tuples is used, as in ((f,x0,y0), (f',x0,z0)).

Example:

julia> using SymPy

julia> x = Sym("x")
x

julia> y = SymFunction("y")
y

julia> eqn = y'(x) - y(x);

julia> dsolve(eqn, y(x), ics=(y,0,1)) |> string # technical to avoid  parsing issue with doctesting
"Eq(y(x), exp(x))"

julia> eqn = y''(x) - y(x) - exp(x); 

julia> dsolve(eqn, y(x), ics=((y,0,1), (y, 1, 1//2))) |> string
"Eq(y(x), (x/2 - (-1 + 2*exp(-1) + E)/(4*sinh(1)))*exp(x) - (1 - 3*E)*exp(-x)/(4*sinh(1)))"
SymPy.elements β€” Method
elements(s)

return elements of a set s as an array, unlike convert(Set,s)

SymPy.free_symbols β€” Method
free_symbols(ex)
free_symbols(ex::Vector{Sym})

Return vector of free symbols of expression or vector of expressions. The results are orderded by sortperm(string.(fs)).

Example:

julia> using SymPy

julia> @vars x y z a
(x, y, z, a)

julia> free_symbols(2*x + a*y) # [a, x, y]
3-element Array{Sym,1}:
 a
 x
 y
SymPy.import_from β€” Method
import_from(module, meths; kwargs...)

Import methods from a python module. Implements functionality of from module import function in Python.

  • module: a python module, such as sympy
  • meths: nothing or a tuple of symbols to import. If nothing, then all member functions of the module are imported (but not constructors or other objects)
  • Ms: additional Julia Modules to import from. By default, a few base modules are searched for to avoid namespace collisions.
  • typ: a symbol indicating variable type for first argument that the new function should be restricted to. For most, the default, :SymbolicObject will be appropriate
  • exclude: when importing all (meths=nothing), this can be used to avoid importing some methods by name. The default has a few to avoid.

Examples:

import_from(sympy)  # bring in functions from sympy (done `import_sympy`)
import_from(sympy, (:sin, :cos))  # just bring in a few methods
import_from(sympy , (:Wild,), typ=:Any) # Allows `Wild("x")`
#
import PyCall
PyCall.pyimport_conda("sympy.physics.wigner", "sympy")
import_from(sympy.physics.wigner)
SymPy.lambdify β€” Function
 lambdify(ex, vars; typ, fns, values, use_julia_code=false, invoke_latest=true)

Take a symbolic expression and returns an anonymous Julia function.

Converts from a SymPy object to a Julia expression by walking the SymPy expression tree and converting each step, (e.g. essentially calls convert(Expr, ex)).

Then creates a function. The function arguments are based on free_symbols and its ordering, unless vars is specified directly.

  • use_julia_code=true will use SymPy's conversion to an expression, the default is false

  • invoke_latest=true calls Base.invokelatest to work around world age issues. This is the safe default, but setting to false will result in faster-executing functions.

Example:

julia> using SymPy

julia> @vars x y z
(x, y, z)

julia> ex = x^2 * sin(x)
 2       
x β‹…sin(x)

julia> fn = lambdify(ex)
#87 (generic function with 1 method)

julia> fn(pi)
1.2086779438644711e-15

julia> ex = x + 2y + 3z
x + 2β‹…y + 3β‹…z

julia> fn = lambdify(ex)
#87 (generic function with 1 method)

julia> fn(1,2,3) # order is by free_symbols
14

julia> ex(x=>1, y=>2, z=>3)
14

julia> fn = lambdify(ex, (y,x,z))
#87 (generic function with 1 method)

julia> fn(1,2,3)
13
SymPy.nonlinsolve β€” Method
nonlinsolve

Note: if passing variables in use a tuple (e.g., (x,y)) and not a vector (e.g., [x,y]).

SymPy.plot_implicit β€” Method

Plot an implicit equation

@syms x y
plot_implicit(Eq(x^2+ y^2,3), (x, -2, 2), (y, -2, 2))
SymPy.plot_parametric_surface β€” Function

Render a parametrically defined surface plot.

Example:

@vars u, v
plot_parametric_surface((u*v,u-v,u+v), (u,0,1), (v,0,1))

This uses PyPlot, not Plots for now.

SymPy.simplify β€” Method
simplify

SymPy has dozens of functions to perform various kinds of simplification. There is also one general function called simplify that attempts to apply all of these functions in an intelligent way to arrive at the simplest form of an expression. (See Simplification for details on simplify and other related functionality).

For non-symbolic expressions, simplify returns its first argument.

SymPy.solve β€” Method
solve

Use solve to solve algebraic equations.

Examples:

julia> using SymPy

julia> @vars x y a b c d
(x, y, a, b, c, d)

julia> solve(x^2 + 2x + 1, x) # [-1]
1-element Array{Sym,1}:
 -1

julia> solve(x^2 + 2a*x + a^2, x) # [-a]
1-element Array{Sym,1}:
 -a

julia> solve([a*x + b*y-3, c*x + b*y - 1], [x,y]) # Dict(y => (a - 3*c)/(b*(a - c)),x => 2/(a - c))
Dict{Any,Any} with 2 entries:
  x => 2/(a - c)
  y => (a - 3*c)/(b*(a - c))
Note

A very nice example using solve is a blog entry on Napolean's theorem by Xing Shi Cai.

SymPy.subs β€” Method

subs is used to subsitute a value in an expression with another value. Examples:

julia> using SymPy

julia> x,y = symbols("x,y")
(x, y)

julia> ex = (x-y)*(x+2y)
(x - y)β‹…(x + 2β‹…y)

julia> subs(ex, (y, y^2))
βŽ›     2⎞ βŽ›       2⎞
⎝x - y βŽ β‹…βŽx + 2β‹…y ⎠

julia> subs(ex, (x,1), (y,2))
-5

julia> subs(ex, (x,y^3), (y,2))
72

julia> subs(ex, y, 3)
(x - 3)β‹…(x + 6)

There is a curried form of subs to use with the chaining |> operator

julia> ex |> subs(x,β„―)
(β„― - y)β‹…(2β‹…y + β„―)

The use of pairs gives a convenient alternative:

julia> subs(ex, x=>1, y=>2)
-5

julia> ex |> subs(x=>1, y=>2)
-5
SymPy.@symfuns β€” Macro
symfuns...

Thanks to @alhirzel for the contribution.

SymPy.@vars β€” Macro
@vars x y z

Define symbolic values, possibly with assumptions

Examples:

@vars x y
@vars a b real=true
Base.getindex β€” Method
x[i]

Some SymPy Python objects have index notation provided for them through __getitem__. This allows Julia's getindex to dispatch to Python's __getitem__. The index (indices) must be symbolic. This will use 0-based indexing, as it is a simple pass through to Python.

Examples:

julia> using SymPy

julia> i,j = sympy.symbols("i j", integer=True)
(i, j)

julia> x = sympy.IndexedBase("x")
x

julia> a = sympy.Sum(x[i], (i, 1, j))
  j       
 ___      
 β•²        
  β•²       
  β•±   x[i]
 β•±        
 β€Ύβ€Ύβ€Ύ      
i = 1     
Base.getindex β€” Method
M[i,j]

Define getindex for SymPy's ImmutableMatrix class.

SymMatrix is 0-based, like python, not Julia. Use Matrix{Sym} for that.

Base.match β€” Method
match(pattern, expression, ...)

Match a pattern against an expression; returns a dictionary of matches.

If a match is unsuccesful, returns an empty dictionary. (SymPy returns "nothing")

The order of the arguments follows Julia's match function, not sympy.match, which can be used directly, otherwise.

Base.occursin β€” Method

occursin(x, G::SymPermutationGroup)

Does G contain x. (In SymPy, this is `contains.)

Base.replace β€” Method
replace(expression, pattern, value, ...)

From: SymPy Docs

Traverses an expression tree and performs replacement of matching subexpressions from the bottom to the top of the tree. The default approach is to do the replacement in a simultaneous fashion so changes made are targeted only once. If this is not desired or causes problems, simultaneous can be set to false. In addition, if an expression containing more than one Wild symbol is being used to match subexpressions and the exact flag is true, then the match will only succeed if non-zero values are received for each Wild that appears in the match pattern.

Differences from SymPy:

  • "types" are specified via calling func on the head of an expression: func(sin(x)) -> sin, or directly through sympy.sin

  • functions are supported, but only with PyCall commands.

Examples (from the SymPy docs)

julia> using SymPy

julia> x, y, z = symbols("x, y, z")
(x, y, z)

julia> f = log(sin(x)) + tan(sin(x^2)); string(f) # `string(f)` only so doctest can run
"log(sin(x)) + tan(sin(x^2))"

"type" -> "type"

Types are specified through func:

julia> func = SymPy.Introspection.func
func (generic function with 1 method)

julia> replace(f, func(sin(x)), func(cos(x))) |> string  # type -> type
"log(cos(x)) + tan(cos(x^2))"

julia> replace(f, sympy.sin, sympy.cos)  |>  string
"log(cos(x)) + tan(cos(x^2))"

julia> sin(x).replace(sympy.sin, sympy.cos, map=true)
(cos(x), Dict{Any,Any}(sin(x) => cos(x)))

The func function finds the head of an expression (sin and cos above). This could also have been written (perhaps more directly) as:

julia> replace(f, sympy.sin, sympy.cos) |> string
"log(cos(x)) + tan(cos(x^2))"

"type" -> "function"

To replace with a more complicated function, requires some assistance from Python, as an anonymous function must be defined witin Python, not Julia:

julia> import PyCall

julia> ## Anonymous function a -> sin(2a)
       PyCall.py"""
       from sympy import sin, Mul
       def anonfn(*args):
           return sin(2*Mul(*args))
       """)


julia> replace(f, sympy.sin, PyCall.py"anonfn")
                   βŽ›   βŽ›   2⎞⎞
log(sin(2β‹…x)) + tan⎝sin⎝2β‹…x ⎠⎠

"pattern" -> "expression"

Using "Wild" variables allows a pattern to be replaced by an expression:

julia> a, b = Wild("a"), Wild("b")
(a_, b_)

julia> replace(f, sin(a), tan(2a)) |> string
"log(tan(2*x)) + tan(tan(2*x^2))"

julia> replace(f, sin(a), tan(a/2)) |> string
"log(tan(x/2)) + tan(tan(x^2/2))"

julia> f.replace(sin(a), a) |> string
"log(x) + tan(x^2)"

julia> (x*y).replace(a*x, a)
y

In the SymPy docs we have:

Matching is exact by default when more than one Wild symbol is used: matching fails unless the match gives non-zero values for all Wild symbols."

julia> replace(2x + y, a*x+b, b-a)  # y - 2
y - 2

julia> replace(2x + y, a*x+b, b-a, exact=false)  # y + 2/x
    2
y + ─
    x

"pattern" -> "func"

The function is redefined, as a fixed argument is passed:

julia> PyCall.py"""
       from sympy import sin
       def anonfn(a):
           return sin(2*a)
       """

julia> replace(f, sin(a), PyCall.py"anonfn")
                   βŽ›   βŽ›   2⎞⎞
log(sin(2β‹…x)) + tan⎝sin⎝2β‹…x ⎠⎠

"func" -> "func"


julia> PyCall.py"""
       def fn1(expr):
           return expr.is_Number

       def fn2(expr):
           return expr**2
       """

julia> replace(2*sin(x^3), PyCall.py"fn1", PyCall.py"fn2")
     βŽ› 9⎞
4β‹…sin⎝x ⎠
julia> PyCall.py"""
       def fn1(x):
           return x.is_Mul

       def fn2(x):
           return 2*x
       """

julia> replace(x*(x*y + 1), PyCall.py"fn1", PyCall.py"fn2")
2β‹…xβ‹…(2β‹…xβ‹…y + 1)
SymPy.import_sympy β€” Method
import_sympy

This method imports all functions from mpmath and a priviledged set of functions from sympy, as well as the relational operators.

These functions are narrowed on their first argument being of type SymbolicObject.

A few modules are checked for namespace collisions.

If a function naturally takes an non-Symbolic argument as a first argument, then it can be qualified: e.g. sympy.sin(2) (as opposed to sin(Sym(2))).

If a constructor is needed (which is not a function) then it must be qualified. (E.g. sympy.Function("F"), though for this particular case, there is SymFunction defined for convenience.)