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 how Table 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.

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]

Figure 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]]

Figure 2

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]]

Figure 3

Tags:

Plotting