How can I prevent renaming the arguments to Function[{...},...] for making plots of spherical harmonics coloured by phase
Let's look at a simpler example:
makeFunc[body_] := Function[{x}, body]
makeFunc[x^2]
(* Function[{x$}, x^2] *)
As you've correctly identified, the problem is that the arguments of Function
are renamed. Interestingly, this replacement is performed during the evaluation of the definition of makeFunc
, not as part of the evaluation of Function[…]
: The evaluator notices that the expression body
contains symbols that might clash with the arguments of the Function[…]
expression. To prevent this from happening, the functions arguments are renamed. Some reading on the topic:
- Scoping constructs, lexical scoping and variable renamings
- Variables in Pure Functions and Rules
- Unexpected variable renaming depending on the form of a pure function
- Local constants
To fix the issue, you have a few options:
Hide the fact that there is a scoping construct:
makeFunc[body_] := Function@@Hold[{x}, body] makeFunc[x^2] (* Function[{x}, x^2] *)
Perform a manual replacement of the variables inside the
Function
, as suggested by @kglr:makeFunc[body_]:=Function[{x1}, body /. x -> x1] makeFunc[x^2] (* Function[{x1$}, x^2 /. x -> x1$] *) makeFunc[x^2][y] (* y^2 *)
Use the "correct" way to pass symbolic expressions ("correct" as in "used by built-in functions and not dependent on magic variable names"): Pass in the symbols as well:
makeFunc[body_, sym_] := Function[{sym}, body] makeFunc[x^2, x] (* Function[{x}, x^2] *) makeFunc[y^2, y] (* Function[{x}, x^2] *)
While it is the most work for the user, this approach has several advantages:
- It does not break if the user defined
x
as something else already - the user can just choose a different symbol here - If you employ the proper
Hold*
attributes, you can even allow the user to reuse already defined symbols, similar to howTable
localizes its iterator variable. - It does not break when used inside a package, since there is no issue with contexts of the symbols
- It is very clear in intent, since the user doesn't have to know which symbols are to be used, since they have to be manually specified.
- It does not break if the user defined
Here we can use function Texture[]
. Note that function Arg[f]
is discontinuous, so there are two sectors without coverage. Create a texture
texture[l_, m_] :=
DensityPlot[
Arg[SphericalHarmonicY[l, m, \[Theta], \[Phi]]], {\[Theta], 0,
Pi}, {\[Phi], 0, 2 Pi}, ColorFunction -> Hue, PlotPoints -> 200,
Frame -> False]
Now use this to cover
sphWithPhase[l_, m_] :=
SphericalPlot3D[
Abs[SphericalHarmonicY[l, m, \[Theta], \[Phi]]]^2, {\[Theta], 0,
Pi}, {\[Phi], 0, 2.01 Pi}, Mesh -> None,
PlotStyle -> Texture[texture[l, m]], Lighting -> "Neutral",
Boxed -> False, Axes -> False, PlotRange -> All, PlotPoints -> 150]
sphWithPhase[1, -1]
Now we show how to modify the source code to get a similar result. We use a hint from Mr.Wizard.
psi[l_, m_] := SphericalHarmonicY[l, m, \[Theta], \[Phi]]
sphWithPhase[f_] :=
SphericalPlot3D[Abs[f]^2, {\[Theta], 0, Pi}, {\[Phi], 0, 2 Pi},
ColorFunction -> (Function[{x, y, z, \[Theta], \[Phi], r}, #] &@
Hue[Arg[f]/2/Pi]), PlotRange -> All,
ColorFunctionScaling -> False, Mesh -> None, Boxed -> False,
Axes -> False]
sphWithPhase[psi[1, -1]]
Now we use the hint from @kglr.
psi[l_, m_] := SphericalHarmonicY[l, m, \[Theta], \[Phi]]
sphWithPhase[f_] :=
SphericalPlot3D[Abs[f]^2, {\[Theta], 0, Pi}, {\[Phi], 0, 2 Pi},
ColorFunction -> (Hue[(Arg[f /. {\[Theta] -> #4, \[Phi] -> #5}] +
Pi)/2/Pi] &), PlotRange -> All,
ColorFunctionScaling -> False, Mesh -> None, Boxed -> False,
Axes -> False]
sphWithPhase[psi[1, -1]]