Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3D streamtube traces #2658

Merged
merged 41 commits into from
Jul 5, 2018
Merged

3D streamtube traces #2658

merged 41 commits into from
Jul 5, 2018

Conversation

etpinard
Copy link
Contributor

@etpinard etpinard commented May 23, 2018

Note that this branch is @kig 's https://github.com/plotly/plotly.js/tree/gl-vis-streamtube rebased with the 2018/05/28 master.


Streamtubes are coming to plotly.js, this time for real (#701 😏 )

peek 2018-05-23 17-15

But first, we'll need to bring this branch up-to-speed with the work done for cones in #2641 ⚒️


Here's a list of TODOs:

  • get a few fixes gl-vis/gl-streamtube3d#1 merged
  • figure out how to split stream tubes from stream lines:
  • figure out how hover should work? Just like cones with gl-scatter3d points at the vector fields' (x,y,z) coordinates or something that would suit streamtubes better
  • figure out if we expose or not "meshgrid" attributes?
    • meshgrid should be more useful for tubes compared to cones
    • MATLAB and the current gl-streamtube3d iteration allows users to define starting positions, would that be enough for the v1?
      812be20#r29119106
    • we should think about an API allow user to "fill the scene with tubes" even in scenarios where several streamlines converge and end.
  • properly scaffold streamtubes as in fa32a74
  • hook autorange logic
    • need to consider startx/starty/startz as well as vector field x/y/z,
    • need to consider tubeScale
  • Implement auto "widthScale" algo (see 812be20#r29119106)
  • plotly.js-ize attributes
    • no camelCase
    • use colorscale, cmin and cmax as for cone traces
    • bounds isn't a per-trace setting in plotly.js
    • find out what maxLength does
    • find out what widthScale does (would using sizeref in plotly.js like for cones be ok?)
    • fill in attribute descriptions
  • properly hook in colorbar (we might be able to simply reuse cone/calc.js and cone/colorbar.js here)
  • add a few image mocks
  • add jasmine tests similar to the cone suite
  • add react test as in 07fc2a0
  • get syntax and plotschema tests to pass
  • publish gl-streamtube3d to npm and update package.json dep
  • open "open item" issue

cc @alexcjohnson @jackparmer @kig

@etpinard etpinard added this to the v1.39.0 milestone May 23, 2018
- reuse cone colorbar
- compute x/y/z bounds in calc
- pass cmin/cmax to meshData.vertexIntensityBounds
- rename starting position attributes
  cx/cy/cz -> startx/starty/startz
- misc cleanup to bring streamtube closer to cone
- add new TODOs
@etpinard
Copy link
Contributor Author

Update:

Here's a few problems I noticed yesterday:

gl-streamtube3d exposes a meshgrid attribute that "samples" (i.e. downscales) the vector field about the cartesian product of arrays of x,y,z coordinates. But now, @kig how can users specify the vector field's own x,y,z coordinates (like what the positions setting does for gl-cone3d)?

From 812be20#r29129877, gl-streambed3d's widthScale setting has a hard-coded default that does a very poor job in some cases. 🆘 We must fix this before the v1 release. 🆘 I think @kig will be taking this one.

On-par with cone traces, the colorscale's cmin and cmax auto value are computed using the u/v/w max vector norm. For cones, this turns out to be exactly the span of color values displayed, but for streamtubes cmin-cmax overestimate the span of color values displayed, as the tubes aren't drawn at the vector field coordinates. So, should we compute the max vector norm at the tubes' location to determine cmin and cmax? I can see an argument for both methods. Computing cmin/cmax just from u/v/w is probably more "true to the data" whereas computing cmin/cmax from the vector field interpolated to the tubes' locations will give better aesthetically-pleasing results.

As for hover, it's an interesting problem. I'm thinking of three families of solution. 1) Just show the vector field x/y/z and u/v/w pt values just like for cones. This solution is true to the input data, but doesn't offer info about the tubes. 2) Just show the coordinates of the tubes' vertices and the u/v/w components at their location. Here, we're entirely in "tube-space", the input x/y/z and u/v/w don't show up on hover. 3) Show the tubes' starting position (i.e. items from startx, starty and startz) and the tubes' center coordinates plus (optionally) the scalar velocity at the tubes' center. When hovering over a section of a tube, it seems useful to me to know where that tracer started from.

Thoughts?

@alexcjohnson
Copy link
Collaborator

Computing cmin/cmax just from u/v/w is probably more "true to the data" whereas computing cmin/cmax from the vector field interpolated to the tubes' locations will give better aesthetically-pleasing results.

"True to the data" is my preference here - it shows you a bit of what you've missed about the data by converting to tubes. It shouldn't often be very far off the interpolated values, right?

As for hover, it's an interesting problem.

Interesting indeed - I actually think "true to the data" would be really confusing here, as there's nothing to "hover on" at the data point. Particularly since this is 3D, you could end up hovering on something at a totally different depth from the tube you're inspecting. The tube vertices (2) don't mean anything. I assume when you say this you're talking about the surface of the tube? I'm thinking the ideal solution (tell me how painful this would be to calculate...) would be to find the exact tube surface point the cursor is over (not the closest vertex) and project from there back to the centerline of the tube; show x/y/z and u/v/w/norm for that point (or perhaps the closest actual data point? that could be confusing though); and perhaps somehow highlight the whole tube, so you can see not just where it came from but the whole streamline? That last part seems more like a "nice to have" that we could add later though.

@etpinard
Copy link
Contributor Author

"True to the data" is my preference here

Fantastic 👌

I assume when you say this you're talking about the surface of the tube?

Yes, exactly.

I'm thinking the ideal solution (tell me how painful this would be to calculate...) would be to find the exact tube surface point the cursor is over (not the closest vertex) and project from there back to the centerline of the tube; show x/y/z and u/v/w/norm for that point

I agree, this sounds like ideal. I'll have to dig into how the tubes coordinates are computed to answer the "how painful this would be" question. Maybe @kig would be interested in helping us out?

@kig
Copy link
Contributor

kig commented May 29, 2018

gl-streamtube3d exposes a meshgrid attribute that "samples" (i.e. downscales) the vector field about the cartesian product of arrays of x,y,z coordinates. But now, @kig how can users specify the vector field's own x,y,z coordinates (like what the positions setting does for gl-cone3d)?

The streamtube visualization needs a continuous vector field. It works by sampling the vector field at each of the starting points, then moving a small amount along the sampled vector, sampling again, moving again, repeat until far enough from the starting point to create a stream segment. Push stream segment to the stream points array, continue until out of bounds or reach maxLength segments in stream points array.

The meshgrid parameter defines the vector field for the streamtubes. It maps each of the vectors to a position in space. And since it's a nicely defined grid, it's easy to sample values inside each grid cell by interpolating between the 8 corners of the cell according to the sample position.

Doing arbitrary vector positions for the vector field would be doable if you've got some nice algorithm to sample them as a continuous vector field.

Re: widthScale, auto scale, I wrote an auto scaler this morning. gl-vis/gl-streamtube3d@97c89d7 -- it also removes the widthScale parameter and replaces it with tubeSize and absoluteTubeSize params.

The auto scaler keeps track of the maximum divergence value encountered and uses that to calculate the radius of the tubes. The tubeSize param controls the auto scaled width so that tubeSize of 1 would scale two max divergence tubes at adjacent starting positions so that they don't overlap.

The absoluteTubeSize param overrides the auto scaler, and is used to multiply the divergence value to get the tube radius in coordinate space units.

The divergence used for the streamtubes is || divF || instead of the divF 3-vector. I don't know if that's the right thing to do or if we should scale the tubes in a non-uniform fashion according to the 3D divergence.

I'm thinking the ideal solution (tell me how painful this would be to calculate...) would be to find the exact tube surface point the cursor is over (not the closest vertex) and project from there back to the centerline of the tube; show x/y/z and u/v/w/norm for that point

This sounds doable, the tubes definition contains all the sampled interpolated data points, and picking should get you the triangle & data point matching that. What would you need for this?

@etpinard
Copy link
Contributor Author

Re: widthScale, auto scale, I wrote an auto scaler this morning. gl-vis/gl-streamtube3d@97c89d7 -- it also removes the widthScale parameter and replaces it with tubeSize and absoluteTubeSize params.

Thanks @kig ! I'll try to incorporate your work in this PR in the next few hours.

@etpinard
Copy link
Contributor Author

etpinard commented May 29, 2018

The streamtube visualization needs a continuous vector field.
The meshgrid parameter defines the vector field for the streamtubes. It maps each of the vectors to a position in space. And since it's a nicely defined grid, it's easy to sample values inside each grid cell by interpolating between the 8 corners of the cell according to the sample position.

Thanks for writing this down @kig . You're absolutely right, the "mesh" requirements for streamtubes are much more stringent than for cones. Too bad x/y/z will have a slightly different meaning in streamtube vs in cone traces, but I think we can live with that.

Having x/y/z generate of "mesh" of x.length * y.length * z.length items using a cartesian product and requiring u/v/w to be of length x.length * y.length * z.length isn't the most plotly.js-esque API unfortunately. I guess we could make u/v/w 3D arrays. This would be a first though, and I doubt that's better. Perhaps requiring x/y/z to list all coordinates (so that x/y/z and u/v/w would have matching lengths) would be best albeit (way) less efficient. For example,

var x = [], y = [], z = [];
var u = [], v = [], w = [];

for(var i = 0; i < nx; i++) {
  for(var j = 0; j < ny; j++) {
    for(var k = 0; k < nz; k++) {
      x.push(i); y.push(j); z.push(k);

      u.push(1+Math.sin(i));
      v.push(Math.cos(j));
      w.push(Math.sin(k*0.3)*0.3);
    }
  }
}

@alexcjohnson
Copy link
Collaborator

Perhaps requiring x/y/z to list all coordinates (so that x/y/z and u/v/w would have matching lengths) would be best albeit (way) less efficient.

It's not way less efficient - x/y/z are way bigger, but the trace as a whole is only twice as big. I think we should require this form (x/y/z/u/v/w all the same length) as the default. We can extend this with some non-default option that lets users specify the more efficient x/y/z (either with 3D or 1D u/v/w arrays, we can discuss that later), but then we can also handle arbitrary sample points in the future, either by pre-interpolating like in convert_column_xyz and interp2d or some Delaunay-like algorithm. And it'll ensure users can switch between streamtube and cone relatively robustly. Then we could fill in all the corresponding gaps in cone data handling as well.

@etpinard
Copy link
Contributor Author

etpinard commented May 30, 2018

And it'll ensure users can switch between streamtube and cone relatively robustly. Then we could fill in all the corresponding gaps in cone data handling as well.

This is winning argument 🏆 . @kig would you be interested in converting the gl-streambed3d meshgrid argument to x/y/z positions?

@kig
Copy link
Contributor

kig commented May 31, 2018

By converting the meshgrid argument to x/y/z positions, do you mean that it'd be like an "expanded" meshgrid? You'd have the same x coordinates for every y and the same y coords for every z, and they'd be sorted small to big?

For example,

var x = [0,1,2,0,1,2,0,1,2,0,1,2],
    y = [4,4,4,5,5,5,4,4,4,5,5,5],
    z = [6,6,6,6,6,6,7,7,7,7,7,7];

var uniq = function(arr, e) {
  if (arr.length === 0 || e > arr[arr.length-1]) {
    arr.push(e);
  }
  return arr; 
};

var meshgrid = [x.reduce(uniq, []), y.reduce(uniq, []), z.reduce(uniq, [])];
=> [ [0,1,2], [4,5], [6,7] ]

@kig
Copy link
Contributor

kig commented May 31, 2018

Btw the cone meshgrid argument works in a similar fashion. It interprets the vector array as a vector field, and then uses the cone positions as points to sample from the vector field.

- which makes x/y/z and u/v/w streamtube data interchangeable
  with cone traces
- convert x/y/z columns to gl-streamtube "meshgrid" using
  Lib.distinctVals
- add pad around tube x/y/z bounds to allow them to go a little
  bit beyond the mesh w/o getting clipped.
@etpinard
Copy link
Contributor Author

etpinard commented Jun 1, 2018

it'd be like an "expanded" meshgrid? You'd have the same x coordinates for every y and the same y coords for every z, and they'd be sorted small to big?

Yes, exactly. Commit e99ffc9 adds a x/y/z columns -> meshgrid vector converter in plotly.js before calling tube2mesh.


It interprets the vector array as a vector field, and then uses the cone positions as points to sample from the vector field.

We chose to not expose gl-cone3d's "meshgrid" setting to plotly.js users in 5a42de0. In this context, (if I understand correctly), gl-cone3d assumes that the x/y/z positions and u/v/w vectors input match. So now, plotly users using the same x/y/z and u/v/w columns can plot streamtube and cone traces with no array additional array manipulations.

@etpinard
Copy link
Contributor Author

etpinard commented Jun 5, 2018

I also notice that when multiple tubes come together and overlap we don't do anything to stop this, for example this set of 3 near the one I flag ^^ that spend most of their length conjoined. Is that a normal way of handling things? Not what I would have expected comparing these to 2D streamline plots, for example, where a line usually disappears when it gets too close to another.

cc @kig

or goes on forever?

gl-streamtube3d has a maxLength (mapped to maxdisplayed in plotly.js) which sets the maximum number of segments per tube. The default is 1000 segments. So don't worry about tubes that go on forever.

} else {
// use all pts on 2x2 and 1x1 planes
sx = valsx.slice();
sz = valsz.slice();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably this length > 2 condition should be done separately for x and z? Also, I don't know what model is used for extending the field outside the x/y/z bounds, but if it's uncertain enough that at 3+ points it's best not to start at the last coordinate, then I'd think at 2 points it'd be best to use a single point halfway between the two.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I don't know what model is used for extending the field outside the x/y/z bounds

Currently, positions outside the mesh get (u=0, v=0, w=0) if I understand correctly:

https://github.com/gl-vis/gl-streamtube3d/blob/0359ed9696de5af570fa6a5e36bdfbca2013220b/streamtube.js#L228-L231

cc @kig

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New attempt in 83f3bdc

// Does not correspond to input x/y/z, so delete them
delete out.x;
delete out.y;
delete out.z;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍰

@kig
Copy link
Contributor

kig commented Jun 7, 2018

I also notice that when multiple tubes come together and overlap we don't do anything to stop this, for example this set of 3 near the one I flag ^^ that spend most of their length conjoined. Is that a normal way of handling things? Not what I would have expected comparing these to 2D streamline plots, for example, where a line usually disappears when it gets too close to another.

This could be done in a post processing pass, checking if any two points have a distance below an epsilon -> throw away the shorter stream suffix. As it is, the stream generator just generates a stream from each starting point and draws them all out.

@etpinard
Copy link
Contributor Author

@alexcjohnson are any of your remarks in #2658 (comment) blocking for a v1 release?

@kig would you be interested in looking at the broken tube @alexcjohnson noticed in gl3d_streamtube-first ?

extendFlat(attrs, colorAttrs('', 'calc', true), {
showscale: colorscaleAttrs.showscale,
extendFlat(attrs, colorscaleAttrs('', {
colorAttr: 'u/v/w norm',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This give better attribute descriptions for cone colorscale and friends. For example, for cmin we have

"Sets the lower bound of the color domain. Value should have the same units as the u/v/w norm and if set, cmax must be set as well."

@alexcjohnson
Copy link
Collaborator

are any of your remarks in #2658 (comment) blocking for a v1 release?

Just the broken tube - if there's a benign explanation for it then fine, I'd just like us to make sure we're not setting ourselves up for breaks in some broad class of conditions we haven't fully explored yet.

@etpinard etpinard mentioned this pull request Jun 26, 2018
5 tasks
@etpinard
Copy link
Contributor Author

More on the broken tube of #2658 (comment),

At sizeref: 0.2 we have:

peek 2018-06-26 16-20

which looks like one (very) narrow but not broken section.

the same view at sizeref: 0.1 is:

peek 2018-06-26 16-22

where many tubes are overlapping.

@kig
Copy link
Contributor

kig commented Jun 30, 2018

Right, two ideas of what might be causing this.

The coordinate system generated based on the velocity vector is not continuous, so when a stream passes through a discontinuous angle, the coordinate axes suddenly change, which would cause a >< kink in the tube.

Alternative explanation: the divergence function returns a zero for some reason.

@alexcjohnson
Copy link
Collaborator

Looking at that point again - I can confirm that it's not broken, it does all seem to connect ala pinched sausage links. But we're off the edge of the data here, the largest x in the data is 4 and we're almost at x=5 here. So I'm not sure what extrapolation is going on but I can't imagine it would take what looks like a fairly large and stable divergence and take it through zero before coming back out to essentially the same magnitude at the next point. Is there any chance it's taking two neighboring rings and rather than connecting them directly, it's connecting them inverted? Meaning it's making a mesh that connects points at the top of the tube on the first ring to points at the bottom of the next ring, and so on? I feel like I see some tight folding that makes it look like a wrapped present, which seems like it could happen with an inversion scenario like that:
screen shot 2018-07-03 at 10 01 06 pm

@etpinard etpinard mentioned this pull request Jul 5, 2018
4 tasks
@etpinard
Copy link
Contributor Author

etpinard commented Jul 5, 2018

Ok merging this thing.

Go to #2781 for a list of open items.

@etpinard etpinard merged commit 9698e78 into master Jul 5, 2018
@etpinard etpinard deleted the streamtube-traces branch July 5, 2018 14:07
@kig
Copy link
Contributor

kig commented Aug 2, 2018

Is there any chance it's taking two neighboring rings and rather than connecting them directly, it's connecting them inverted?

Yeah, that's probably what happens. At some angles, the coordinate system generated from only the velocity vector suddenly swaps its axes. I can fix it by also sending the previous velocity vector, then the shader could compare the coordinate systems to avoid sudden swaps. Or do it in JS and pass up vectors to the shader.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature something new
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants