setiastrosuitepro 1.6.1.post1__py3-none-any.whl → 1.6.4__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.

Potentially problematic release.


This version of setiastrosuitepro might be problematic. Click here for more details.

Files changed (139) hide show
  1. setiastro/images/Background_startup.jpg +0 -0
  2. setiastro/images/rotatearbitrary.png +0 -0
  3. setiastro/qml/ResourceMonitor.qml +126 -0
  4. setiastro/saspro/__main__.py +162 -25
  5. setiastro/saspro/_generated/build_info.py +2 -1
  6. setiastro/saspro/abe.py +62 -11
  7. setiastro/saspro/aberration_ai.py +3 -3
  8. setiastro/saspro/add_stars.py +5 -2
  9. setiastro/saspro/astrobin_exporter.py +3 -0
  10. setiastro/saspro/astrospike_python.py +3 -1
  11. setiastro/saspro/autostretch.py +4 -2
  12. setiastro/saspro/backgroundneutral.py +60 -9
  13. setiastro/saspro/batch_convert.py +3 -0
  14. setiastro/saspro/batch_renamer.py +3 -0
  15. setiastro/saspro/blemish_blaster.py +3 -0
  16. setiastro/saspro/blink_comparator_pro.py +474 -251
  17. setiastro/saspro/cheat_sheet.py +50 -15
  18. setiastro/saspro/clahe.py +27 -1
  19. setiastro/saspro/comet_stacking.py +103 -38
  20. setiastro/saspro/convo.py +3 -0
  21. setiastro/saspro/copyastro.py +3 -0
  22. setiastro/saspro/cosmicclarity.py +70 -45
  23. setiastro/saspro/crop_dialog_pro.py +28 -1
  24. setiastro/saspro/curve_editor_pro.py +18 -0
  25. setiastro/saspro/debayer.py +3 -0
  26. setiastro/saspro/doc_manager.py +40 -17
  27. setiastro/saspro/fitsmodifier.py +3 -0
  28. setiastro/saspro/frequency_separation.py +8 -2
  29. setiastro/saspro/function_bundle.py +18 -16
  30. setiastro/saspro/generate_translations.py +715 -1
  31. setiastro/saspro/ghs_dialog_pro.py +3 -0
  32. setiastro/saspro/graxpert.py +3 -0
  33. setiastro/saspro/gui/main_window.py +364 -92
  34. setiastro/saspro/gui/mixins/dock_mixin.py +119 -7
  35. setiastro/saspro/gui/mixins/file_mixin.py +7 -0
  36. setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
  37. setiastro/saspro/gui/mixins/menu_mixin.py +29 -0
  38. setiastro/saspro/gui/mixins/toolbar_mixin.py +33 -10
  39. setiastro/saspro/gui/statistics_dialog.py +47 -0
  40. setiastro/saspro/halobgon.py +29 -3
  41. setiastro/saspro/histogram.py +3 -0
  42. setiastro/saspro/history_explorer.py +2 -0
  43. setiastro/saspro/i18n.py +22 -10
  44. setiastro/saspro/image_combine.py +3 -0
  45. setiastro/saspro/image_peeker_pro.py +3 -0
  46. setiastro/saspro/imageops/stretch.py +5 -13
  47. setiastro/saspro/isophote.py +3 -0
  48. setiastro/saspro/legacy/numba_utils.py +64 -47
  49. setiastro/saspro/linear_fit.py +3 -0
  50. setiastro/saspro/live_stacking.py +13 -2
  51. setiastro/saspro/mask_creation.py +3 -0
  52. setiastro/saspro/mfdeconv.py +5 -0
  53. setiastro/saspro/morphology.py +30 -5
  54. setiastro/saspro/multiscale_decomp.py +713 -256
  55. setiastro/saspro/nbtorgb_stars.py +12 -2
  56. setiastro/saspro/numba_utils.py +148 -47
  57. setiastro/saspro/ops/scripts.py +77 -17
  58. setiastro/saspro/ops/settings.py +1 -43
  59. setiastro/saspro/perfect_palette_picker.py +1 -0
  60. setiastro/saspro/pixelmath.py +6 -2
  61. setiastro/saspro/plate_solver.py +1 -0
  62. setiastro/saspro/remove_green.py +18 -1
  63. setiastro/saspro/remove_stars.py +136 -162
  64. setiastro/saspro/remove_stars_preset.py +55 -13
  65. setiastro/saspro/resources.py +36 -10
  66. setiastro/saspro/rgb_combination.py +1 -0
  67. setiastro/saspro/rgbalign.py +4 -4
  68. setiastro/saspro/save_options.py +1 -0
  69. setiastro/saspro/selective_color.py +79 -20
  70. setiastro/saspro/sfcc.py +50 -8
  71. setiastro/saspro/shortcuts.py +94 -21
  72. setiastro/saspro/signature_insert.py +3 -0
  73. setiastro/saspro/stacking_suite.py +924 -446
  74. setiastro/saspro/star_alignment.py +291 -331
  75. setiastro/saspro/star_spikes.py +116 -32
  76. setiastro/saspro/star_stretch.py +38 -1
  77. setiastro/saspro/stat_stretch.py +35 -3
  78. setiastro/saspro/status_log_dock.py +1 -1
  79. setiastro/saspro/subwindow.py +63 -2
  80. setiastro/saspro/supernovaasteroidhunter.py +3 -0
  81. setiastro/saspro/swap_manager.py +77 -42
  82. setiastro/saspro/translations/all_source_strings.json +4726 -0
  83. setiastro/saspro/translations/ar_translations.py +4096 -0
  84. setiastro/saspro/translations/de_translations.py +441 -446
  85. setiastro/saspro/translations/es_translations.py +278 -32
  86. setiastro/saspro/translations/fr_translations.py +280 -32
  87. setiastro/saspro/translations/hi_translations.py +3803 -0
  88. setiastro/saspro/translations/integrate_translations.py +38 -1
  89. setiastro/saspro/translations/it_translations.py +1211 -145
  90. setiastro/saspro/translations/ja_translations.py +556 -307
  91. setiastro/saspro/translations/pt_translations.py +3316 -3322
  92. setiastro/saspro/translations/ru_translations.py +3082 -0
  93. setiastro/saspro/translations/saspro_ar.qm +0 -0
  94. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  95. setiastro/saspro/translations/saspro_de.qm +0 -0
  96. setiastro/saspro/translations/saspro_de.ts +14428 -133
  97. setiastro/saspro/translations/saspro_es.qm +0 -0
  98. setiastro/saspro/translations/saspro_es.ts +11503 -7821
  99. setiastro/saspro/translations/saspro_fr.qm +0 -0
  100. setiastro/saspro/translations/saspro_fr.ts +11168 -7812
  101. setiastro/saspro/translations/saspro_hi.qm +0 -0
  102. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  103. setiastro/saspro/translations/saspro_it.qm +0 -0
  104. setiastro/saspro/translations/saspro_it.ts +14347 -7821
  105. setiastro/saspro/translations/saspro_ja.qm +0 -0
  106. setiastro/saspro/translations/saspro_ja.ts +14860 -137
  107. setiastro/saspro/translations/saspro_pt.qm +0 -0
  108. setiastro/saspro/translations/saspro_pt.ts +14904 -137
  109. setiastro/saspro/translations/saspro_ru.qm +0 -0
  110. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  111. setiastro/saspro/translations/saspro_sw.qm +0 -0
  112. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  113. setiastro/saspro/translations/saspro_uk.qm +0 -0
  114. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  115. setiastro/saspro/translations/saspro_zh.qm +0 -0
  116. setiastro/saspro/translations/saspro_zh.ts +10581 -7812
  117. setiastro/saspro/translations/sw_translations.py +3897 -0
  118. setiastro/saspro/translations/uk_translations.py +3929 -0
  119. setiastro/saspro/translations/zh_translations.py +283 -32
  120. setiastro/saspro/versioning.py +36 -5
  121. setiastro/saspro/view_bundle.py +20 -17
  122. setiastro/saspro/wavescale_hdr.py +22 -1
  123. setiastro/saspro/wavescalede.py +23 -1
  124. setiastro/saspro/whitebalance.py +39 -3
  125. setiastro/saspro/widgets/minigame/game.js +991 -0
  126. setiastro/saspro/widgets/minigame/index.html +53 -0
  127. setiastro/saspro/widgets/minigame/style.css +241 -0
  128. setiastro/saspro/widgets/resource_monitor.py +263 -0
  129. setiastro/saspro/widgets/spinboxes.py +18 -0
  130. setiastro/saspro/widgets/wavelet_utils.py +52 -20
  131. setiastro/saspro/wimi.py +100 -80
  132. setiastro/saspro/wims.py +33 -33
  133. setiastro/saspro/window_shelf.py +2 -2
  134. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/METADATA +15 -4
  135. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/RECORD +139 -115
  136. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/WHEEL +0 -0
  137. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/entry_points.txt +0 -0
  138. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/LICENSE +0 -0
  139. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/license.txt +0 -0
@@ -0,0 +1,53 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Neon Invaders: Hyperdrive</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <div id="game-container">
11
+ <canvas id="gameCanvas"></canvas>
12
+ <div id="ui-layer">
13
+ <div id="hud" class="hidden">
14
+ <div class="hud-item">SCORE: <span id="score">0</span></div>
15
+ <div class="hud-item center">LEVEL: <span id="level">1</span></div>
16
+ <div class="hud-item right">HP: <div id="hp-bar"><div id="hp-fill"></div></div></div>
17
+ </div>
18
+
19
+ <div id="main-menu" class="screen">
20
+ <h1 class="glow-text">NEON INVADERS<br><span class="subtitle">HYPERDRIVE</span></h1>
21
+ <div class="menu-options">
22
+ <button id="btn-start" class="menu-btn">START GAME</button>
23
+ <button id="btn-levels" class="menu-btn">SELECT LEVEL</button>
24
+ <div id="controls-hint">ARROWS to Move • SPACE to Shoot</div>
25
+ </div>
26
+ </div>
27
+
28
+ <div id="level-select" class="screen hidden">
29
+ <h2>SELECT LEVEL</h2>
30
+ <div class="level-grid" id="level-grid">
31
+ <!-- Populated by JS -->
32
+ </div>
33
+ <button id="btn-back" class="menu-btn">BACK</button>
34
+ </div>
35
+
36
+ <div id="game-over" class="screen hidden">
37
+ <h1 class="danger-text">GAME OVER</h1>
38
+ <p>FINAL SCORE: <span id="final-score">0</span></p>
39
+ <button id="btn-retry" class="menu-btn">RETRY</button>
40
+ <button id="btn-menu" class="menu-btn">MAIN MENU</button>
41
+ </div>
42
+
43
+ <div id="victory" class="screen hidden">
44
+ <h1 class="victory-text">MISSION COMPLETE</h1>
45
+ <p>YOU SAVED THE GALAXY!</p>
46
+ <p>FINAL SCORE: <span id="victory-score">0</span></p>
47
+ <button id="btn-menu-win" class="menu-btn">MAIN MENU</button>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ <script src="game.js"></script>
52
+ </body>
53
+ </html>
@@ -0,0 +1,241 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap');
2
+
3
+ :root {
4
+ --neon-blue: #0ff;
5
+ --neon-pink: #f0f;
6
+ --neon-green: #0f0;
7
+ --neon-red: #f00;
8
+ --bg-color: #050510;
9
+ }
10
+
11
+ * {
12
+ box-sizing: border-box;
13
+ user-select: none;
14
+ }
15
+
16
+ body {
17
+ margin: 0;
18
+ padding: 0;
19
+ background-color: #000;
20
+ color: var(--neon-blue);
21
+ font-family: 'Orbitron', sans-serif;
22
+ overflow: hidden;
23
+ height: 100vh;
24
+ display: flex;
25
+ justify-content: center;
26
+ align-items: center;
27
+ }
28
+
29
+ #game-container {
30
+ position: relative;
31
+ width: 100%;
32
+ height: 100%;
33
+ max-width: 800px;
34
+ /* Arcade aspect ratio sort of */
35
+ max-height: 100vh;
36
+ box-shadow: 0 0 50px rgba(0, 255, 255, 0.1);
37
+ background: #000;
38
+ }
39
+
40
+ canvas {
41
+ display: block;
42
+ width: 100%;
43
+ height: 100%;
44
+ }
45
+
46
+ #ui-layer {
47
+ position: absolute;
48
+ top: 0;
49
+ left: 0;
50
+ width: 100%;
51
+ height: 100%;
52
+ pointer-events: none;
53
+ /* Let clicks pass through generally, but enable for buttons */
54
+ }
55
+
56
+ /* Screens */
57
+ .screen {
58
+ position: absolute;
59
+ top: 0;
60
+ left: 0;
61
+ width: 100%;
62
+ height: 100%;
63
+ background: rgba(0, 0, 0, 0.85);
64
+ display: flex;
65
+ flex-direction: column;
66
+ justify-content: center;
67
+ align-items: center;
68
+ text-align: center;
69
+ pointer-events: auto;
70
+ transition: opacity 0.3s;
71
+ z-index: 10;
72
+ }
73
+
74
+ .hidden {
75
+ display: none !important;
76
+ }
77
+
78
+ /* Typography */
79
+ h1 {
80
+ font-size: 3rem;
81
+ margin-bottom: 2rem;
82
+ text-shadow: 0 0 10px currentColor;
83
+ }
84
+
85
+ .subtitle {
86
+ font-size: 1.2rem;
87
+ letter-spacing: 0.5rem;
88
+ color: var(--neon-pink);
89
+ }
90
+
91
+ .glow-text {
92
+ color: var(--neon-blue);
93
+ text-shadow: 0 0 20px var(--neon-blue);
94
+ }
95
+
96
+ .danger-text {
97
+ color: var(--neon-red);
98
+ text-shadow: 0 0 20px var(--neon-red);
99
+ }
100
+
101
+ .victory-text {
102
+ color: var(--neon-green);
103
+ text-shadow: 0 0 20px var(--neon-green);
104
+ }
105
+
106
+ /* Buttons */
107
+ .menu-btn {
108
+ background: transparent;
109
+ border: 2px solid var(--neon-blue);
110
+ color: var(--neon-blue);
111
+ padding: 15px 40px;
112
+ font-size: 1.2rem;
113
+ font-family: 'Orbitron', sans-serif;
114
+ margin: 10px;
115
+ cursor: pointer;
116
+ transition: all 0.2s;
117
+ text-transform: uppercase;
118
+ box-shadow: 0 0 5px var(--neon-blue);
119
+ position: relative;
120
+ overflow: hidden;
121
+ }
122
+
123
+ .menu-btn:hover {
124
+ background: var(--neon-blue);
125
+ color: #000;
126
+ box-shadow: 0 0 20px var(--neon-blue);
127
+ transform: scale(1.05);
128
+ }
129
+
130
+ .menu-btn:active {
131
+ transform: scale(0.98);
132
+ }
133
+
134
+ /* HUD */
135
+ #hud {
136
+ position: absolute;
137
+ top: 0;
138
+ left: 0;
139
+ width: 100%;
140
+ padding: 20px;
141
+ display: flex;
142
+ justify-content: space-between;
143
+ pointer-events: none;
144
+ z-index: 5;
145
+ text-shadow: 0 0 5px currentColor;
146
+ font-weight: bold;
147
+ font-size: 1.2rem;
148
+ }
149
+
150
+ .hud-item {
151
+ flex: 1;
152
+ }
153
+
154
+ .center {
155
+ text-align: center;
156
+ }
157
+
158
+ .right {
159
+ text-align: right;
160
+ display: flex;
161
+ justify-content: flex-end;
162
+ align-items: center;
163
+ gap: 10px;
164
+ }
165
+
166
+ #hp-bar {
167
+ width: 150px;
168
+ height: 15px;
169
+ border: 2px solid var(--neon-red);
170
+ background: #300;
171
+ position: relative;
172
+ }
173
+
174
+ #hp-fill {
175
+ width: 100%;
176
+ height: 100%;
177
+ background: var(--neon-red);
178
+ box-shadow: 0 0 10px var(--neon-red);
179
+ transition: width 0.2s;
180
+ }
181
+
182
+ /* Level Grid */
183
+ .level-grid {
184
+ display: grid;
185
+ grid-template-columns: repeat(5, 1fr);
186
+ gap: 10px;
187
+ margin-bottom: 2rem;
188
+ }
189
+
190
+ .level-btn {
191
+ padding: 15px;
192
+ border: 1px solid var(--neon-pink);
193
+ color: var(--neon-pink);
194
+ background: transparent;
195
+ font-family: 'Orbitron', sans-serif;
196
+ cursor: pointer;
197
+ transition: all 0.2s;
198
+ }
199
+
200
+ .level-btn:hover:not(:disabled) {
201
+ background: var(--neon-pink);
202
+ color: #000;
203
+ box-shadow: 0 0 15px var(--neon-pink);
204
+ }
205
+
206
+ .level-btn:disabled {
207
+ opacity: 0.3;
208
+ cursor: not-allowed;
209
+ border-color: #555;
210
+ color: #555;
211
+ }
212
+
213
+ #controls-hint {
214
+ margin-top: 30px;
215
+ color: #fff;
216
+ opacity: 0.7;
217
+ font-size: 0.8rem;
218
+ }
219
+
220
+ /* Animations */
221
+ @keyframes fadeUp {
222
+ 0% {
223
+ opacity: 0;
224
+ transform: translate(-50%, 0);
225
+ }
226
+
227
+ 20% {
228
+ opacity: 1;
229
+ transform: translate(-50%, -50%);
230
+ }
231
+
232
+ 80% {
233
+ opacity: 1;
234
+ transform: translate(-50%, -50%);
235
+ }
236
+
237
+ 100% {
238
+ opacity: 0;
239
+ transform: translate(-50%, -100%);
240
+ }
241
+ }
@@ -0,0 +1,263 @@
1
+ # src/setiastro/saspro/widgets/resource_monitor.py
2
+ from __future__ import annotations
3
+ import os
4
+ import psutil
5
+ from PyQt6.QtCore import Qt, QUrl, QTimer, QObject, pyqtProperty, pyqtSignal, QThread
6
+ from PyQt6.QtWidgets import QWidget, QVBoxLayout, QFrame
7
+ from PyQt6.QtQuickWidgets import QQuickWidget
8
+
9
+ from setiastro.saspro.memory_utils import get_memory_usage_mb
10
+ from setiastro.saspro.resources import _get_base_path
11
+
12
+ class GPUWorker(QThread):
13
+ """Background worker to monitor GPU without blocking the UI."""
14
+ resultReady = pyqtSignal(float)
15
+
16
+ def __init__(self, has_nvidia: bool, parent=None):
17
+ super().__init__(parent)
18
+ self._has_nvidia = has_nvidia
19
+ self._last_val = 0.0
20
+
21
+ def _get_windows_gpu_load(self) -> float:
22
+ if os.name != 'nt':
23
+ return 0.0
24
+ try:
25
+ import subprocess
26
+ # Aggregation logic to match Task Manager:
27
+ # 1. Group by unique engine (Adapter LUID + Engine Type/Index).
28
+ # 2. Sum utilization of all processes sharing that engine.
29
+ # 3. Take the Maximum of these sums as the overall GPU load.
30
+ # Aggregation logic to match Task Manager:
31
+ # 1. Group by unique engine (Adapter LUID + Engine Type/Index).
32
+ # 2. Sum utilization of all processes sharing that engine.
33
+ # 3. Take the Maximum of these sums as the overall GPU load.
34
+ cmd = (
35
+ "powershell -NoProfile -ExecutionPolicy Bypass -Command \""
36
+ "$groups = Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine -ErrorAction SilentlyContinue | "
37
+ "Group-Object -Property { $_.Name -replace '^pid_\\d+_', '' }; "
38
+ "$res_list = $groups | ForEach-Object { ($_.Group | Measure-Object -Property UtilizationPercentage -Sum).Sum }; "
39
+ "$max_val = ($res_list | Measure-Object -Maximum).Maximum; "
40
+ "if ($max_val) { [math]::Round($max_val, 1) } else { 0 }\""
41
+ )
42
+ startupinfo = subprocess.STARTUPINFO()
43
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
44
+ startupinfo.wShowWindow = 0
45
+
46
+ out = subprocess.check_output(cmd, startupinfo=startupinfo, timeout=5.0)
47
+ val_str = out.decode("utf-8").strip()
48
+
49
+ if not val_str: return 0.0
50
+ return float(val_str.replace(",", "."))
51
+ except Exception:
52
+ return 0.0
53
+
54
+ def _get_gpu_load(self) -> float:
55
+ nv_val = 0.0
56
+ win_val = 0.0
57
+
58
+ # 1. Check NVIDIA (Discrete)
59
+ if self._has_nvidia:
60
+ try:
61
+ import subprocess
62
+ startupinfo = None
63
+ if os.name == 'nt':
64
+ startupinfo = subprocess.STARTUPINFO()
65
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
66
+ startupinfo.wShowWindow = 0
67
+
68
+ out = subprocess.check_output(
69
+ ["nvidia-smi", "--query-gpu=utilization.gpu", "--format=csv,noheader,nounits"],
70
+ startupinfo=startupinfo,
71
+ timeout=0.6
72
+ )
73
+ line = out.decode("utf-8").strip().split('\n')[0]
74
+ nv_val = float(line)
75
+ except Exception:
76
+ pass
77
+
78
+ # 2. Check Universal (Integrated)
79
+ if os.name == 'nt':
80
+ win_val = self._get_windows_gpu_load()
81
+
82
+ return max(nv_val, win_val)
83
+
84
+ def run(self):
85
+ while not self.isInterruptionRequested():
86
+ try:
87
+ val = self._get_gpu_load()
88
+ self.resultReady.emit(val)
89
+ # Sleep between measurements. 250ms as requested.
90
+ # Note: PowerShell queries might take longer than 250ms,
91
+ # but this loop will run as fast as the hardware allows without blocking UI.
92
+ self.msleep(250)
93
+ except Exception:
94
+ self.msleep(1000) # Error backoff on failure
95
+
96
+ class ResourceBackend(QObject):
97
+ """Backend logic for the QML Resource Monitor."""
98
+
99
+ cpuChanged = pyqtSignal()
100
+ ramChanged = pyqtSignal()
101
+ gpuChanged = pyqtSignal()
102
+ appRamChanged = pyqtSignal()
103
+
104
+ def __init__(self, parent=None):
105
+ super().__init__(parent)
106
+ self._cpu = 0.0
107
+ self._ram = 0.0
108
+ self._gpu = 0.0
109
+ self._app_ram_val = 0.0
110
+ self._app_ram_str = "0 MB"
111
+
112
+ # Check if nvidia-smi is reachable once
113
+ has_nvidia = False
114
+ try:
115
+ import shutil
116
+ if shutil.which("nvidia-smi"):
117
+ has_nvidia = True
118
+ except Exception:
119
+ pass
120
+
121
+ # Start Background GPU Worker
122
+ self._gpu_worker = GPUWorker(has_nvidia, self)
123
+ self._gpu_worker.resultReady.connect(self._on_gpu_measured)
124
+ self._gpu_worker.start()
125
+
126
+ # Timer for CPU/RAM updates (250ms as requested)
127
+ self._timer = QTimer(self)
128
+ self._timer.setInterval(250)
129
+ self._timer.timeout.connect(self._update_stats)
130
+ self._timer.start()
131
+
132
+ def _on_gpu_measured(self, val: float):
133
+ self._gpu = val
134
+ self.gpuChanged.emit()
135
+
136
+ @pyqtProperty(float, notify=cpuChanged)
137
+ def cpuUsage(self):
138
+ return self._cpu
139
+
140
+ @pyqtProperty(float, notify=ramChanged)
141
+ def ramUsage(self):
142
+ return self._ram
143
+
144
+ @pyqtProperty(float, notify=gpuChanged)
145
+ def gpuUsage(self):
146
+ return self._gpu
147
+
148
+ @pyqtProperty(str, notify=appRamChanged)
149
+ def appRamString(self):
150
+ return self._app_ram_str
151
+
152
+ def _update_stats(self):
153
+ # 1. CPU
154
+ try:
155
+ self._cpu = psutil.cpu_percent(interval=None)
156
+ except Exception:
157
+ self._cpu = 0.0
158
+
159
+ # 2. System RAM
160
+ try:
161
+ vm = psutil.virtual_memory()
162
+ self._ram = vm.percent
163
+ except Exception:
164
+ self._ram = 0.0
165
+
166
+ # 3. App RAM
167
+ try:
168
+ mb = get_memory_usage_mb()
169
+ self._app_ram_val = mb
170
+ self._app_ram_str = f"{int(mb)} MB"
171
+ except Exception:
172
+ self._app_ram_str = "? MB"
173
+
174
+ self.cpuChanged.emit()
175
+ self.ramChanged.emit()
176
+ self.appRamChanged.emit()
177
+
178
+ def stop(self):
179
+ """Explicitly stop background threads."""
180
+ if hasattr(self, "_gpu_worker") and self._gpu_worker.isRunning():
181
+ self._gpu_worker.requestInterruption()
182
+ self._gpu_worker.quit()
183
+ self._gpu_worker.wait(1000)
184
+
185
+ def __del__(self):
186
+ self.stop()
187
+
188
+
189
+ class SystemMonitorWidget(QQuickWidget):
190
+ """
191
+ The QQuickWidget hosting the QML content.
192
+ """
193
+ def __init__(self, parent=None):
194
+ super().__init__(parent)
195
+
196
+ self.setResizeMode(QQuickWidget.ResizeMode.SizeRootObjectToView)
197
+ self.setAttribute(Qt.WidgetAttribute.WA_AlwaysStackOnTop, False)
198
+ self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
199
+ self.setClearColor(Qt.GlobalColor.transparent)
200
+
201
+ # Connect Backend
202
+ self.backend = ResourceBackend(self)
203
+ self.rootContext().setContextProperty("backend", self.backend)
204
+
205
+ # We need to manually wire property updates because we are binding to root properties in QML
206
+ # Actually, simpler pattern: QML file reads from an object we inject.
207
+ # Let's adjust QML slightly to bind to `backend.cpuUsage` etc. if we can,
208
+ # OR we leave QML as having properties and we set them from Python.
209
+ #
210
+ # Better approach for Py+QML:
211
+ # Inject `backend` into context, modify QML to use `backend.cpuUsage`.
212
+ # But since I already wrote QML with root properties, I will just set them directly
213
+ # or update the QML file. Updating QML is cleaner.
214
+ #
215
+ # For now, let's keep QML independent and binding via setProperty?
216
+ # No, properly: context property is best.
217
+ #
218
+ # Let's re-write the QML loading part to use a safer 'initialProperties' approach or just signal/slots.
219
+ #
220
+ # EASIEST: QML binds to `root.cpuUsage`. Python sets `root.cpuUsage`.
221
+
222
+ self.backend.cpuChanged.connect(self._push_data_to_qml)
223
+ self.backend.ramChanged.connect(self._push_data_to_qml)
224
+ self.backend.gpuChanged.connect(self._push_data_to_qml)
225
+ self.backend.appRamChanged.connect(self._push_data_to_qml)
226
+
227
+ # Load QML
228
+ qml_path = os.path.join(_get_base_path(), "qml", "ResourceMonitor.qml")
229
+ self.setSource(QUrl.fromLocalFile(qml_path))
230
+
231
+ def _push_data_to_qml(self):
232
+ root = self.rootObject()
233
+ if root:
234
+ root.setProperty("cpuUsage", self.backend.cpuUsage)
235
+ root.setProperty("ramUsage", self.backend.ramUsage)
236
+ root.setProperty("gpuUsage", self.backend.gpuUsage)
237
+ root.setProperty("appRamString", self.backend.appRamString)
238
+
239
+ # --- Drag & Drop Support ---
240
+ def mousePressEvent(self, event):
241
+ if event.button() == Qt.MouseButton.LeftButton:
242
+ self._drag_start_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
243
+ event.accept()
244
+ else:
245
+ super().mousePressEvent(event)
246
+
247
+ def mouseMoveEvent(self, event):
248
+ if event.buttons() & Qt.MouseButton.LeftButton:
249
+ if hasattr(self, "_drag_start_pos"):
250
+ self.move(event.globalPosition().toPoint() - self._drag_start_pos)
251
+ event.accept()
252
+ else:
253
+ super().mouseMoveEvent(event)
254
+
255
+ def mouseReleaseEvent(self, event):
256
+ if event.button() == Qt.MouseButton.LeftButton:
257
+ from PyQt6.QtCore import QSettings
258
+ settings = QSettings("SetiAstro", "SetiAstroSuitePro")
259
+ pos = self.pos()
260
+ settings.setValue("ui/resource_monitor_pos_x", pos.x())
261
+ settings.setValue("ui/resource_monitor_pos_y", pos.y())
262
+ event.accept()
263
+ super().mouseReleaseEvent(event)
@@ -134,6 +134,15 @@ class CustomSpinBox(QWidget):
134
134
  """Decrease value by step."""
135
135
  self.setValue(self._value - self.step)
136
136
 
137
+ def value(self) -> int:
138
+ """
139
+ Qt-compatible getter (QSpinBox uses value()).
140
+
141
+ Note: we also have @property value for convenience,
142
+ but code that expects QSpinBox calls value().
143
+ """
144
+ return self._value
145
+
137
146
  def _update_button_states(self) -> None:
138
147
  """Enable/disable buttons at limits."""
139
148
  self.upButton.setEnabled(self._value < self.maximum)
@@ -228,6 +237,15 @@ class CustomDoubleSpinBox(QWidget):
228
237
  self.setValue(minimum)
229
238
  self._update_button_states()
230
239
 
240
+ def value(self) -> float:
241
+ """
242
+ Qt-compatible getter (QDoubleSpinBox uses value()).
243
+
244
+ Note: we also have @property value for convenience,
245
+ but code that expects QDoubleSpinBox calls value().
246
+ """
247
+ return self._value
248
+
231
249
  def setMaximum(self, maximum: float) -> None:
232
250
  """Set the maximum value."""
233
251
  self.maximum = maximum
@@ -39,26 +39,44 @@ def conv_sep_reflect(image2d: np.ndarray, k1d: np.ndarray, axis: int) -> np.ndar
39
39
  """
40
40
  if _HAVE_SCIPY:
41
41
  if axis == 1: # x
42
+ # Use 'reflect' mode to match original behavior
42
43
  return _nd_convolve(image2d, k1d.reshape(1, -1), mode="reflect")
43
44
  else: # y
44
45
  return _nd_convolve(image2d, k1d.reshape(-1, 1), mode="reflect")
45
46
  else:
46
- # Fallback numpy implementation
47
- image2d = np.asarray(image2d, dtype=np.float32)
48
- k1d = np.asarray(k1d, dtype=np.float32)
49
- r = len(k1d) // 2
50
- if axis == 1: # horizontal
51
- pad = np.pad(image2d, ((0, 0), (r, r)), mode="reflect")
52
- out = np.empty_like(image2d, dtype=np.float32)
53
- for i in range(image2d.shape[0]):
54
- out[i] = np.convolve(pad[i], k1d, mode="valid")
55
- return out
56
- else: # vertical
57
- pad = np.pad(image2d, ((r, r), (0, 0)), mode="reflect")
58
- out = np.empty_like(image2d, dtype=np.float32)
59
- for j in range(image2d.shape[1]):
60
- out[:, j] = np.convolve(pad[:, j], k1d, mode="valid")
61
- return out
47
+ # Optimization: Use OpenCV if available, falling back to NumPy only if strictly necessary.
48
+ # cv2.sepFilter2D can tackle 1D separable/separable passes easily,
49
+ # but here we need a specific generic 1D convolution.
50
+ # cv2.filter2D is generic 2D but fast.
51
+ try:
52
+ import cv2
53
+ # cv2.filter2D(src, ddepth, kernel, anchor, delta, borderType)
54
+ # kernel needs to be created correctly.
55
+ if axis == 1: # horizontal
56
+ k = k1d.reshape(1, -1)
57
+ else: # vertical
58
+ k = k1d.reshape(-1, 1)
59
+
60
+ # Using ddepth=-1 to keep input depth (float32)
61
+ # borderType=cv2.BORDER_REFLECT to match mode="reflect"
62
+ return cv2.filter2D(image2d, -1, k, borderType=cv2.BORDER_REFLECT)
63
+ except ImportError:
64
+ # Fallback numpy implementation (slow!)
65
+ image2d = np.asarray(image2d, dtype=np.float32)
66
+ k1d = np.asarray(k1d, dtype=np.float32)
67
+ r = len(k1d) // 2
68
+ if axis == 1: # horizontal
69
+ pad = np.pad(image2d, ((0, 0), (r, r)), mode="reflect")
70
+ out = np.empty_like(image2d, dtype=np.float32)
71
+ for i in range(image2d.shape[0]):
72
+ out[i] = np.convolve(pad[i], k1d, mode="valid")
73
+ return out
74
+ else: # vertical
75
+ pad = np.pad(image2d, ((r, r), (0, 0)), mode="reflect")
76
+ out = np.empty_like(image2d, dtype=np.float32)
77
+ for j in range(image2d.shape[1]):
78
+ out[:, j] = np.convolve(pad[:, j], k1d, mode="valid")
79
+ return out
62
80
 
63
81
 
64
82
  def gauss1d(sigma: float) -> np.ndarray:
@@ -95,10 +113,24 @@ def gauss_blur(image2d: np.ndarray, sigma: float) -> np.ndarray:
95
113
  """
96
114
  if _HAVE_SCIPY and _nd_gauss is not None:
97
115
  return _nd_gauss(image2d, sigma=sigma, mode="reflect")
98
- else:
99
- k = gauss1d(float(sigma))
100
- tmp = conv_sep_reflect(image2d, k, axis=1)
101
- return conv_sep_reflect(tmp, k, axis=0)
116
+
117
+ # Try OpenCV GaussianBlur first -> much faster
118
+ try:
119
+ import cv2
120
+ # kernel size: sigma*3 or auto (0)
121
+ # cv2 uses (ksize_w, ksize_h), ksize must be odd.
122
+ # To strictly match sigma, simple let cv2 compute it with ksize=(0,0)
123
+ # BUT 'reflect' border is not default (BORDER_DEFAULT is Reflect_101).
124
+ # mode="reflect" in scipy is usually dcb a | abcd | dc
125
+ # cv2.BORDER_REFLECT is fedcba | abcdef | gfedcb
126
+ # They are compatible enough for this application.
127
+ return cv2.GaussianBlur(image2d, (0, 0), sigmaX=sigma, sigmaY=sigma, borderType=cv2.BORDER_REFLECT)
128
+ except ImportError:
129
+ pass
130
+
131
+ k = gauss1d(float(sigma))
132
+ tmp = conv_sep_reflect(image2d, k, axis=1)
133
+ return conv_sep_reflect(tmp, k, axis=0)
102
134
 
103
135
 
104
136
  # ─────────────────────────────────────────────────────────────────────────────