hyperstellar 1.5.7__cp315-cp315-win_amd64.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.
@@ -0,0 +1,312 @@
1
+ # hyperstellar/src/hyperstellar/__init__.py
2
+
3
+
4
+ # start delvewheel patch
5
+ def _delvewheel_patch_1_13_0():
6
+ import os
7
+ if os.path.isdir(libs_dir := os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'hyperstellar.libs'))):
8
+ os.add_dll_directory(libs_dir)
9
+
10
+
11
+ _delvewheel_patch_1_13_0()
12
+ del _delvewheel_patch_1_13_0
13
+ # end delvewheel patch
14
+
15
+ import os
16
+ import sys
17
+ import time
18
+ import platform
19
+ from pathlib import Path
20
+ from typing import List, Dict, Any, Iterator, Optional, Union
21
+
22
+ # Import the decorator function from jit.py (module-level)
23
+ from .jit import script as _script_func
24
+
25
+ __version__ = "1.5.7"
26
+
27
+ # Platform detection
28
+ system = platform.system().lower()
29
+ arch = platform.machine().lower()
30
+
31
+ if system == "windows" and ("amd64" in arch or "x86_64" in arch):
32
+ platform_dir = "windows-x64"
33
+ lib_name = "stellar.pyd"
34
+ elif system == "linux" and ("x86_64" in arch or "amd64" in arch):
35
+ platform_dir = "linux-x64"
36
+ lib_name = "stellar.so"
37
+ elif system == "darwin":
38
+ platform_dir = "macos-x64"
39
+ lib_name = "stellar.so"
40
+ else:
41
+ raise ImportError(f"Unsupported platform: {system} {arch}")
42
+
43
+ # Native module path
44
+ module_dir = Path(__file__).parent / "_native" / platform_dir
45
+ module_path = module_dir / lib_name
46
+
47
+ if not module_path.exists():
48
+ raise ImportError(
49
+ f"Native module not found: {module_path}\n"
50
+ f"Platform detected: {system} {arch}\n"
51
+ f"Expected: {platform_dir}/{lib_name}"
52
+ )
53
+
54
+ # Windows: help loader find DLL dependencies
55
+ if system == "windows":
56
+ dll_dir = str(module_dir)
57
+ os.environ['PATH'] = dll_dir + ';' + os.environ.get('PATH', '')
58
+ try:
59
+ os.add_dll_directory(dll_dir)
60
+ except Exception:
61
+ pass
62
+
63
+ # ----------------------------------------------------------------------
64
+ # Determine if shader cache already exists
65
+ # ----------------------------------------------------------------------
66
+ cache_dir = None
67
+ if system == "windows":
68
+ local_appdata = os.environ.get("LOCALAPPDATA")
69
+ if local_appdata:
70
+ cache_dir = Path(local_appdata) / "hyperstellar" / "cache"
71
+ else: # Linux/macOS
72
+ home = os.environ.get("HOME")
73
+ if home:
74
+ cache_dir = Path(home) / ".cache" / "hyperstellar"
75
+
76
+ has_cache = False
77
+ if cache_dir and cache_dir.exists():
78
+ has_cache = any(cache_dir.glob("*.bin"))
79
+
80
+ if not has_cache:
81
+ sys.stderr.write(
82
+ f"hyperstellar {__version__}: First-time shader compilation may take 30-60 seconds on older GPUs.\n"
83
+ f" Subsequent runs will be instant (binary cache).\n"
84
+ f"Compiling... "
85
+ )
86
+ sys.stderr.flush()
87
+ else:
88
+ sys.stderr.write(f"hyperstellar {__version__}: Loading cached shaders... ")
89
+ sys.stderr.flush()
90
+
91
+ start_time = time.perf_counter()
92
+
93
+ # Import native module
94
+ try:
95
+ sys.modules.pop('stellar', None)
96
+ sys.path.insert(0, str(module_dir))
97
+ import stellar as _stellar_module
98
+ sys.path.pop(0)
99
+
100
+ # Expose everything from the native module
101
+ for attr_name in dir(_stellar_module):
102
+ if not attr_name.startswith('__'):
103
+ globals()[attr_name] = getattr(_stellar_module, attr_name)
104
+
105
+ # Define __all__
106
+ if hasattr(_stellar_module, '__all__'):
107
+ __all__ = _stellar_module.__all__ + ['script', 'EquationPresets']
108
+ else:
109
+ __all__ = ['script', 'EquationPresets']
110
+
111
+ # Attach @sim.script decorator to the Simulation class
112
+ def _script_method(self, mode='object', debug=False):
113
+ def decorator(func):
114
+ return _script_func(self, debug=debug, mode=mode)(func)
115
+ return decorator
116
+
117
+ Simulation.script = _script_method
118
+
119
+ # =====================================================================
120
+ # ITERATION PROTOCOL
121
+ # =====================================================================
122
+ def _sim_iter(self):
123
+ for i in range(self.object_count()):
124
+ yield self.get_object(i)
125
+
126
+ Simulation.__iter__ = _sim_iter
127
+ Simulation.__len__ = lambda self: self.object_count()
128
+
129
+ # =====================================================================
130
+ # CONTEXT MANAGER
131
+ # =====================================================================
132
+ def _sim_enter(self):
133
+ return self
134
+
135
+ def _sim_exit(self, exc_type, exc_val, exc_tb):
136
+ self.cleanup()
137
+ return False
138
+
139
+ Simulation.__enter__ = _sim_enter
140
+ Simulation.__exit__ = _sim_exit
141
+
142
+ # =====================================================================
143
+ # DUPLICATE_OBJECT (uses native get_all_objects, not overwritten)
144
+ # =====================================================================
145
+ def _duplicate_object(self, obj: Union[int, 'ObjectHandle'], **overrides):
146
+ """
147
+ Duplicate an existing object with optional property overrides.
148
+
149
+ Args:
150
+ obj: Object ID (raw index or ObjectHandle)
151
+ **overrides: Any property to override (x, y, vx, vy, mass, etc.)
152
+
153
+ Returns:
154
+ ObjectHandle: Handle to the new object
155
+ """
156
+ state = self.get_object(obj) # accepts both raw int and handle
157
+ kwargs = {
158
+ 'x': state.x, 'y': state.y,
159
+ 'vx': state.vx, 'vy': state.vy,
160
+ 'mass': state.mass, 'charge': state.charge,
161
+ 'rotation': state.rotation,
162
+ 'angular_velocity': state.angular_velocity,
163
+ 'skin': state.skin_type,
164
+ 'size': state.radius,
165
+ 'width': state.width, 'height': state.height,
166
+ 'r': state.r, 'g': state.g, 'b': state.b, 'a': state.a,
167
+ 'polygon_sides': state.polygon_sides
168
+ }
169
+ kwargs.update(overrides)
170
+ return self.add_object(**kwargs)
171
+
172
+ Simulation.duplicate_object = _duplicate_object
173
+
174
+ # =====================================================================
175
+ # EQUATION PRESETS
176
+ # =====================================================================
177
+ class EquationPresets:
178
+ @staticmethod
179
+ def gravity(G=1.0, eps=0.001):
180
+ return f"ax = -{G} * x / (x*x + y*y + {eps})**1.5; ay = -{G} * y / (x*x + y*y + {eps})**1.5"
181
+
182
+ @staticmethod
183
+ def spring(k=1.0):
184
+ return f"ax = -{k} * x; ay = -{k} * y"
185
+
186
+ @staticmethod
187
+ def damped_spring(k=1.0, damping=0.1):
188
+ return f"ax = -{k} * x - {damping} * vx; ay = -{k} * y - {damping} * vy"
189
+
190
+ @staticmethod
191
+ def orbit(G=1.0, eps=0.001):
192
+ # Note: this function does NOT accept a 'target' argument; target is handled in set_equation_named.
193
+ return (f"dx = target.x - x; dy = target.y - y; r2 = dx*dx + dy*dy + {eps}; "
194
+ f"r = sqrt(r2); ax = {G} * target.mass * dx / (r * r2); ay = {G} * target.mass * dy / (r * r2)")
195
+
196
+ globals()['EquationPresets'] = EquationPresets
197
+
198
+ # =====================================================================
199
+ # OBJECT TAGS/GROUPS (handle‑only, no raw indices)
200
+ # =====================================================================
201
+ class _ObjectTags:
202
+ """
203
+ Lightweight Python-side object tagging system.
204
+ Only ObjectHandles should be stored – raw indices are not accepted.
205
+ """
206
+ def __init__(self, sim):
207
+ self.sim = sim
208
+ self._tags: Dict[str, List['ObjectHandle']] = {}
209
+
210
+ def add(self, tag: str, obj: 'ObjectHandle'):
211
+ if not isinstance(obj, ObjectHandle):
212
+ raise TypeError("Only ObjectHandle can be tagged; use add_object() to get a handle.")
213
+ if tag not in self._tags:
214
+ self._tags[tag] = []
215
+ if obj not in self._tags[tag]:
216
+ self._tags[tag].append(obj)
217
+
218
+ def remove(self, tag: str, obj: 'ObjectHandle'):
219
+ if tag in self._tags and obj in self._tags[tag]:
220
+ self._tags[tag].remove(obj)
221
+ if not self._tags[tag]:
222
+ del self._tags[tag]
223
+
224
+ def get(self, tag: str) -> List['ObjectHandle']:
225
+ return self._tags.get(tag, []).copy()
226
+
227
+ def clear(self, tag: str = None):
228
+ if tag is None:
229
+ self._tags.clear()
230
+ elif tag in self._tags:
231
+ del self._tags[tag]
232
+
233
+ def __contains__(self, tag: str) -> bool:
234
+ return tag in self._tags
235
+
236
+ def __iter__(self):
237
+ return iter(self._tags.items())
238
+
239
+ def __repr__(self):
240
+ return f"<ObjectTags tags={list(self._tags.keys())}>"
241
+
242
+ def _get_tags(self):
243
+ if not hasattr(self, '_tags_storage'):
244
+ self._tags_storage = _ObjectTags(self)
245
+ return self._tags_storage
246
+
247
+ Simulation.tags = property(_get_tags)
248
+
249
+ # =====================================================================
250
+ # REMOVE TAGGED
251
+ # =====================================================================
252
+ def _remove_tagged(self, tag: str):
253
+ if tag in self.tags:
254
+ handles = self.tags.get(tag)
255
+ if handles:
256
+ self.remove_objects(handles)
257
+ self.tags.clear(tag)
258
+
259
+ Simulation.remove_tagged = _remove_tagged
260
+
261
+ # =====================================================================
262
+ # NAMED EQUATION SETTER (with proper target handling for orbit)
263
+ # =====================================================================
264
+ def _set_equation_named(self, obj, preset_name: str, **kwargs):
265
+ """
266
+ Set a physics equation using a named preset.
267
+
268
+ Args:
269
+ obj: Object ID or ObjectHandle
270
+ preset_name (str): "gravity", "spring", "damped_spring", or "orbit"
271
+ **kwargs: Parameters to substitute. For orbit, pass `target=other_handle`.
272
+ """
273
+ # Extract target early for orbit, since orbit's function signature doesn't accept it.
274
+ target = kwargs.pop('target', None) if preset_name == 'orbit' else None
275
+
276
+ if preset_name == "gravity":
277
+ base = EquationPresets.gravity(**kwargs)
278
+ elif preset_name == "spring":
279
+ base = EquationPresets.spring(**kwargs)
280
+ elif preset_name == "damped_spring":
281
+ base = EquationPresets.damped_spring(**kwargs)
282
+ elif preset_name == "orbit":
283
+ base = EquationPresets.orbit(**kwargs)
284
+ if target is not None:
285
+ # target can be a raw int or a handle; convert to p[index] syntax.
286
+ if hasattr(target, 'slot'): # ObjectHandle
287
+ idx = target.slot
288
+ else:
289
+ idx = int(target) # raw index
290
+ base = base.replace('target', f'p[{idx}]')
291
+ else:
292
+ raise ValueError("orbit preset requires a 'target' object (pass target=handle)")
293
+ else:
294
+ raise ValueError(f"Unknown preset: {preset_name}. Available: gravity, spring, damped_spring, orbit")
295
+
296
+ self.set_equation(obj, base)
297
+
298
+ Simulation.set_equation_named = _set_equation_named
299
+
300
+ except ImportError as e:
301
+ raise ImportError(
302
+ f"Failed to load native module: {e}\n"
303
+ f"Module path: {module_path}\n"
304
+ f"Platform: {system} {arch}"
305
+ )
306
+ except Exception as e:
307
+ raise ImportError(f"Unexpected error loading hyperstellar: {e}")
308
+
309
+ # Print completion time
310
+ elapsed = time.perf_counter() - start_time
311
+ sys.stderr.write(f" done in {elapsed:.1f}s.\n")
312
+ sys.stderr.flush()