Repository: pyvista/pyvista

Issue:
streamlines using highly nonuniform vector field
**Updates**

After I set the `max_time` to `10`, which is the default `Maximum Streamline Length` in Paraview, I can reproduce the Paraview results.

The default value `max_time=None` might not play well with certain data. In my case, the magnitude of the vector field varies greatly. Then, the default length figured out by the following [code](https://github.com/pyvista/pyvista/blob/ef205f48454bb5bef09b2d00e38c9b49ec31eb50/pyvista/core/filters/data_set.py#L3117)  does not work well:

```python
if max_time is None:
    max_velocity = self.get_data_range()[-1]
    max_time = 4.0 * self.GetLength() / max_velocity
```

I'm about to close this issue. But still,  the usage of the parameter `MaximumPropagation` might be improved, like I discussed in my own reply [below](https://github.com/pyvista/pyvista/issues/2301#issuecomment-1060201545).

------------------
**Describe the bug, what's wrong, and what you expect:**

`streamlines` using highly nonuniform vector field do not reproduce Paraview results.

I guess it could just be a parameter issue, but I haven't found the cause.

-----

**To Reproduce**

Please include a code snippet to reproduce the bug in the block below:

Input data (sorry for its nontrivial size ~ 10 MB; please remove the .zip extension which was added to be recognized by github issue system):
[bfield.vti.zip](https://github.com/pyvista/pyvista/files/8193436/bfield.vti.zip)

```py
import pyvista

bfield = pyvista.read('bfield.vti')

bfieldlines = bfield.streamlines(
    pointa=(-23, 0, 0),
    pointb=(-13, 0, 0),
    n_points=100,
    step_unit='cl',
    initial_step_length=0.2,
    min_step_length=0.01,
    max_step_length=0.5,
    max_steps=2000, # tried 20000, no difference
    terminal_speed=1e-12,
    max_error=1e-06,
    max_time=None,
)
plotter = pyvista.Plotter()
plotter.add_mesh(bfieldlines.tube(radius=0.01), color='r')
plotter.add_mesh(pyvista.Sphere(center=(-18,0,0), radius=0.3)) # just for reference
plotter.add_axes()
plotter.camera_position = 'xz'
plotter.show()
```

**Screenshots**
Pyvista results using `streamline` parameters taken from Paraview test:
![demo-pyvista](https://user-images.githubusercontent.com/1000547/156940368-0f3d1622-8a35-4db0-8cd9-5e9792715ac8.png)

Paraview results:
![demo-paraview](https://user-images.githubusercontent.com/1000547/156940329-b2b8e017-e8c7-4e74-8f9e-ab1511fa7270.png)



-----

**System Information:**
 Please run the following code wherever you are experiencing the bug and paste the output below. This report helps us track down bugs and it is critical to addressing your bug:

```py
# Get system info
import pyvista as pv
print(pv.Report())
```
```txt

--------------------------------------------------------------------------------
  Date: Sun Mar 06 15:19:38 2022 EST

                OS : Linux
            CPU(s) : 4
           Machine : x86_64
      Architecture : 64bit
               RAM : 15.5 GiB
       Environment : Jupyter
       File system : ext4
        GPU Vendor : Intel
      GPU Renderer : Mesa Intel(R) HD Graphics 4600 (HSW GT2)
       GPU Version : 4.6 (Core Profile) Mesa 21.3.6

  Python 3.8.12 (default, Oct 12 2021, 13:49:34)  [GCC 7.5.0]

           pyvista : 0.34.dev0
               vtk : 9.0.3
             numpy : 1.21.2
           imageio : 2.9.0
           appdirs : 1.4.4
            scooby : 0.5.11
        matplotlib : 3.5.1
             PyQt5 : 5.9.2
           IPython : 7.31.1
          colorcet : 3.0.0
           cmocean : 2.0
        ipyvtklink : 0.2.2
             scipy : 1.8.0
        itkwidgets : 0.32.1
              tqdm : 4.62.3
            meshio : 5.3.2

  Intel(R) oneAPI Math Kernel Library Version 2021.4-Product Build 20210904
  for Intel(R) 64 architecture applications

```


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pyvista/pyvista/pull/6703?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 91.62%. Comparing base [(`4b905fd`)](https://app.codecov.io/gh/pyvista/pyvista/commit/4b905fd9899b0b5113bd5f00b5f1c0377f048a3d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) to head [(`20992c0`)](https://app.codecov.io/gh/pyvista/pyvista/commit/20992c0ec31f86393a65fda7b477dd593ee7cc6a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista).

> :exclamation:  There is a different number of reports uploaded between BASE (4b905fd) and HEAD (20992c0). Click for more details.
> 
> <details><summary>HEAD has 34 uploads less than BASE</summary>
>
>| Flag | BASE (4b905fd) | HEAD (20992c0) |
>|------|------|------|
>||35|1|
></details>

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #6703      +/-   ##
==========================================
- Coverage   97.43%   91.62%   -5.81%     
==========================================
  Files         143      143              
  Lines       27988    27991       +3     
==========================================
- Hits        27271    25648    -1623     
- Misses        717     2343    +1626     
```

</details>
MatthewFlamm: I have chosen to put `max_length` after `progress_bar` for minimizing other breaking changes, but I'm not sure if we have a standard for this.
MatthewFlamm: cc @akaszynski since we chatted briefly about this in the linked discussion.
pyvista-bot: <!-- NETLIFY DEPLOY COMMENT GENERATED BY ACTIONS_NETLIFY - APP ID SHA256: d3574f413fe51079937885070831e027ca5b10f7facb9e609494e0a481a31f79 -->
🚀 Deployed on https://670eaee4bcd6c423e0b6694b--pyvista-dev.netlify.app

Patch:
diff --git a/examples/01-filter/streamlines.py b/examples/01-filter/streamlines.py
index 7f684d8c..062d45ca 100644
--- a/examples/01-filter/streamlines.py
+++ b/examples/01-filter/streamlines.py
@@ -33,7 +33,7 @@ mesh = examples.download_carotid()
 
 streamlines, src = mesh.streamlines(
     return_source=True,
-    max_time=100.0,
+    max_length=100.0,
     initial_step_length=2.0,
     terminal_speed=0.1,
     n_points=25,
@@ -123,7 +123,7 @@ mesh = examples.download_kitchen()
 kitchen = examples.download_kitchen(split=True)
 
 # %%
-streamlines = mesh.streamlines(n_points=40, source_center=(0.08, 3, 0.71))
+streamlines = mesh.streamlines(n_points=40, source_center=(0.08, 3, 0.71), max_length=200)
 
 # %%
 p = pv.Plotter()
diff --git a/examples/01-filter/streamlines_2D.py b/examples/01-filter/streamlines_2D.py
index 5c33b97a..47b4c3f8 100644
--- a/examples/01-filter/streamlines_2D.py
+++ b/examples/01-filter/streamlines_2D.py
@@ -35,7 +35,7 @@ print(fluid_mesh)
 
 one_streamline = fluid_mesh.streamlines(
     start_position=(0.0, 0.4, 0.0),
-    max_time=100.0,
+    max_length=100.0,
     compute_vorticity=False,  # vorticity already exists in dataset
 )
 
@@ -57,7 +57,7 @@ line_streamlines = fluid_mesh.streamlines(
     pointa=(0, -5, 0),
     pointb=(0, 5, 0),
     n_points=25,
-    max_time=100.0,
+    max_length=100.0,
     compute_vorticity=False,  # vorticity already exists in dataset
 )
 
diff --git a/examples/99-advanced/magnetic-fields.py b/examples/99-advanced/magnetic-fields.py
index 2f8e2c1c..92812092 100644
--- a/examples/99-advanced/magnetic-fields.py
+++ b/examples/99-advanced/magnetic-fields.py
@@ -60,7 +60,7 @@ seed = pv.Disc(inner=1, outer=5.4, r_res=2, c_res=12)
 strl = grid.streamlines_from_source(
     seed,
     vectors='B',
-    max_time=180,
+    max_length=180,
     initial_step_length=0.1,
     integration_direction='both',
 )
diff --git a/examples/99-advanced/openfoam-cooling.py b/examples/99-advanced/openfoam-cooling.py
index c6ed7681..d27f9895 100644
--- a/examples/99-advanced/openfoam-cooling.py
+++ b/examples/99-advanced/openfoam-cooling.py
@@ -124,7 +124,7 @@ points = []
 for x in np.linspace(0.045, 0.105, 7, endpoint=True):
     points.extend([x, 0.2, z] for z in np.linspace(0, 0.03, 5))
 points = pv.PointSet(points)
-lines = air.streamlines_from_source(points, max_time=2.0)
+lines = air.streamlines_from_source(points, max_length=2.0)
 
 # Plot
 pl = pv.Plotter()
diff --git a/examples/99-advanced/openfoam-tubes.py b/examples/99-advanced/openfoam-tubes.py
index 780b3fb2..6c6bbc7d 100644
--- a/examples/99-advanced/openfoam-tubes.py
+++ b/examples/99-advanced/openfoam-tubes.py
@@ -71,7 +71,7 @@ pset = pv.PointSet(inlet.points[::5])
 lines = air.streamlines_from_source(
     pset,
     vectors='U',
-    max_time=1.0,
+    max_length=1.0,
 )
 
 pl = pv.Plotter()
diff --git a/pyvista/core/filters/data_set.py b/pyvista/core/filters/data_set.py
index d2c14802..43ab8bda 100644
--- a/pyvista/core/filters/data_set.py
+++ b/pyvista/core/filters/data_set.py
@@ -4244,6 +4244,7 @@ class DataSetFilters:
         rotation_scale=1.0,
         interpolator_type='point',
         progress_bar=False,
+        max_length=None,
     ):
         """Generate streamlines of vectors from the points of a source mesh.
 
@@ -4305,7 +4306,11 @@ class DataSetFilters:
             Maximum error tolerated throughout streamline integration.
 
         max_time : float, optional
-            Specify the maximum length of a streamline expressed in LENGTH_UNIT.
+            Specify the maximum length of a streamline expressed in physical length.
+
+            .. deprecated:: 0.45.0
+               ``max_time`` parameter is deprecated. Use ``max_length`` instead.
+                It will be removed in v0.48. Default for ``max_time`` changed in v0.45.0.
 
         compute_vorticity : bool, default: True
             Vorticity computation at streamline points. Necessary for generating
@@ -4325,6 +4330,11 @@ class DataSetFilters:
         progress_bar : bool, default: False
             Display a progress bar to indicate progress.
 
+        max_length : float, optional
+            Specify the maximum length of a streamline expressed in physical length.
+            Default is 4 times the diagonal length of the bounding box of the ``source``
+            dataset.
+
         Returns
         -------
         pyvista.PolyData
@@ -4361,9 +4371,22 @@ class DataSetFilters:
         elif vectors is None:
             pyvista.set_default_active_vectors(self)
 
-        if max_time is None:
-            max_velocity = self.get_data_range()[-1]
-            max_time = 4.0 * self.GetLength() / max_velocity
+        if max_time is not None:
+            if max_length is not None:
+                warnings.warn(
+                    '``max_length`` and ``max_time`` provided. Ignoring deprecated ``max_time``.',
+                    PyVistaDeprecationWarning,
+                )
+            else:
+                warnings.warn(
+                    '``max_time`` parameter is deprecated.  It will be removed in v0.48',
+                    PyVistaDeprecationWarning,
+                )
+                max_length = max_time
+
+        if max_length is None:
+            max_length = 4.0 * self.GetLength()
+
         if not isinstance(source, pyvista.DataSet):
             raise TypeError('source must be a pyvista.DataSet')
 
@@ -4385,7 +4408,7 @@ class DataSetFilters:
         alg.SetMaximumError(max_error)
         alg.SetMaximumIntegrationStep(max_step_length)
         alg.SetMaximumNumberOfSteps(max_steps)
-        alg.SetMaximumPropagation(max_time)
+        alg.SetMaximumPropagation(max_length)
         alg.SetMinimumIntegrationStep(min_step_length)
         alg.SetRotationScale(rotation_scale)
         alg.SetSurfaceStreamlines(surface_streamlines)
diff --git a/pyvista/examples/downloads.py b/pyvista/examples/downloads.py
index 23218ed7..2435af70 100644
--- a/pyvista/examples/downloads.py
+++ b/pyvista/examples/downloads.py
@@ -3536,7 +3536,8 @@ def download_kitchen(split=False, load=True):  # pragma: no cover
     >>> point_a = (0.08, 2.50, 0.71)
     >>> point_b = (0.08, 4.50, 0.71)
     >>> line = pv.Line(point_a, point_b, resolution=39)
-    >>> dataset.streamlines_from_source(line).plot(show_grid=True)
+    >>> streamlines = dataset.streamlines_from_source(line, max_length=200)
+    >>> streamlines.plot(show_grid=True)
 
     .. seealso::
 
@@ -7265,7 +7266,7 @@ def download_coil_magnetic_field(load=True):  # pragma: no cover
     >>> strl = grid.streamlines_from_source(
     ...     seed,
     ...     vectors='B',
-    ...     max_time=180,
+    ...     max_length=180,
     ...     initial_step_length=0.1,
     ...     integration_direction='both',
     ... )


Test patch:
diff --git a/tests/core/test_dataset_filters.py b/tests/core/test_dataset_filters.py
index a5b18693..f851de8e 100644
--- a/tests/core/test_dataset_filters.py
+++ b/tests/core/test_dataset_filters.py
@@ -19,6 +19,7 @@ from pyvista.core import _vtk_core
 from pyvista.core.celltype import CellType
 from pyvista.core.errors import MissingDataError
 from pyvista.core.errors import NotAllTrianglesError
+from pyvista.core.errors import PyVistaDeprecationWarning
 from pyvista.core.errors import VTKVersionError
 from pyvista.core.filters.data_set import _swap_axes
 
@@ -1683,6 +1684,72 @@ def test_streamlines_start_position(uniform_vec):
     assert all([stream.n_points, stream.n_cells])
 
 
+def test_streamlines_max_length():
+    # mesh that is 50x50x50 length units in size
+    mesh = pv.ImageData(dimensions=(6, 6, 6), spacing=(10, 10, 10))
+
+    vel = np.zeros((mesh.n_points, 3))
+    vel[:, 0] = 1
+    mesh['vel'] = vel
+
+    for step_unit in ('cl', 'l'):
+        # First show that the default integrates to near the end of the domain.
+        # Use max_step_length to limit error near end of domain.
+        stream = mesh.streamlines(
+            vectors='vel',
+            start_position=(0, 0, 0),
+            integration_direction='forward',
+            step_unit=step_unit,
+            max_step_length=0.1,
+        )
+        # It doesn't identify edge of domain, and just stops at last point inside domain.
+        # Check that is ended reasonably close to edge of domain.
+        assert stream.length > 45
+
+        # Next show that the max_length is satisfied
+        stream = mesh.streamlines(
+            vectors='vel',
+            start_position=(0, 0, 0),
+            integration_direction='forward',
+            max_length=1,
+            step_unit=step_unit,
+        )
+        assert np.isclose(stream.length, 1)
+
+    def check_deprecation():
+        if pv._version.version_info >= (0, 48):
+            raise RuntimeError(
+                'Convert error ``max_time`` parameter in ``streamlines_from_source``'
+            )
+        if pv._version.version_info >= (0, 49):
+            raise RuntimeError('Remove ``max_time`` parameter in ``streamlines_from_source``')
+
+    with pytest.warns(PyVistaDeprecationWarning, match='``max_time`` parameter is deprecated'):
+        stream = mesh.streamlines(
+            vectors='vel',
+            start_position=(0, 0, 0),
+            integration_direction='forward',
+            max_time=1,
+            max_step_length=0.1,
+        )
+        check_deprecation()
+    assert np.isclose(stream.length, 1)
+
+    with pytest.warns(
+        PyVistaDeprecationWarning,
+        match='``max_length`` and ``max_time`` provided. Ignoring deprecated ``max_time``.',
+    ):
+        stream = mesh.streamlines(
+            vectors='vel',
+            start_position=(0, 0, 0),
+            integration_direction='forward',
+            max_time=5,
+            max_length=1,
+        )
+        check_deprecation()
+    assert np.isclose(stream.length, 1)
+
+
 def test_streamlines_errors(uniform_vec):
     with pytest.raises(ValueError):  # noqa: PT011
         uniform_vec.streamlines('vectors', integration_direction='not valid')