If arrayToQPath uses connect=all, use a different construction for QPainterPath#1796
If arrayToQPath uses connect=all, use a different construction for QPainterPath#1796j9ac9k merged 16 commits intopyqtgraph:masterfrom
Conversation
c9c6d71 to
521509b
Compare
1861685 to
aa60188
Compare
Do you guys have any benchmarks set up? If not, maybe consider https://github.com/airspeed-velocity/asv. |
|
We have asv setup, but the coverage right now is just for ImageItem.setData
for a wide variety of inputs. Incorporating it for plotcurveitem.setData
will be coming soon
…On Sun, May 23, 2021 at 12:04 rafael ***@***.***> wrote:
EDIT: on second thought, this NaN check might not be buying us anything...
doesn't look like arrayToQPath is called any more often than
PlotCurveItem.updateData... so we're just moving the calculation from one
part of the library to another...
Do you guys have any benchmarks set up? If not, maybe consider
https://github.com/airspeed-velocity/asv.
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#1796 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAE5Z7W4ETLL7QAUYHJZB4TTPFGVHANCNFSM45LHD3MQ>
.
|
|
Im currently working on trying to optimize the this here is almost as fast as the current library implementation moveTo = path.moveTo
lineTo = path.lineTo
for elementType, x, y in zip(
itertools.cycle((lineTo, moveTo)),
arr['x'],
arr['y']
):
elementType(x, y)
return pathI suspect there exists a For the use-case of |
|
@pijyoi I'm curious if you have input here. I was trying to find alternative methods for doing the The fastest I got (which is about 1-2 fps slower than current) is: # 26-27 fps
for elementType, x, y in zip(
itertools.cycle((path.moveTo, path.lineTo)),
arr[1:-1]['x'].tolist(),
arr[1:-1]['y'].tolist()
):
elementType(x, y)
return pathsome other things I tried: really had high hopes for this one... haha # 3 fps
for segment in np.split(arr[1:-1], np.argwhere(arr[1:-1]['c'] == 0).flatten()):
polygon = arrayToQPolygonF(
segment['x'],
segment['y']
)
path.addPolygon(polygon)
return path # 18 fps
_ = list(
map(
lambda x: path.lineTo(x[0], x[1]) if x[2] else path.moveTo(x[0], x[1]),
zip(arr[1:-1]['x'], arr[1:-1]['y'], arr[1:-1]['c'])
)
)
return path # 11 fps
operation = itertools.cycle((path.moveTo, path.lineTo))
list(map(lambda x: next(operation)(x[0], x[1]), arr[1:-1][['x', 'y']]))
return pathI also experimented with The one thing I couldn't get was a go of EDIT: for context I'm using |
|
I would say that reading arr = np.empty(n+2, dtype=[('c', '>i4'), ('x', '>f8'), ('y', '>f8')])Why not just: for i in range(0, n, 2):
path.moveTo(x[i], y[i])
path.lineTo(x[i+1], y[i+1])
return path |
|
oh that's a good tip; still not quite as performant as x = x.tolist()
y = y.tolist()
for i in range(0, n, 2):
path.moveTo(x[i], y[i])
path.lineTo(x[i+1], y[i+1]) |
|
I have an old patch that uses It avoids an extra conversion to bytes for PySide{2,6} because QByteArray accepts bytes or bytearray but not a generic buffer. If you find it worthwhile, you could incorporate it.
|
If that low level code can be simplified without taking a performance penalty or losing compatibility, I'd be all for merging. I think part of this PR will add a number of tests to this function as well. |
|
I applied the (fixed) patch to your PR in https://github.com/pijyoi/pyqtgraph/commits/arraypath. |
1f426b8 to
11eb207
Compare
Looks like you and I were both working on handling merge conflicts; I ended up cherry picking your commit, and embedded it earlier in the commit history of the branch, if you'd rather I adopt the commit at the head of your branch, let me know, I can do that too. I really want to get faster methods for Maybe this is a good time to do some test cases for the test suite before I try more. |
pyqtgraph/functions.py
Outdated
| """ | ||
| if not ( | ||
| x.size == y.size == x.shape[0] == y.shape[0] | ||
| and np.issubdtype(x.dtype, np.double) |
There was a problem hiding this comment.
I think there is no requirement for the source data to be float64, as it will be converted during assignment.
nbytes = 2 * size * np.finfo(dtype).dtype.itemsizecould be changed to
nbytes = 2 * size * 8 # dst buffer is float64There was a problem hiding this comment.
I'll test this right now, this would certainly simplify things even more; if I don't have to do that conversion that would be great.
pyqtgraph/functions.py
Outdated
| if eq(connect, 'all'): | ||
| arr[1:-1]['c'] = 1 | ||
| polygon = arrayToQPolygonF( | ||
| x.astype(np.double), |
There was a problem hiding this comment.
No need to convert to double if we relax the requirement in arrayToQPolygonF()
There was a problem hiding this comment.
Just tested this with a variety of other numpy dtypes, and you're right, it still works....
There was a problem hiding this comment.
I'm assuming I still need to keep this guy tho memory = np.frombuffer(buffer, np.double).reshape((-1, 2))
There was a problem hiding this comment.
Yes, that is the destination buffer, which is defined to be doubles.
8871757 to
db025de
Compare
|
You're right; probably shouldn't put our eggs in the undocumented feature basket. I should probably lobby the pyside2/qt devs to see if
either that, or give us a way to pass numpy arrays in directly (functionality they seem interested in providing, but unfortunately it probably won't be matched by PyQt6 :( ). |
pyqtgraph/functions.py
Outdated
|
|
||
| # create QDataStream object and stream into QPainterPath | ||
| path.strn = backstore | ||
| if QT_LIB == "PyQt6" and qtver < [6, 1, 1]: |
There was a problem hiding this comment.
So for the PyQt6 6.1.1-dev wheels, it's QtCore.QT_VERSION == 0x60100 and QtCore.PYQT_VERSION == 0x60101.
So version testing should be:
if QT_LIB == "PyQt6" and QtCore.PYQT_VERSION < 0x60101:We also use PYQT_VERSION here : https://github.com/pyqtgraph/pyqtgraph/blob/master/pyqtgraph/functions.py#L1406
There was a problem hiding this comment.
I have no problem with this change, but can you describe the motivation behind using the hexadecimal values instead?
There was a problem hiding this comment.
oh re-read I assume that using the hex values, it should handle differences in -dev wheels?
There was a problem hiding this comment.
PyQt version does not always match the Qt version. Sometimes PyQt fixes bugs in the bindings and releases a new version without waiting for the corresponding Qt release.
This is the case here, although only for a testing -dev wheel.
There was a problem hiding this comment.
oh 🤦🏻 yes, difference between library version and Qt version built again, ...yes, I've been bitten by that. I'll make the change shortly. I've been poking people to review #1807, I'd feel better getting the blessing from someone else before I merge my own PR of that size.
|
Depending on how much you want to split hairs, I see a few % increase by avoiding dot access on some methods, i.e. You also can compare string versions directly, i.e. You can also define ndarray_from_qpolygonf_qt6 and qt5, set it during module init, and avoid a branch statement inside the tight loop. Again, splitting hairs but what's the point of a CS minor if I can't agonize over worthless performance gains? |
Aha, I found that examples/beeswarm.py is horribly broken by the usage of undocumented behavior. err = pg.ErrorBarItem(x=np.arange(4), y=data.mean(axis=1), height=data.std(axis=1), beam=0.5, pen={'color':'w', 'width':2})This is probably related to the issue of plots being slow when using thick pens; some other drawing codepath probably gets activated in Qt. |
|
I'm afk for tonight; hopefully we don't have to abandon this optimization!
…On Wed, Jun 2, 2021 at 19:37 pijyoi ***@***.***> wrote:
Also the Qt6-only codepaths are using undocumented behaviour, so who knows,
they may need to get reverted once people really try it out.
Aha, I found that examples/beeswarm.py is *horribly* broken by the usage
of undocumented behavior.
What actually breaks it is the usage of a pen width other than 1.
err = pg.ErrorBarItem(x=np.arange(4), y=data.mean(axis=1), height=data.std(axis=1), beam=0.5, pen={'color':'w', 'width':2})
This is probably related to the issue of plots being slow when using thick
pens; some other drawing codepath probably gets activated in Qt.
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#1796 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAE5Z7T25R74QWH4BJDIGPTTQ3TGPANCNFSM45LHD3MQ>
.
|
|
Disregard, I was on the wrong branch plt = pg.plot()
plt.setWindowTitle('pyqtgraph example: ErrorBarItem')
err = pg.ErrorBarItem(x=x, y=y, top=top, bottom=bottom, beam=0.5, pen={'color':'w', 'width':2})
plt.addItem(err)
plt.plot(x, y, symbol='o', pen={'color': 0.8, 'width': 2})here is the result... |
|
err = pg.ErrorBarItem(x=np.arange(4).astype(np.float64), y=data.mean(axis=1), height=data.std(axis=1), beam=0.5, pen={'color':'w', 'width':2})Disregard, was on the wrong branch |
|
seems to be an issue limited to EDIT: appears that finite behavior is also problematic Slight modification to p4 = win.addPlot(title="Parametric, grid enabled")
x = np.cos(np.linspace(0, 2*np.pi, 1000))
x[500] = np.nan
y = np.sin(np.linspace(0, 4*np.pi, 1000))
p4.plot(x, y, connect='finite', pen={'color': (255,0,0), 'width': 2})
p4.showGrid(x=True, y=True)This took a really long time to display (like 1-2 minutes) |
|
May I suggest that the Qt6-only "finite" and "pairs" codepaths be moved out from the scope of this PR? Don't let them block the merge of the optimized "all" codepath. From what I can see, no part of the library makes use of "finite". The places that make use of "pairs" only do so with a small number of points, so speed shouldn't be an issue. |
I agree. I was hoping to make use of I'll revert/update the commits accordingly. |
|
Would it be better to remove the version check and just unconditionally do the non-finite back-filling? The case qtver = [int(x) for x in QtVersion.split('.')]
if qtver >= [5, 12, 3]: |
|
Yeah at this point this covers almost all our supported configurations, was
thinking about removing this check.
I'll remove it and put a comment for the commit that caused this change.
…On Thu, Jun 3, 2021 at 17:59 pijyoi ***@***.***> wrote:
Would it be better to remove the version check and just unconditionally do
the non-finite back-filling? The case qtver < [5, 12, 3] is no longer
being tested by the CI.
qtver = [int(x) for x in QtVersion.split('.')]
if qtver >= [5, 12, 3]:
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#1796 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAE5Z7QLUMZR4L6ZIA32RDDTRAQP5ANCNFSM45LHD3MQ>
.
|
With some experimentation, it was determined that when the QPainterPath is drawn with a QPen that has a thickness greater than 1, the end result is quite broken. Due to us trying to exploit non-advertised behavior we are removing this optimization step.
The function had many comments/explanations, decided to convert to numpy docstring style, which is the direction pyqtgraph is going in as it stands
|
I am working on an alternate QPolygonF "finite" implementation: "https://github.com/pijyoi/pyqtgraph/tree/split_finite Will wait for #1807 and #1796 to be merged first. As I understand it, there are 2 use-cases for "finite":
|
|
@pijyoi your use case of pyqtgraph is something I've been really curious about. At some point I'd love to hear more about how yo use the library in your own work. Those PRs will be merged shortly (end of the day today, this time for sure 😆 ) ... also I think we should do another release with removing Qt 6.0 support (and officially supporting 6.1+) before much longer... I was wanting to do a 0.13.0 release, but we have quite a few deprecation warnings that we say we will remove in 0.13 version of the library... so maybe 0.12.2 ... or maybe I should move this topic to the discussions section. |
|
@pijyoi thank you so much for your help with this @PierreRaybaut thanks for showing how to construct a QPolygonF object straight from a numpy array. If you ever look to bring Qt6 functionality to PythonQwt, feel free to see how we implemented it. Merging this PR, yay for faster line plots! |



It's been discussed that the QDataStream >> QPainterPath operation is a real bottle-neck in arrayToQPath. Here is a stack overflow post that points out that
QDataStream >> QPainterPathoperation is really slow.Taking a peak at what PythonQwt does, that project constructs an open QPolygonF and draw that. QPainterPath does have an
addPolygonmethod, so we can generate a QPainterPath that is made up of a QPolygonF.Doing some testing on
examples/MultiPlotSpeedTest.pyshows a significant performance boost when doing this. This PR shows 35 fps while master branch is at 25 fps.This PR also incorporates a second optimization, it checks for
NaNvalues onPlotCurveItem.updateData()instead of at arrayToQPath check, and allows for an optimization parameter to be passed to skip the NaN check which does cause a performance hit.So first thing, the function is largely lifted from PythonQwt, so huge thanks to that project for figuring out this optimization.
Some caveats, this does not work w/ Qt6; so definitely not ready for merging.Works with Qt6 now.EDIT: on second thought, this
NaNcheck might not be buying us anything... doesn't look likearrayToQPathis called any more often thanPlotCurveItem.updateData... so we're just moving the calculation from one part of the library to another...