enkimute / ganja.js

:triangular_ruler: Javascript Geometric Algebra Generator for Javascript, c++, c#, rust, python. (with operator overloading and algebraic literals) -
MIT License
1.52k stars 108 forks source link

Questions on parallel lines and l*l motors #172

Open obliviand opened 2 months ago

obliviand commented 2 months ago

I'm still learning GA, but I've been running into confusing issues with what I thought should be simple examples with what I thought were simple lines given the examples provided with ganja.js. I'm unsure if these are issues or the way it is supposed to work.

I've tried to document two of the items below.

The PGA3D "distances and angles" example seems to give the correct positive angle because of ordering of points to make the line directions opposite at point (i.e. tip to tail like vectors). If I'm reading them correctly (and I'm probably not) the SIGGRAPH 2019 Figure 9 and PGA3D cheat sheet provide acos(l1|l2). Based on the direction arrows on the two lines in Figure 9, they are in the same direction entering the point and that it should give the smaller positive angle for two lines in the same direction including parallel.

I checked this using klein c++ and it appears to have the same result as ganja.js, and if I understand correctly that library is based off of the SIGGRAPH 2019 paper, not the PGA4CS v2.0 setup for e0.

Here are parallel lines with reversed direction result in an angle of 0:

Algebra(3,0,1,()=>{ 
  var point = (x,y,z)=>1e123-x*1e012+y*1e013+z*1e023;
  var angle_ll = (l1,l2)=>Math.acos(l1.Normalized<<l2.Normalized)*180/Math.PI; // Angle between lines 
  var A=point(1,1,0), B=point(1,-1,0), C=point(1,-1,0), D=point(1,1,0); 

  document.body.appendChild(this.graph(()=>{
    return [0x444444,A,"A",B,"B",C,"C",D,"D",
            0x884488,[A,B],[C,D],(angle_ll(A&B,C&D).toFixed(2))+"&deg;"
           ];
  },{animate:false,grid:1,labels:1,lineWidth:3})); 

});

and parallel lines in the same direction result in an angle of 180:

Algebra(3,0,1,()=>{ 
  var point = (x,y,z)=>1e123-x*1e012+y*1e013+z*1e023;
  var angle_ll = (l1,l2)=>Math.acos(l1.Normalized<<l2.Normalized)*180/Math.PI; // Angle between lines 
  var A=point(1,1,0), B=point(1,-1,0), C=point(1,1,0), D=point(1,-1,0); 

  document.body.appendChild(this.graph(()=>{
    return [0x444444,A,"A",B,"B",C,"C",D,"D",
            0x884488,[A,B],[C,D],(angle_ll(A&B,C&D).toFixed(2))+"&deg;"
           ];
  },{animate:false,grid:1,labels:1,lineWidth:3})); 
});

Is the difference because of the PGA4CS V2.0 sign change of e to -e0, or something else I'm missing?


Another example is creating lines on the axis at the origin from points and trying to move from one to the other with a motor between them:

Algebra(3,0,1,()=>{
  var line  = (...plucker)=>(plucker*[1e01,1e02,1e03,1e12,1e13,1e23]).Normalized,
      point = (x,y,z)=>!(1e0 + x*1e1 + y*1e2 + z*1e3);
  var motor = (line,angle_or_distance)=>Math.E**(angle_or_distance/2 * line);
  var sqrt = motor => ((Math.sign(motor.s) + motor)*(1 + 0.5*(motor.s + motor.Grade(4)))).Normalized;
  var lerp = (motor, x) => (Math.sign(motor().s)*(1-x) + x*motor).Normalized;

  var l1 = (point(0,0,0)&point(0,1,0)).Normalized, l2 = (point(0,0,0)&point(0,0,1)).Normalized;

  var l1tol2 = ()=>sqrt(l2 * l1);
  var l3 = l1tol2() >>> l1;

  document.body.appendChild(this.graph(()=>{
    var t = 0.5+0.5*Math.sin(performance.now()/1000);

    return [
      0x882288,lerp(l1tol2,t) >>> l1,          // move between lines
      0x228844,l1,"l1",l2,"l2",                // our two lines.
      0xff0000,l3,"l3"
    ];
  },{grid:true, labels:true,lineWidth:3, h:-0.4, p:-0.1, scale:1.4, animate:true}));
});

This doesn't work and l1 stays where it is. Looking at the values for the original l2 case where it doesn't appear to move, it appears the operation swaps the direction of l1. If l2 is given a small y offset such as l2 = (point(0,0,0)&point(0,0.00000000000000001,1)).Normalized the example works and the line goes from l1 to l2.

This example sort of works with klein without the small y offset. In the case of klein it does a rotation to the x axis instead of the z axis, so I'm guessing this case has a different issue in both libraries?

obliviand commented 2 months ago

Here's a more complete example of the second issue with l*l motors.

// Create a Clifford Algebra with 3,0,1 metric.
Algebra(3,0,1,()=>{

  const point = (x, y, z) => !(1e0 + x * 1e1 + y * 1e2 + z * 1e3);
  const motor = (line, angle_or_distance) => Math.E ** (angle_or_distance / 2 * line);
  const sqrt = motor => ((Math.sign(motor.s) + motor) * (1 + 0.5 * (motor.s + motor.Grade(4)))).Normalized;

  const SV = 0.0000000000000000000001;
  const o = point(0, 0, 0);
  let x = point(1, 0, 0);
  let y = point(0, 1, 0);
  let z = point(0, 0, 1);
  let x_sv = point(1, SV, SV);
  let y_sv = point(SV, 1, SV);
  let z_sv = point(SV, SV, 1);

  let lx = (o & x).Normalized;
  let ly = (o & y).Normalized;
  let lz = (o & z).Normalized;
  let lx_sv = (o & x_sv).Normalized;
  let ly_sv = (o & y_sv).Normalized;
  let lz_sv = (o & z_sv).Normalized;

  // broken?
  let lx_out;
  lx_out = sqrt(ly*lx) >>> lx; // swaps direction of x-axis instead of moving it to y-axis
  //lx_out = sqrt(lz*lx) >>> lx; // swaps direction of x-axis instead of moving it to z-axis
  // working?
  //lx_out = sqrt(ly*lx_sv) >>> lx; // moves x-axis to y-axis keeping positive direction
  //lx_out = sqrt(lz*lx_sv) >>> lx; // moves x-axis to z-axis keeping positive direction

  let Mx = motor(!lx_out, 1);

  // broken?
  let ly_out;
  ly_out= sqrt(lz*ly) >>> ly; // swaps direction of y-axis instead of moving it to z-axis
  //ly_out = sqrt(lx*ly) >>> ly; // swaps direction of y-axis instead of moving it to x-axis
  // working?
  //ly_out = sqrt(lz*ly_sv) >>> ly; // moves y-axis to z-axis keeping positive direction
  //ly_out = sqrt(lx*ly_sv) >>> ly; // moves y-axis to x-axis keeping positive direction

  let My = motor(!ly_out, 1);

  // broken?
  let lz_out;
  lz_out = sqrt(lx*lz) >>> lz; // swaps direction of z-axis instead of moving it to x-axis
  //lz_out = sqrt(ly*lz) >>> lz; // swaps direction of z-axis instead of moving it to y-axis
  // working?
  //lz_out = sqrt(lx*lz_sv) >>> lz; // moves z-axis to x-axis keeping positive direction
  //lz_out = sqrt(ly*lz_sv) >>> lz; // moves z-axis to y-axis keeping positive direction

  let Mz = motor(!lz_out, 1);

  // Graph it
  document.body.appendChild(this.graph(()=>{
    return [
      0xff0000, lx_out,
      0x00ff00, ly_out,
      0x0000ff, lz_out,
      0xff0000, Mx >>> o, "x dir",
      0x00ff00, My >>> o, "y dir",
      0x0000ff, Mz >>> o, "z dir"
    ];
  },{grid:true, labels:true,lineWidth:3, h:-0.4, p:-0.1, scale:1.4, animate:false}));

});

Output of the code with motors moving a point in the negative direction with zeros:

image

And then the output of the above with the first "working" selections uncommented with small values

  lx_out = sqrt(ly*lx_sv) >>> lx; // moves x-axis to y-axis keeping positive direction
  ly_out = sqrt(lz*ly_sv) >>> ly; // moves y-axis to z-axis keeping positive direction
  lz_out = sqrt(lx*lz_sv) >>> lz; // moves z-axis to x-axis keeping positive direction

image

obliviand commented 2 months ago

Edited: Added Object.is check for -0 since javascript still considers -0 as 0 for a regular compare.

Based on checking each term, the Math.sign(motor.s) in the sqrt function from the PGA3D examples is returning +/-0 when I think the first term of the motor is expecting +/-1? The small bias value prevents Math.sign from returning 0 which is why it appeared to "fix" the problem.

Changing the sqrt function to the following fixes the problems when lines and planes are through the origin:

const sqrt = motor => ((((motor.s>=0 && Object.is(motor.s,-0))?1:-1) + motor) * (1 + 0.5 * (motor.s + motor.Grade(4)))).Normalized;

Motors constructed between planes have the same orientation as the original destination plane after applying the motor to the starting plane.

Motors between lines are now drawing on the correct axis and correct orientation without the small bias value.