Are there any obvious methods for streamlining this Cartesian -> Spherical coordinates code

TL;DR: in the below code example, are there any obvious methods for streamlining code, maybe through more functional programming maps?

Let me know if this question is too vague, and if the MWE is too large to feasibly work with.

I needed to investigate what it looks like when one analyzes a rectangle in 3D space using spherical coordinates, specifically the polar and azimuthal angle. I decided to try using Typst for the entirety of the computation. The below is the culmination of my efforts. The parameters height, width, central-angle, and dist-nps aren’t really important, as they’re more technical geometry specifications. What is important is the 4 points I specified as the vertices of the rectangle. From there, I used Lilaq for array generation and plotting, and through some array mapping, zipping, and concatenation, I was able to generate a good plot.

However, there is a decent amount of repeated code, since I initially tackled each edge of the rectangle separately. I wanted to see if there were any Typst scripting capabilities I wasn’t using to their full capacity.

Thus, in the below computation and plotting, is there any obvious ways I could streamline the code? Of course, streamlining at the cost of readability is not something to strive for, but I feel there’s probably some streamlining that can be done without making the process incomprehensible.

#import "@preview/lilaq:0.4.0" as lq

#set page(
  height: auto,
  width: auto,
  margin: 2mm
)

#show lq.selector(lq.diagram): set text(.9em) // Figure Text Size
#show lq.selector(lq.legend): set text(.9em) // Example Specific Text Size
#show lq.selector(lq.tick-label): set text(.9em)

#{
  // Scalar parameter that will span over each edge of the rectangle
  let param = lq.linspace(0, 1)
  
  // Parameters for defining bounds of rectangle
  let height = 0.02 * 18 * 2
  let width = 0.02 * 15 * 2
  let central-angle = 0.2175rad
  let dist-nps = 3.07
  
  // Vertices of rectangle
  let P0 = (height/2, -dist-nps*calc.sin(central-angle) - (width/2 * calc.cos(central-angle)), dist-nps * calc.cos(central-angle) - (width/2 * calc.sin(central-angle)))
  let P1 = (height/2, -dist-nps*calc.sin(central-angle) + (width/2 * calc.cos(central-angle)), dist-nps * calc.cos(central-angle) + (width/2 * calc.sin(central-angle)))
  let P2 = (-height/2, -dist-nps*calc.sin(central-angle) + (width/2 * calc.cos(central-angle)), dist-nps * calc.cos(central-angle) + (width/2 * calc.sin(central-angle)))
  let P3 = (-height/2, -dist-nps*calc.sin(central-angle) - (width/2 * calc.cos(central-angle)), dist-nps * calc.cos(central-angle) - (width/2 * calc.sin(central-angle)))
  
  // Edge attaches P0 and P1 of rectangle
  let x-span01 = param.map(x => P0.at(0) + (P1.at(0) - P0.at(0))*x)
  let y-span01 = param.map(x => P0.at(1) + (P1.at(1) - P0.at(1))*x)
  let z-span01 = param.map(x => P0.at(2) + (P1.at(2) - P0.at(2))*x)
  
  // Edge attaches P1 and P2 of rectangle
  let x-span12 = param.map(x => P1.at(0) + (P2.at(0) - P1.at(0))*x)
  let y-span12 = param.map(x => P1.at(1) + (P2.at(1) - P1.at(1))*x)
  let z-span12 = param.map(x => P1.at(2) + (P2.at(2) - P1.at(2))*x)
  
  // Edge attaches P2 and P3 of rectangle
  let x-span23 = param.map(x => P2.at(0) + (P3.at(0) - P2.at(0))*x)
  let y-span23 = param.map(x => P2.at(1) + (P3.at(1) - P2.at(1))*x)
  let z-span23 = param.map(x => P2.at(2) + (P3.at(2) - P2.at(2))*x)
  
  // Edge attaches P3 and P0 of rectangle
  let x-span30 = param.map(x => P3.at(0) + (P0.at(0) - P3.at(0))*x)
  let y-span30 = param.map(x => P3.at(1) + (P0.at(1) - P3.at(1))*x)
  let z-span30 = param.map(x => P3.at(2) + (P0.at(2) - P3.at(2))*x)
  
  // Zip the coordinates into of each edge
  let span01 = x-span01.zip(y-span01, z-span01)
  let span12 = x-span12.zip(y-span12, z-span12)
  let span23 = x-span23.zip(y-span23, z-span23)
  let span30 = x-span30.zip(y-span30, z-span30)
  
  // Concatonate rectangle points into single array
  let rect-span = span01 + span12 + span23 + span30
  
  // Cartesian to spherical coordinates conversion
  let mag(x, y, z) = calc.sqrt(calc.pow(x, 2) + calc.pow(y, 2) + calc.pow(z, 2))
  let ph(x, y) = calc.atan2(x, y).rad()
  let thet(x, y, z) = calc.acos(z/mag(x, y, z)).rad()
  
  let mag-span = rect-span.map(x => mag(x.at(0), x.at(1), x.at(2)))
  let ph-span = rect-span.map(x => ph(x.at(0), x.at(1)) + 2*calc.pi)
  let thet-span = rect-span.map(x => thet(x.at(0), x.at(1), x.at(2)))
  
  lq.diagram(
    xlim: auto,
    ylim: auto,
    xlabel: [$theta$],
    ylabel: [$phi$],
    title: [],
    // legend: (position: (100% + .5em, 40%)),
    legend: none,
    cycle: lq.color.map.petroff10,
    lq.plot(
      thet-span,
      ph-span,
      stroke: auto,
      mark: none
    )
  )
}

One thing that simplifies writing the code is using unpacking and argument spreading, for example:

let mag-span = rect-span.map(x => mag(..x))
let ph-span = rect-span.map(((x, y, _)) => ph(x, y) + 2*calc.pi)

Effect on performance: unknown.

You could also consider using unpacking in certain places:

// Edge attaches P0 and P1 of rectangle
let (x0, y0, z0) = P0
let (x1, y1, z1) = P1
let x-span01 = param.map(s => x0 + (x1 - x0)*s)
let y-span01 = param.map(s => y0 + (y1 - y0)*s)
let z-span01 = param.map(s => z0 + (z1 - z0)*s)

Effect on performance: Maybe good, avoids the many .at calls? But that’s just a guess. Effect on readability: locally improved, is it a net positive, you decide.

Since this operation:

let x-span01 = param.map(x => P0.at(0) + (P1.at(0) - P0.at(0))*x)

Is repeated 3 * 4 = 12 times it should just be a function.

let span(pA, pB, index) = {
  let xA = pA.at(index)
  let xB = pB.at(index)
  param.map(s => xA + (xB - xA)*s)
}
let span-xyz(pA, pB) = {
  (span(pA, pB, 0), span(pA, pB, 1), span(pA, pB, 2))
}

let (x-span01, y-span01, z-span01) = span-xyz(P0, P1)
let (x-span12, y-span12, z-span12) = span-xyz(P1, P2)

That’s the rinse and repeat of it, simplify, extract functions, combine similar operations with new functions. And repeat that a step at a time.

1 Like