evereux / pycatia

python module for CATIA V5 automation
MIT License
196 stars 53 forks source link

[QUESTION] pycatia perfo vs VBA #205

Open Djey51 opened 6 months ago

Djey51 commented 6 months ago

Hello,

Is there some optimization to follow to obtain better performances with pycatia?

When I use the same macro developed in python with pycatia than one developed in VBA, I can see that the pycatia one is slower (next to 2x) than the VBA one.

Do you have some tips and tricks for me please?

Thanks

evereux commented 6 months ago

Some more information / context?

I don't understand why there would be any difference as they're both accessing the COM object.

Djey51 commented 6 months ago

Same for me. But it is just a constatation. I'm the only guy who give you tgis feedback? Did you already perform performance tests on both languages?

deloarts commented 6 months ago

I noticed the same, especially when dealing with many many user ref props. It's on my schedule to investigate this problem, but what I've found out so far isn't consistent, so I think I'm on a goose chase for now.

An interesting fact tho: I didn't have any perfomance issues when I used a very stripped version of pycatia. But I didn't test it under the same premisses as I do now.

evereux commented 6 months ago

I'm the only guy who give you tgis feedback?

Yes.

Did you already perform performance tests on both languages?

Nope. Believe it or not I rarely do any CATIA scripting, pycatia or VBA.

I've been thinking this over the past week or so and my guess is that any noticeable performance difference is probably in those class methods that use system_service.evaluate(). For example Point.get_coordinates().

If only I / we / someone could figure out a way to remove the need for it. I've searched again today and no real luck. Lots of clues but nothing I can get to work.

As a test for the speed I created a part with 500 points in:

"""

Creates n random points in a new geometric set 'points'.

Requires CATIA V5 to be running and a CATPart active.

"""
import time

from random import randint

from pycatia import catia
from pycatia.mec_mod_interfaces.part_document import PartDocument

MIN = 0
MAX = 10000
TOTAL_POINTS = 500

start = time.time()

caa = catia()
active_document = PartDocument(caa.active_document.com_object)
part = active_document.part
hsf = part.hybrid_shape_factory
hbs = part.hybrid_bodies
gs_points = hbs.add()
gs_points.name = 'points'

for i in range(1, TOTAL_POINTS+1):
    new_point = hsf.add_new_point_coord(randint(MIN, MAX), randint(MIN, MAX), randint(MIN, MAX))
    gs_points.append_hybrid_shape(new_point)

part.update()
end = time.time()
print(end-start)

11 seconds

I then have a script that uses Point.get_coordinates() for all 500 points.

import time

from pycatia import catia
from pycatia.hybrid_shape_interfaces.point import Point
from pycatia.mec_mod_interfaces.part_document import PartDocument
from pycatia.enumeration.enumeration_types import geometrical_feature_type
from pycatia.space_analyses_interfaces.inertia import Inertia

start = time.time()

caa = catia()
application = caa.application
part_doc = PartDocument(application.active_document.com_object)
part = part_doc.part
spa_workbench = part_doc.spa_workbench()
hsf = part.hybrid_shape_factory
hbs = part.hybrid_bodies
hb_points = hbs.item('points')
shapes = hb_points.hybrid_shapes

for shape in shapes:
    gft = hsf.get_geometrical_feature_type(shape)
    gft_text = geometrical_feature_type[gft]
    if gft_text == 'Point':
        p = Point(shape.com_object)
        coord = p.get_coordinates()
        print(shape.name, coord)

end = time.time()
print(end-start)

23 seconds.

For just 500 points this took my machine 23 seconds. I think that's slow? But I don't know. I tried writing the equivalent in VBA but gave up as I just never really learned that side of things and writing VBA makes me feel dirty. Perhaps one of you could so we can do a real comparison?

An interesting fact tho: I didn't have any performance issues when I used a very stripped version of pycatia. But I didn't test it under the same premisses as I do now.

That framework looks pretty much identical to pycatia so why would it be any different? You've just got less modules which would possibly show a small difference in RAM usage and startup time. I doubt it would be enough to show a significance difference in execution time for a script hundreds/thousands of functions. But I'd be happy to see some evidence.

Less conjecture and more examples my friends. ❤️ 😄

evereux commented 6 months ago

Also, for completeness we should time the point creation script too and create a VBA equivalent of that. This is because the point creation script doesn't use any system_service.evaluate() calls.

edited above reply to include the point creation script time.

Djey51 commented 6 months ago

Hi, I propose to write the equivalent in VBA and also in VB.net to compare the 3 versions on the same device even if I really prefer python. But I need good performances with my scripts... Other point: I will be absent during the next 3 weeks. So it is not for very soon. Bye

evereux commented 6 months ago

No problem. 👍

Mithro86 commented 3 months ago

I did some testing comparing your code to a VBA version.

pycatia ~ 5s VBA ~ 2.3s

Something I noticed was that when using pycatia the display is refreshed. You can see the points created (if that makes sense). When using VBA however you don't.

I tried caa.refresh_display = False, but no difference.

Sub CATMain()

Dim MIN
Dim MAX
Dim TOTAL_POINTS
Dim i
Dim startTime
Dim endTime
Dim catia
Dim activeDocument 
Dim part
Dim hybridShapeFactory
Dim hybridBodies
Dim gsPoints
Dim newPoint

MIN = 0
MAX = 10000
TOTAL_POINTS = 500

startTime = Timer

Set catia = GetObject(, "CATIA.Application")
Set activeDocument = catia.ActiveDocument
Set part = activeDocument.Part
Set hybridShapeFactory = part.HybridShapeFactory
Set hybridBodies = part.HybridBodies
Set gsPoints = hybridBodies.Add()
gsPoints.Name = "points"

For i = 1 To TOTAL_POINTS
    Set newPoint = hybridShapeFactory.AddNewPointCoord(Int((MAX - MIN + 1) * Rnd + MIN), Int((MAX - MIN + 1) * Rnd + MIN), Int((MAX - MIN + 1) * Rnd + MIN))
    gsPoints.AppendHybridShape newPoint
Next

part.Update()

endTime = Timer
MsgBox "Elapsed time: " & (endTime - startTime) & " seconds"
End Sub
evereux commented 3 months ago

Great stuff. I'll have a play with this tomorrow.

Interesting what you say about the screen refresh. There's a pycatia script I was running the other day that I swear was faster by disabling the refresh_display, seemed to work. That was generating points I'm sure.

Also, we should really remove the random point generation outside of the timing. Probably won't make a noticeable difference but would be more scientific.

Mithro86 commented 3 months ago

Correction: caa.refresh_display = True --> 6s caa.refresh_display = False --> 5s

Mithro86 commented 3 months ago

So, thanks to this I got exposed to dealing with early and late bindings for the first time. I will never be the same. Hahaha

I did a test with a mix off early bindings, did not do much ~0.3s (NOTE: if you run this you have to clear gen_py afterwards otherwise it get stuck to early-bindings). After creating the bindings I commented it out:

from win32com.client import Dispatch, CastTo
from win32com.client.dynamic import DumbDispatch
from timeit import default_timer as timer
from random import randint

def GetRightDispatch(o):
    return Dispatch(DumbDispatch(o))

MIN = 0
MAX = 10000
TOTAL_POINTS = 500

start = timer()
caa = Dispatch('CATIA.Application')

caa.RefreshDisplay = False

active_document = caa.ActiveDocument

part = Dispatch(active_document.Part)
#part = GetRightDispatch(active_document.Part)
#part = CastTo(part, "Part") 

hsf = Dispatch(part.HybridShapeFactory)
#hsf = GetRightDispatch(part.HybridShapeFactory)
#hsf = CastTo(hsf, "HybridShapeFactory")

hbs = Dispatch(part.HybridBodies)
#hbs = GetRightDispatch(part.HybridBodies)
#hbs = CastTo(hbs, "HybridBodies")

gs_points = hbs.Add()
gs_points.Name = 'points'

for i in range(1, TOTAL_POINTS+1):
    new_point = hsf.AddNewPointCoord(i, i, i)
    gs_points.AppendHybridShape(new_point)

part.Update()
end = timer()
caa.RefreshDisplay = True
print(end - start)

When running both scripts, yours and this, I noted that (at random) the Geometrical Set was collapsed when the points was created. When it was, it was ~1s faster. I have not found a way to control this.

evereux commented 3 months ago

Interesting. Thanks so much for posting your findings.

I stated:

Interesting what you say about the screen refresh. There's a pycatia script I was running the other day that I swear was faster by disabling the refresh_display, seemed to work. That was generating points I'm sure.

It wasn't generating points, it was for speeding up the drawing of a template into the Drawing background for my little side project pycatia-tools (shamless plug).

Jumping about from project to project is frying my little brain. 😃

deloarts commented 3 months ago

Finally had the time to play around a bit with this issue. I wrote 3 scripts, one with pycatia, one using the win32com.Dispatch and one in catvbs. All 3 of them do the same:

  1. Create 10 new parts
  2. Create 400 new StrParam type UserRefProperties in each part
  3. Read each single StrParam by index
  4. Read each single StrParam by name

I added all scripts as zip so I don't clutter this issue with code: time_comp_240707.zip

Setup:

Results:

Script Average creating time Average reading time (index) Average reading time (name)
catvbs 0.009375s 0.002734s 0.952343s
dispatch 0.098642s 0.038168s 3.057094s
pycatia 0.043048s 0.084434s 3.586073s

Some things I found noteworthy:

Sidenotes:

Edit: Corrected values in result table

evereux commented 3 months ago

Great work @deloarts.

reading the UserRefProperties by index is significantly faster than reading them by name in all 3 scripts (could be important for anyone who might have performance issues regarding UserRefProperties)

That does make sense to me. Like a database lookup by id rather than string.

pycatia is faster than the dispatch when creating the StrParams, but slower in reading by index (I really did not expect this and I will try this on another machine)

A noticeable difference? That does surprise me. Recently, to pycatia's dispatch method I added pythoncom.CoInitialize(). This was so I could use pycatia in a GUI application (wouldn't work without it).

Could you run your manual dispatch version of the script with this added? If this is slowing things down I'll update pycatia to make that call optional. I'll do it myself later this week using your script if you haven't time. It didn't help prevent the behavior I have noted below.


So, this may or may not be related ... I've just run a script opening and closing a document a 1000 times in a for loop and I noticed that CATIA V5 kind of freezes every so often (no obvious pattern I can see).

The times were not consistent at all!

So, I did a bit of playing:

import time
import statistics
from pathlib import Path

from pycatia import catia

def open_document(file: Path, number_of_tries:int):
    times = []
    caa = catia()
    caa.logger.disabled = True
    documents = caa.documents
    for i in range(number_of_tries):
        start = time.time()
        document = documents.open(test_file)
        document.close()
        end = time.time()
        times.append(end-start)
    return times

test_file = Path(r'C:\Users\evereux\cloud\pycatia\__archive__\Part1.CATPart')
number_of_tries = 100
times = open_document(test_file, number_of_tries)

print(f'TIMES_REUSE: The fastest opening time was {min(times)}.')
print(f'TIMES_REUSE: The slowest opening time was {max(times)}.')
print(f'TIMES_REUSE: The slowest opening time was {statistics.mean(times)}.')
evereux commented 3 months ago

Just another thought .. when comparing VB versus Python I don't think we should allow anything to output to terminal as this will slow things down. For example, disable logging caa.logger.disabled = True and don't print anything to terminal within a timed operation.

I think I'm the only one that has done that above though. d'oh.

Djey51 commented 3 months ago

Hello, Here is my contribution and my results:

Tests done with python, python_alt, vb.net, catvba for 500 and 5000 points (yes, my laptop is very efficient and the results are more significant with 5000 points 😜) For 500 pts. For 5000 pts

You will find below used scripts for these tests.

Python

import time
from random import randint
##########################################################
# insert syspath to project folder so examples can be run.
# for development purposes.
import os
import sys
from pathlib import Path
sys.path.insert(0, os.path.abspath('..\\pycatia'))
##########################################################
from pycatia import catia
from pycatia.mec_mod_interfaces.part_document import PartDocument
from pycatia.mec_mod_interfaces.part import Part
from pycatia.hybrid_shape_interfaces.hybrid_shape_factory import HybridShapeFactory
from pycatia.mec_mod_interfaces.hybrid_bodies import HybridBodies
def cat_main():
    min_val = 0
    max_val = 10000
    total_points = 5000
    start_time = time.time()
    # Get the CATIA application and active document
    cat = catia()
    cat.refresh_display = False
    # docs = Documents(cat.documents.com_object)
    part_doc = PartDocument(cat.active_document.com_object)
    part = Part(part_doc.part.com_object)
    hybrid_shape_factory = HybridShapeFactory(part.hybrid_shape_factory.com_object)
    hybrid_bodies = HybridBodies(part.hybrid_bodies.com_object)
    # Create a new hybrid body and set its name
    gs_points = hybrid_bodies.add()
    [gs_points.name](https://github.com/evereux/pycatia/issues/gs_points.name) = "points"
    for i in range(1, total_points +1):
        # Generate random coordinates for each point
        x, y, z = (randint(min_val, max_val), randint(min_val, max_val), randint(min_val, max_val))
        # Add a new point to the hybrid body
        new_point = hybrid_shape_factory.add_new_point_coord(x, y, z)
        gs_points.append_hybrid_shape(new_point)
    part.update()
    end_time = time.time()
    elapsed_time = end_time - start_time
    # cat.refresh_display = True
    print(f"Elapsed time: {elapsed_time:.2f} seconds")
if __name__ == "__main__":
    cat_main()

Python_alt

from win32com.client import Dispatch, CastTo
from win32com.client.dynamic import DumbDispatch
from timeit import default_timer as timer
from random import randint
def GetRightDispatch(o):
    return Dispatch(DumbDispatch(o))
MIN = 0
MAX = 10000
TOTAL_POINTS = 5000
start = timer()
caa = Dispatch('CATIA.Application')
caa.RefreshDisplay = False
active_document = caa.ActiveDocument
part = Dispatch(active_document.Part)
#part = GetRightDispatch(active_document.Part)
#part = CastTo(part, "Part")
hsf = Dispatch(part.HybridShapeFactory)
#hsf = GetRightDispatch(part.HybridShapeFactory)
#hsf = CastTo(hsf, "HybridShapeFactory")
hbs = Dispatch(part.HybridBodies)
#hbs = GetRightDispatch(part.HybridBodies)
#hbs = CastTo(hbs, "HybridBodies")
gs_points = hbs.Add()
gs_points.Name = 'points'
for i in range(1, TOTAL_POINTS+1): 
    # Generate random coordinates for each point
    x, y, z = (randint(MIN, MAX), randint(MIN, MAX), randint(MIN, MAX))
    # Add a new point to the hybrid body
    new_point = hsf.AddNewPointCoord(x, y, z)
    gs_points.AppendHybridShape(new_point)
part.Update()
end = timer()
caa.RefreshDisplay = True
print(end - start)

Vb.net

Imports System.Diagnostics
Imports MECMOD
Imports INFITF
Imports PARTITF
Imports HybridShapeTypeLib
Module Module1
    Sub Main()
        Dim MIN
        Dim MAX
        Dim TOTAL_POINTS
        Dim i As Integer
        Dim startTime
        Dim endTime
        Dim CATIA As Application
        MIN = 0
        MAX = 10000
        TOTAL_POINTS = 500
        startTime = Timer
        CATIA = GetObject(, "CATIA.Application")
        CATIA.RefreshDisplay = False
        ' Récupérer le document actif
        Dim partDocument As PartDocument
        partDocument = CATIA.ActiveDocument
        ' Récupérer la partie active
        Dim part As Part
        part = partDocument.Part
        ' Récupérer la collection des bodies
        Dim bodies As Bodies
        bodies = part.Bodies
        ' Créer un nouveau set géométrique
        Dim hybridBodies As HybridBodies
        hybridBodies = part.HybridBodies
        Dim hybridBody As HybridBody
        hybridBody = hybridBodies.Add()
        hybridBody.Name = "points"
        ' Créer un point
        Dim hybridShapeFactory As HybridShapeFactory
        hybridShapeFactory = part.HybridShapeFactory
        Dim point As HybridShapePointCoord
        Dim random As New Random()
        Dim x As Double
        Dim y As Double
        Dim z As Double
        For i = 1 To TOTAL_POINTS
            x = random.NextDouble() * 1000
            y = random.NextDouble() * 1000
            z = random.NextDouble() * 1000
            point = hybridShapeFactory.AddNewPointCoord(x, y, z)
            ' Ajouter le point au set géométrique
            hybridBody.AppendHybridShape(point)
        Next
        ' Mettre à jour la partie
        part.Update()
        endTime = Timer
        CATIA.RefreshDisplay = True
        MsgBox("Temps d'exécution : " & (endTime - startTime) & " Secondes")
    End Sub
End Module

Catvba

Sub CATMain()
    Dim MIN
    Dim MAX
    Dim TOTAL_POINTS
    Dim i
    Dim startTime
    Dim endTime
    Dim catia
    Dim activeDocument
    Dim part
    Dim hybridShapeFactory
    Dim hybridBodies
    Dim gsPoints
    Dim newPoint
   
    MIN = 0
    MAX = 10000
    TOTAL_POINTS = 500
   
    startTime = Timer
   
    Set catia = GetObject(, "CATIA.Application")
    Set activeDocument = catia.activeDocument
    Set part = activeDocument.part
    Set hybridShapeFactory = part.hybridShapeFactory
    Set hybridBodies = part.hybridBodies
    Set gsPoints = hybridBodies.Add()
    gsPoints.Name = "points"
   
    For i = 1 To TOTAL_POINTS
    Set newPoint = hybridShapeFactory.AddNewPointCoord(Int((MAX - MIN + 1) * Rnd + MIN), Int((MAX - MIN + 1) * Rnd + MIN), Int((MAX - MIN + 1) * Rnd + MIN))
    gsPoints.AppendHybridShape newPoint
    Next
   
    part.Update
   
    endTime = Timer
    MsgBox "Elapsed time: " & (endTime - startTime) & " seconds"
End Sub

Best regards

evereux commented 3 months ago

Thanks @Djey51!

my laptop is very efficient and the results are more significant with 5000 points

you're not kidding! 😄