PyParticles3 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. pyparticles/__init__.py +26 -0
  2. pyparticles/__main__.py +7 -0
  3. pyparticles/animation/__init__.py +21 -0
  4. pyparticles/animation/animated_cli.py +53 -0
  5. pyparticles/animation/animated_ogl.py +691 -0
  6. pyparticles/animation/animated_ogl_compat.py +243 -0
  7. pyparticles/animation/animated_scatter.py +94 -0
  8. pyparticles/animation/animation.py +251 -0
  9. pyparticles/animation/test_animation.py +310 -0
  10. pyparticles/demo/__init__.py +33 -0
  11. pyparticles/demo/bubble.py +106 -0
  12. pyparticles/demo/electromagnetic_demo.py +128 -0
  13. pyparticles/demo/electrostatic_demo.py +126 -0
  14. pyparticles/demo/fountain.py +478 -0
  15. pyparticles/demo/gas_lennard_jones.py +87 -0
  16. pyparticles/demo/gravity_clusters.py +73 -0
  17. pyparticles/demo/solar_system.py +222 -0
  18. pyparticles/demo/springs.py +141 -0
  19. pyparticles/demo/springs_constr.py +135 -0
  20. pyparticles/demo/test.py +34 -0
  21. pyparticles/forces/__init__.py +21 -0
  22. pyparticles/forces/const_force.py +140 -0
  23. pyparticles/forces/damping.py +174 -0
  24. pyparticles/forces/drag.py +167 -0
  25. pyparticles/forces/electromagnetic.py +105 -0
  26. pyparticles/forces/electromagnetic_field.py +95 -0
  27. pyparticles/forces/electrostatic.py +72 -0
  28. pyparticles/forces/force.py +75 -0
  29. pyparticles/forces/force_constrained.py +32 -0
  30. pyparticles/forces/fused_const_drag.py +445 -0
  31. pyparticles/forces/gravity.py +345 -0
  32. pyparticles/forces/lennard_jones.py +74 -0
  33. pyparticles/forces/linear_spring.py +86 -0
  34. pyparticles/forces/linear_spring_constrained.py +72 -0
  35. pyparticles/forces/multiple_force.py +134 -0
  36. pyparticles/forces/pseudo_bubble.py +226 -0
  37. pyparticles/forces/van_der_waals_force.py +60 -0
  38. pyparticles/forces/vector_field_force.py +52 -0
  39. pyparticles/geometry/__init__.py +21 -0
  40. pyparticles/geometry/dist.py +24 -0
  41. pyparticles/geometry/intersection.py +62 -0
  42. pyparticles/geometry/transformations.py +387 -0
  43. pyparticles/main/__init__.py +21 -0
  44. pyparticles/main/main.py +466 -0
  45. pyparticles/measures/__init__.py +21 -0
  46. pyparticles/measures/elastic_potential_energy.py +67 -0
  47. pyparticles/measures/gravitational_potential_energy.py +69 -0
  48. pyparticles/measures/kinetic_energy.py +68 -0
  49. pyparticles/measures/mass.py +59 -0
  50. pyparticles/measures/measure.py +156 -0
  51. pyparticles/measures/momentum.py +144 -0
  52. pyparticles/measures/total_energy.py +68 -0
  53. pyparticles/ode/__init__.py +21 -0
  54. pyparticles/ode/euler_solver.py +214 -0
  55. pyparticles/ode/euler_solver_constrained.py +49 -0
  56. pyparticles/ode/leapfrog_solver.py +37 -0
  57. pyparticles/ode/leapfrog_solver_constrained.py +53 -0
  58. pyparticles/ode/midpoint_solver.py +43 -0
  59. pyparticles/ode/midpoint_solver_constrained.py +60 -0
  60. pyparticles/ode/ode_solver.py +134 -0
  61. pyparticles/ode/ode_solver_constrained.py +43 -0
  62. pyparticles/ode/runge_kutta_solver.py +79 -0
  63. pyparticles/ode/runge_kutta_solver_constrained.py +98 -0
  64. pyparticles/ode/sim_time.py +56 -0
  65. pyparticles/ode/stormer_verlet_solver.py +50 -0
  66. pyparticles/ode/stormer_verlet_solver_constrained.py +73 -0
  67. pyparticles/ogl/__init__.py +21 -0
  68. pyparticles/ogl/axis_ogl.py +210 -0
  69. pyparticles/ogl/draw_particles_ogl.py +313 -0
  70. pyparticles/ogl/draw_particles_ogl_compat.py +509 -0
  71. pyparticles/ogl/draw_vector_field.py +221 -0
  72. pyparticles/ogl/opencl_gl_vbo.py +485 -0
  73. pyparticles/ogl/trackball.py +131 -0
  74. pyparticles/ogl/translate_scene.py +87 -0
  75. pyparticles/pset/__init__.py +21 -0
  76. pyparticles/pset/boundary.py +69 -0
  77. pyparticles/pset/cluster.py +28 -0
  78. pyparticles/pset/constrained_force_interactions.py +63 -0
  79. pyparticles/pset/constrained_x.py +158 -0
  80. pyparticles/pset/constraint.py +42 -0
  81. pyparticles/pset/default_boundary.py +43 -0
  82. pyparticles/pset/file_cluster.py +131 -0
  83. pyparticles/pset/logger.py +152 -0
  84. pyparticles/pset/octree.py +451 -0
  85. pyparticles/pset/opencl_context.py +374 -0
  86. pyparticles/pset/particles_set.py +499 -0
  87. pyparticles/pset/periodic_boundary.py +36 -0
  88. pyparticles/pset/rand_cluster.py +188 -0
  89. pyparticles/pset/rebound_boundary.py +68 -0
  90. pyparticles/utils/__init__.py +21 -0
  91. pyparticles/utils/parse_args.py +72 -0
  92. pyparticles/utils/problem_config.py +608 -0
  93. pyparticles/utils/pypart_global.py +122 -0
  94. pyparticles/utils/time_formatter.py +50 -0
  95. pyparticles3-0.4.0.dist-info/METADATA +395 -0
  96. pyparticles3-0.4.0.dist-info/RECORD +100 -0
  97. pyparticles3-0.4.0.dist-info/WHEEL +5 -0
  98. pyparticles3-0.4.0.dist-info/entry_points.txt +3 -0
  99. pyparticles3-0.4.0.dist-info/licenses/LICENSE-gpl-3.0.txt +674 -0
  100. pyparticles3-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,221 @@
1
+
2
+ # PyParticles : Particles simulation in python
3
+ # Copyright (C) 2012 Simone Riva
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+
19
+ import numpy as np
20
+ import random
21
+
22
+ import pyparticles.geometry.transformations as tr
23
+
24
+ from OpenGL.GL import *
25
+
26
+
27
+
28
+ class DrawVectorField( object ):
29
+ """
30
+ Draw the given vector fields.
31
+
32
+ Constructor:
33
+
34
+ :param limits: size of the draw volume: (x_min,x_max,y_min,y_max,z_min,z_max)
35
+ :param density: distance between the plotted vectors
36
+
37
+ """
38
+ def __init__( self , limits , density ):
39
+
40
+ self.__fields = dict()
41
+ self.__col_fun = dict()
42
+
43
+ if len(limits) not in ( 4 , 6 ):
44
+ raise ValueError("limits are allowed only for 2D or 3D")
45
+
46
+ self.__limits = limits
47
+ self.__density = density
48
+
49
+ self._build_coords()
50
+
51
+ def __del__(self):
52
+ for key in self.__fields.keys() :
53
+ if self.__fields[key]["display_list"] != None :
54
+ glDeleteLists( self.__fields[key]["display_list"] , 1 )
55
+
56
+
57
+ def _build_coords(self):
58
+ """
59
+ private: build the coordinates of the vectors
60
+ """
61
+
62
+ li = self.__limits
63
+ de = self.__density
64
+
65
+ sz_x = int( ( li[1] - li[0] ) / de )
66
+ sz_y = int( ( li[3] - li[2] ) / de )
67
+
68
+ if len(li) == 6 :
69
+ sz_z = int( ( li[5] - li[4] ) / de )
70
+ else:
71
+ sz_z = 1.0
72
+ li[4] = 0.0
73
+ li[5] = 0.0
74
+
75
+ self.__X = np.zeros(( sz_x * sz_y * sz_z , 3 ))
76
+ self.__V = np.zeros(( sz_x * sz_y * sz_z , 3 ))
77
+ self.__Vs = np.zeros(( sz_x * sz_y * sz_z , 3 ))
78
+
79
+ x = np.float64( li[0] )
80
+ y = np.float64( li[2] )
81
+ z = np.float64( li[4] )
82
+
83
+ indx = 0
84
+ for i in range(sz_z):
85
+ x = li[0]
86
+ for j in range(sz_x):
87
+ y = li[2]
88
+ for l in range(sz_y):
89
+ self.__X[indx,:] = np.array([x,y,z])
90
+ indx+=1
91
+ y = y + de
92
+ x = x + de
93
+ z = z + de
94
+
95
+ def _default_color( self , RGBA , X ):
96
+ RGBA[:] = np.array([ 0.7 , 0.7 , 0.0 , 0.5 ])
97
+
98
+ def add_vector_fun( self , fun , unit_len=1.0 , color_fun=None , key=None , time_dep=False ):
99
+ r"""
100
+ Insert a new vector function,
101
+
102
+ Parameters
103
+ ----------
104
+ fun : Vector filed function
105
+ color_fun : Colors function
106
+ key : [optional] a key used for distinguish the vector field
107
+ time_dep : [True or **False** ] if True Is a time dependent filed
108
+
109
+
110
+ where functions are defined:
111
+ fun( V , X )
112
+ color_fun( RGBA , X )
113
+
114
+ | X : (n by DIM) coordinates array
115
+ | V : (n by DIM) resulting vector field
116
+ | RGBA : (n by 4) colors array
117
+ """
118
+ if key == None :
119
+ key = str( random.randint( 0 , 2**64 ) )
120
+
121
+ if color_fun == None :
122
+ color_fun = self._default_color
123
+
124
+ self.__fields[key] = { "fun": fun ,
125
+ "color_fun": color_fun ,
126
+ "time_dep" : time_dep ,
127
+ "unit" : unit_len ,
128
+ "display_list" : None }
129
+
130
+ return key
131
+
132
+
133
+ def ogl_init(self):
134
+ for key in self.__fields.keys() :
135
+ if not self.__fields[key]["time_dep"] :
136
+ dl = glGenLists(1)
137
+
138
+ glNewList( dl , GL_COMPILE );
139
+ self._draw_field(key)
140
+ glEndList()
141
+
142
+ self.__fields[key]["display_list"] = dl
143
+
144
+
145
+ def _draw_field( self , key ):
146
+
147
+ sz = self.__X.shape[0]
148
+
149
+ transf = tr.Transformations()
150
+ transf.set_points_tuple_size(6)
151
+
152
+ self.__fields[key]["fun"]( self.__V , self.__X )
153
+
154
+ # Vector in spherical coordinates
155
+ self.__Vs[:,0] = np.sqrt( np.sum( self.__V**2 , 1 ) )
156
+ self.__Vs[:,1] = np.arccos( self.__V[:,2] / self.__Vs[:,0] )
157
+ self.__Vs[:,2] = np.arctan( np.divide( self.__V[:,1] , self.__V[:,0] ) )
158
+
159
+ err_nan = np.isnan( self.__Vs[:,2] )
160
+ self.__Vs[err_nan,2] = np.sign( self.__V[err_nan,2] ) * np.pi / 2.0
161
+
162
+ unit = self.__fields[key]["unit"]
163
+
164
+ for i in range(sz):
165
+
166
+ x = self.__X[i,0] / self.__density
167
+ y = self.__X[i,1] / self.__density
168
+ z = self.__X[i,2] / self.__density
169
+
170
+ transf.push_matrix()
171
+ transf.translation( x , y , z )
172
+
173
+ transf.rotZ( self.__Vs[i,2] )
174
+ transf.rotY( -( np.pi/2.0 - self.__Vs[i,1] ) )
175
+
176
+ le = self.__Vs[i,0] / unit
177
+
178
+ transf.append_point( [ 0 , 0 , 0 ] )
179
+ transf.append_point( [ le , 0 , 0 ] )
180
+
181
+ transf.append_point( [ le , 0 , 0 ] )
182
+ transf.append_point( [ 0.8*le , 0.1*le , 0 ] )
183
+
184
+ transf.append_point( [ le , 0 , 0 ] )
185
+ transf.append_point( [ 0.8*le , -0.1*le , 0 ] )
186
+
187
+ transf.pop_matrix()
188
+
189
+ color = np.zeros((4))
190
+
191
+ glBegin(GL_LINES)
192
+
193
+ for pts in transf :
194
+ self.__fields[key]["color_fun"]( color , pts[0] )
195
+
196
+ glColor4f( color[0] , color[1] , color[2] , color[3] )
197
+
198
+ glVertex3fv( pts[0] )
199
+ glVertex3fv( pts[1] )
200
+
201
+ glVertex3fv( pts[2] )
202
+ glVertex3fv( pts[3] )
203
+
204
+ glVertex3fv( pts[4] )
205
+ glVertex3fv( pts[5] )
206
+
207
+ glEnd()
208
+
209
+
210
+ def draw(self):
211
+ for key in self.__fields.keys() :
212
+
213
+ if self.__fields[key]["display_list"] != None :
214
+ glCallList( self.__fields[key]["display_list"] )
215
+ else :
216
+ self._draw_field(key)
217
+
218
+
219
+
220
+
221
+
@@ -0,0 +1,485 @@
1
+ # PyParticles : Particles simulation in python
2
+ # Copyright (C) 2012 Simone Riva
3
+ #
4
+ # OpenCL/OpenGL zero-host-copy rendering helpers.
5
+
6
+ """Share particle positions with OpenGL through double-buffered VBOs.
7
+
8
+ The stable path keeps canonical X in an ordinary OpenCL buffer and copies it to
9
+ an alternating shared VBO after each simulation step. An experimental fused
10
+ path acquires the next VBO before the step so the fountain integration kernel
11
+ can mirror its final X values directly into that VBO. Both paths retain
12
+ per-buffer OpenGL fences and never require host position transfers.
13
+ """
14
+
15
+ import time
16
+
17
+ import numpy as np
18
+
19
+ try:
20
+ import pyopencl as cl
21
+ except ImportError:
22
+ cl = None
23
+
24
+ from OpenGL.GL import (
25
+ GL_ALREADY_SIGNALED,
26
+ GL_ARRAY_BUFFER,
27
+ GL_CONDITION_SATISFIED,
28
+ GL_DYNAMIC_DRAW,
29
+ GL_SYNC_FLUSH_COMMANDS_BIT,
30
+ GL_SYNC_GPU_COMMANDS_COMPLETE,
31
+ glBindBuffer,
32
+ glBufferData,
33
+ glClientWaitSync,
34
+ glDeleteBuffers,
35
+ glDeleteSync,
36
+ glFenceSync,
37
+ glFinish,
38
+ glGenBuffers,
39
+ )
40
+
41
+
42
+ def _event_profile_seconds(event):
43
+ """Return valid timing phases for a completed OpenCL event.
44
+
45
+ NVIDIA's GL acquire/release events may expose ``start``/``end`` while
46
+ leaving ``queued`` or ``submit`` equal to zero. Treat unavailable phases
47
+ as NaN instead of subtracting zero from an absolute device timestamp.
48
+ """
49
+ nan = float("nan")
50
+ result = {
51
+ "queued_to_submit_s": nan,
52
+ "submit_to_start_s": nan,
53
+ "queued_to_start_s": nan,
54
+ "execution_s": nan,
55
+ "queued_to_end_s": nan,
56
+ }
57
+ if event is None:
58
+ return result
59
+
60
+ try:
61
+ queued = int(event.profile.queued)
62
+ submit = int(event.profile.submit)
63
+ start = int(event.profile.start)
64
+ end = int(event.profile.end)
65
+ except Exception:
66
+ return result
67
+
68
+ scale = 1.0e-9
69
+ if queued > 0 and submit >= queued:
70
+ result["queued_to_submit_s"] = (submit - queued) * scale
71
+ if submit > 0 and start >= submit:
72
+ result["submit_to_start_s"] = (start - submit) * scale
73
+ if queued > 0 and start >= queued:
74
+ result["queued_to_start_s"] = (start - queued) * scale
75
+ if start > 0 and end >= start:
76
+ result["execution_s"] = (end - start) * scale
77
+ if queued > 0 and end >= queued:
78
+ result["queued_to_end_s"] = (end - queued) * scale
79
+ return result
80
+
81
+
82
+ class OpenCLGLPositionBuffer(object):
83
+ """Mirror an OpenCL X buffer into alternating GL VBOs without host copies."""
84
+
85
+ _BUFFER_COUNT = 2
86
+ _FENCE_TIMEOUT_NS = 100000000 # 100 ms; fallback to glFinish after this.
87
+
88
+ def __init__(self, ocl_context, pset, draw_particles=None):
89
+ if cl is None:
90
+ raise RuntimeError("PyOpenCL is required for CL/GL interoperability")
91
+ if not getattr(ocl_context, "gl_sharing", False):
92
+ raise RuntimeError("OpenCL context was not created with GL sharing")
93
+
94
+ self.__occ = ocl_context
95
+ self.__pset = pset
96
+ self.__draw_particles = draw_particles
97
+ self.__nbytes = int(pset.size * pset.dim * np.dtype(np.float32).itemsize)
98
+ self.__copy_calls = 0
99
+ self.__copy_bytes = 0
100
+ self.__mirror_calls = 0
101
+ self.__mirror_bytes = 0
102
+ self.__closed = False
103
+ self.__next_index = 0
104
+ self.__active_index = 0
105
+ self.__vbos = []
106
+ self.__gl_buffers = []
107
+ self.__fences = [None] * self._BUFFER_COUNT
108
+ self.__needs_gl_completion = [False] * self._BUFFER_COUNT
109
+ self.__vbo_to_index = {}
110
+ self.__fused_pending = None
111
+ self.__last_profile = self._empty_profile()
112
+
113
+ vertices = np.ascontiguousarray(pset.X, dtype=np.float32)
114
+ try:
115
+ for index in range(self._BUFFER_COUNT):
116
+ vbo = int(np.asarray(glGenBuffers(1)).reshape(-1)[0])
117
+ self.__vbos.append(vbo)
118
+ self.__vbo_to_index[vbo] = index
119
+ glBindBuffer(GL_ARRAY_BUFFER, vbo)
120
+ glBufferData(
121
+ GL_ARRAY_BUFFER,
122
+ self.__nbytes,
123
+ vertices,
124
+ GL_DYNAMIC_DRAW,
125
+ )
126
+ self.__gl_buffers.append(
127
+ self.__occ.create_gl_buffer(
128
+ vbo, flags=cl.mem_flags.READ_WRITE
129
+ )
130
+ )
131
+ finally:
132
+ glBindBuffer(GL_ARRAY_BUFFER, 0)
133
+
134
+ if len(self.__vbos) != self._BUFFER_COUNT:
135
+ self.close()
136
+ raise RuntimeError("Could not allocate both CL/GL position VBOs")
137
+
138
+ if draw_particles is not None:
139
+ draw_particles.set_shared_position_vbo(self.__vbos[0])
140
+ draw_particles.set_shared_position_draw_complete_callback(
141
+ self.mark_draw_complete
142
+ )
143
+
144
+ @staticmethod
145
+ def _empty_profile():
146
+ return {
147
+ "gl_finish_wall_s": 0.0,
148
+ "gl_fence_wait_wall_s": 0.0,
149
+ "gl_finish_fallback_wall_s": 0.0,
150
+ "fence_immediate": 1.0,
151
+ "acquire_gpu_s": 0.0,
152
+ "acquire_queue_s": float("nan"),
153
+ "acquire_total_s": float("nan"),
154
+ "copy_gpu_s": 0.0,
155
+ "copy_queue_s": float("nan"),
156
+ "copy_total_s": float("nan"),
157
+ "release_gpu_s": 0.0,
158
+ "release_queue_s": float("nan"),
159
+ "release_total_s": float("nan"),
160
+ "release_wait_wall_s": 0.0,
161
+ "bridge_wall_s": 0.0,
162
+ "vbo_index": 0.0,
163
+ "fused_mirror": 0.0,
164
+ }
165
+
166
+ def mark_draw_complete(self, vbo):
167
+ """Insert a fence immediately after OpenGL submits a draw using *vbo*."""
168
+ if self.__closed:
169
+ return
170
+
171
+ index = self.__vbo_to_index.get(int(vbo))
172
+ if index is None:
173
+ return
174
+
175
+ old_fence = self.__fences[index]
176
+ if old_fence is not None:
177
+ try:
178
+ glDeleteSync(old_fence)
179
+ except Exception:
180
+ pass
181
+ self.__fences[index] = None
182
+
183
+ self.__needs_gl_completion[index] = True
184
+ try:
185
+ fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0)
186
+ if fence:
187
+ self.__fences[index] = fence
188
+ except Exception:
189
+ self.__fences[index] = None
190
+
191
+ def _wait_for_gl_vbo(self, index):
192
+ """Wait only for the GL draw that last consumed one VBO."""
193
+ if not self.__needs_gl_completion[index]:
194
+ return 0.0, 1.0, 0.0
195
+
196
+ start = time.perf_counter()
197
+ fallback_seconds = 0.0
198
+ immediate = 0.0
199
+ fence = self.__fences[index]
200
+
201
+ try:
202
+ if fence is None:
203
+ fallback_start = time.perf_counter()
204
+ glFinish()
205
+ fallback_seconds = time.perf_counter() - fallback_start
206
+ else:
207
+ result = glClientWaitSync(fence, 0, 0)
208
+ if result in (GL_ALREADY_SIGNALED, GL_CONDITION_SATISFIED):
209
+ immediate = 1.0
210
+ else:
211
+ result = glClientWaitSync(
212
+ fence,
213
+ GL_SYNC_FLUSH_COMMANDS_BIT,
214
+ self._FENCE_TIMEOUT_NS,
215
+ )
216
+ if result not in (GL_ALREADY_SIGNALED, GL_CONDITION_SATISFIED):
217
+ fallback_start = time.perf_counter()
218
+ glFinish()
219
+ fallback_seconds = time.perf_counter() - fallback_start
220
+ except Exception:
221
+ fallback_start = time.perf_counter()
222
+ glFinish()
223
+ fallback_seconds = time.perf_counter() - fallback_start
224
+ finally:
225
+ if fence is not None:
226
+ try:
227
+ glDeleteSync(fence)
228
+ except Exception:
229
+ pass
230
+ self.__fences[index] = None
231
+ self.__needs_gl_completion[index] = False
232
+
233
+ return time.perf_counter() - start, immediate, fallback_seconds
234
+
235
+ def _select_next_vbo(self):
236
+ index = self.__next_index
237
+ self.__next_index = (self.__next_index + 1) % self._BUFFER_COUNT
238
+ fence_wait, fence_immediate, finish_fallback = self._wait_for_gl_vbo(index)
239
+ return index, fence_wait, fence_immediate, finish_fallback
240
+
241
+ def update_from_device(self):
242
+ """Stable path: copy current device X into the next VBO and draw it."""
243
+ if self.__closed:
244
+ raise RuntimeError("The shared OpenGL position buffer is closed")
245
+ if self.__fused_pending is not None:
246
+ raise RuntimeError("A fused CL/GL render target is already acquired")
247
+
248
+ bridge_start = time.perf_counter()
249
+ index, fence_wait, fence_immediate, finish_fallback = self._select_next_vbo()
250
+
251
+ gl_buffer = self.__gl_buffers[index]
252
+ acquire = self.__occ.acquire_gl_objects([gl_buffer])
253
+ copy = cl.enqueue_copy(
254
+ self.__occ.CL_queue,
255
+ gl_buffer,
256
+ self.__occ.X_cla.data,
257
+ byte_count=self.__nbytes,
258
+ wait_for=[acquire],
259
+ )
260
+ release = self.__occ.release_gl_objects(
261
+ [gl_buffer], wait_for=[copy]
262
+ )
263
+
264
+ # Without cl_khr_gl_event, OpenGL must not consume the VBO until the CL
265
+ # release is complete. Keep this wait so rendering remains correct.
266
+ wait_start = time.perf_counter()
267
+ release.wait()
268
+ wait_end = time.perf_counter()
269
+
270
+ acquire_profile = _event_profile_seconds(acquire)
271
+ copy_profile = _event_profile_seconds(copy)
272
+ release_profile = _event_profile_seconds(release)
273
+
274
+ self.__active_index = index
275
+ if self.__draw_particles is not None:
276
+ self.__draw_particles.set_shared_position_vbo(self.__vbos[index])
277
+
278
+ bridge_end = time.perf_counter()
279
+ self.__last_profile = {
280
+ "gl_finish_wall_s": 0.0,
281
+ "gl_fence_wait_wall_s": fence_wait,
282
+ "gl_finish_fallback_wall_s": finish_fallback,
283
+ "fence_immediate": fence_immediate,
284
+ "acquire_gpu_s": acquire_profile["execution_s"],
285
+ "acquire_queue_s": acquire_profile["queued_to_start_s"],
286
+ "acquire_total_s": acquire_profile["queued_to_end_s"],
287
+ "copy_gpu_s": copy_profile["execution_s"],
288
+ "copy_queue_s": copy_profile["queued_to_start_s"],
289
+ "copy_total_s": copy_profile["queued_to_end_s"],
290
+ "release_gpu_s": release_profile["execution_s"],
291
+ "release_queue_s": release_profile["queued_to_start_s"],
292
+ "release_total_s": release_profile["queued_to_end_s"],
293
+ "release_wait_wall_s": wait_end - wait_start,
294
+ "bridge_wall_s": bridge_end - bridge_start,
295
+ "vbo_index": float(index),
296
+ "fused_mirror": 0.0,
297
+ }
298
+
299
+ self.__copy_calls += 1
300
+ self.__copy_bytes += self.__nbytes
301
+ return release
302
+
303
+ def prepare_fused_render(self):
304
+ """Acquire the next VBO so a simulation kernel may write it directly.
305
+
306
+ Returns ``(cl_buffer, acquire_event)``. The caller must arrange for its
307
+ kernel to wait for the acquire event and then call
308
+ :meth:`finish_fused_render` with the kernel event.
309
+ """
310
+ if self.__closed:
311
+ raise RuntimeError("The shared OpenGL position buffer is closed")
312
+ if self.__fused_pending is not None:
313
+ raise RuntimeError("A fused CL/GL render target is already acquired")
314
+
315
+ prepare_start = time.perf_counter()
316
+ index, fence_wait, fence_immediate, finish_fallback = self._select_next_vbo()
317
+ gl_buffer = self.__gl_buffers[index]
318
+ acquire = self.__occ.acquire_gl_objects([gl_buffer])
319
+ prepare_end = time.perf_counter()
320
+
321
+ self.__fused_pending = {
322
+ "index": index,
323
+ "gl_buffer": gl_buffer,
324
+ "acquire": acquire,
325
+ "fence_wait": fence_wait,
326
+ "fence_immediate": fence_immediate,
327
+ "finish_fallback": finish_fallback,
328
+ "prepare_wall_s": prepare_end - prepare_start,
329
+ }
330
+ return gl_buffer, acquire
331
+
332
+ def finish_fused_render(self, kernel_event):
333
+ """Release a VBO written by the fused simulation kernel and draw it."""
334
+ if self.__closed:
335
+ raise RuntimeError("The shared OpenGL position buffer is closed")
336
+ pending = self.__fused_pending
337
+ if pending is None:
338
+ raise RuntimeError("No fused CL/GL render target is pending")
339
+ if kernel_event is None:
340
+ raise ValueError("kernel_event is required for fused CL/GL rendering")
341
+
342
+ finish_start = time.perf_counter()
343
+ release = self.__occ.release_gl_objects(
344
+ [pending["gl_buffer"]], wait_for=[kernel_event]
345
+ )
346
+ wait_start = time.perf_counter()
347
+ release.wait()
348
+ wait_end = time.perf_counter()
349
+
350
+ acquire_profile = _event_profile_seconds(pending["acquire"])
351
+ release_profile = _event_profile_seconds(release)
352
+
353
+ index = pending["index"]
354
+ self.__active_index = index
355
+ if self.__draw_particles is not None:
356
+ self.__draw_particles.set_shared_position_vbo(self.__vbos[index])
357
+
358
+ finish_end = time.perf_counter()
359
+ self.__last_profile = {
360
+ "gl_finish_wall_s": 0.0,
361
+ "gl_fence_wait_wall_s": pending["fence_wait"],
362
+ "gl_finish_fallback_wall_s": pending["finish_fallback"],
363
+ "fence_immediate": pending["fence_immediate"],
364
+ "acquire_gpu_s": acquire_profile["execution_s"],
365
+ "acquire_queue_s": acquire_profile["queued_to_start_s"],
366
+ "acquire_total_s": acquire_profile["queued_to_end_s"],
367
+ "copy_gpu_s": 0.0,
368
+ "copy_queue_s": 0.0,
369
+ "copy_total_s": 0.0,
370
+ "release_gpu_s": release_profile["execution_s"],
371
+ "release_queue_s": release_profile["queued_to_start_s"],
372
+ "release_total_s": release_profile["queued_to_end_s"],
373
+ "release_wait_wall_s": wait_end - wait_start,
374
+ "bridge_wall_s": pending["prepare_wall_s"] + (finish_end - finish_start),
375
+ "vbo_index": float(index),
376
+ "fused_mirror": 1.0,
377
+ }
378
+ self.__fused_pending = None
379
+ self.__mirror_calls += 1
380
+ self.__mirror_bytes += self.__nbytes
381
+ return release
382
+
383
+ def _release_pending_fused_vbo(self):
384
+ pending = self.__fused_pending
385
+ if pending is None or self.__occ is None:
386
+ self.__fused_pending = None
387
+ return
388
+ try:
389
+ release = self.__occ.release_gl_objects([pending["gl_buffer"]])
390
+ release.wait()
391
+ except Exception:
392
+ pass
393
+ self.__fused_pending = None
394
+
395
+ def close(self):
396
+ """Release CL and GL views while the owning GL context is still current."""
397
+ if self.__closed:
398
+ return
399
+ self.__closed = True
400
+
401
+ occ = self.__occ
402
+
403
+ # Teardown is not a hot path. Fully drain both APIs before destroying
404
+ # fences and shared objects so the NVIDIA/GLX lifetime ordering remains
405
+ # deterministic.
406
+ try:
407
+ if occ is not None:
408
+ occ.CL_queue.finish()
409
+ except Exception:
410
+ pass
411
+
412
+ self._release_pending_fused_vbo()
413
+
414
+ try:
415
+ glFinish()
416
+ except Exception:
417
+ pass
418
+
419
+ if self.__draw_particles is not None:
420
+ try:
421
+ self.__draw_particles.set_shared_position_draw_complete_callback(None)
422
+ self.__draw_particles.set_gpu_timing_enabled(False)
423
+ self.__draw_particles.set_shared_position_vbo(None)
424
+ except Exception:
425
+ pass
426
+
427
+ for index, fence in enumerate(self.__fences):
428
+ if fence is not None:
429
+ try:
430
+ glDeleteSync(fence)
431
+ except Exception:
432
+ pass
433
+ self.__fences[index] = None
434
+
435
+ for gl_buffer in self.__gl_buffers:
436
+ try:
437
+ release = getattr(gl_buffer, "release", None)
438
+ if release is not None:
439
+ release()
440
+ except Exception:
441
+ pass
442
+ self.__gl_buffers = []
443
+
444
+ for vbo in self.__vbos:
445
+ try:
446
+ if vbo:
447
+ glDeleteBuffers(1, [vbo])
448
+ except Exception:
449
+ pass
450
+ self.__vbos = []
451
+ self.__vbo_to_index = {}
452
+
453
+ self.__occ = None
454
+ self.__pset = None
455
+ self.__draw_particles = None
456
+
457
+ def get_vbo(self):
458
+ if not self.__vbos:
459
+ return 0
460
+ return self.__vbos[self.__active_index]
461
+
462
+ vbo = property(get_vbo)
463
+
464
+ def get_cl_buffer(self):
465
+ if not self.__gl_buffers:
466
+ return None
467
+ return self.__gl_buffers[self.__active_index]
468
+
469
+ cl_buffer = property(get_cl_buffer)
470
+
471
+ def get_copy_stats(self):
472
+ return {
473
+ "calls": self.__copy_calls,
474
+ "bytes": self.__copy_bytes,
475
+ "mirror_calls": self.__mirror_calls,
476
+ "mirror_bytes": self.__mirror_bytes,
477
+ }
478
+
479
+ copy_stats = property(get_copy_stats)
480
+
481
+ def get_last_profile(self):
482
+ """Return timings for the most recent CL/GL bridge update."""
483
+ return dict(self.__last_profile)
484
+
485
+ last_profile = property(get_last_profile)