advisor-scattering 0.5.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 (69) hide show
  1. advisor/__init__.py +3 -0
  2. advisor/__main__.py +7 -0
  3. advisor/app.py +40 -0
  4. advisor/controllers/__init__.py +6 -0
  5. advisor/controllers/app_controller.py +69 -0
  6. advisor/controllers/feature_controller.py +25 -0
  7. advisor/domain/__init__.py +23 -0
  8. advisor/domain/core/__init__.py +8 -0
  9. advisor/domain/core/lab.py +121 -0
  10. advisor/domain/core/lattice.py +79 -0
  11. advisor/domain/core/sample.py +101 -0
  12. advisor/domain/geometry.py +212 -0
  13. advisor/domain/unit_converter.py +82 -0
  14. advisor/features/__init__.py +6 -0
  15. advisor/features/scattering_geometry/controllers/__init__.py +5 -0
  16. advisor/features/scattering_geometry/controllers/scattering_geometry_controller.py +26 -0
  17. advisor/features/scattering_geometry/domain/__init__.py +5 -0
  18. advisor/features/scattering_geometry/domain/brillouin_calculator.py +410 -0
  19. advisor/features/scattering_geometry/domain/core.py +516 -0
  20. advisor/features/scattering_geometry/ui/__init__.py +5 -0
  21. advisor/features/scattering_geometry/ui/components/__init__.py +17 -0
  22. advisor/features/scattering_geometry/ui/components/angles_to_hkl_components.py +150 -0
  23. advisor/features/scattering_geometry/ui/components/hk_angles_components.py +430 -0
  24. advisor/features/scattering_geometry/ui/components/hkl_scan_components.py +526 -0
  25. advisor/features/scattering_geometry/ui/components/hkl_to_angles_components.py +315 -0
  26. advisor/features/scattering_geometry/ui/scattering_geometry_tab.py +725 -0
  27. advisor/features/structure_factor/controllers/__init__.py +6 -0
  28. advisor/features/structure_factor/controllers/structure_factor_controller.py +25 -0
  29. advisor/features/structure_factor/domain/__init__.py +6 -0
  30. advisor/features/structure_factor/domain/structure_factor_calculator.py +107 -0
  31. advisor/features/structure_factor/ui/__init__.py +6 -0
  32. advisor/features/structure_factor/ui/components/__init__.py +12 -0
  33. advisor/features/structure_factor/ui/components/customized_plane_components.py +358 -0
  34. advisor/features/structure_factor/ui/components/hkl_plane_components.py +391 -0
  35. advisor/features/structure_factor/ui/structure_factor_tab.py +273 -0
  36. advisor/resources/__init__.py +0 -0
  37. advisor/resources/config/app_config.json +14 -0
  38. advisor/resources/config/tips.json +4 -0
  39. advisor/resources/data/nacl.cif +111 -0
  40. advisor/resources/icons/bz_caculator.jpg +0 -0
  41. advisor/resources/icons/bz_calculator.png +0 -0
  42. advisor/resources/icons/minus.svg +3 -0
  43. advisor/resources/icons/placeholder.png +0 -0
  44. advisor/resources/icons/plus.svg +3 -0
  45. advisor/resources/icons/reset.png +0 -0
  46. advisor/resources/icons/sf_calculator.jpg +0 -0
  47. advisor/resources/icons/sf_calculator.png +0 -0
  48. advisor/resources/icons.qrc +6 -0
  49. advisor/resources/qss/styles.qss +348 -0
  50. advisor/resources/resources_rc.py +83 -0
  51. advisor/ui/__init__.py +7 -0
  52. advisor/ui/init_window.py +566 -0
  53. advisor/ui/main_window.py +174 -0
  54. advisor/ui/tab_interface.py +44 -0
  55. advisor/ui/tips.py +30 -0
  56. advisor/ui/utils/__init__.py +6 -0
  57. advisor/ui/utils/readcif.py +129 -0
  58. advisor/ui/visualizers/HKLScan2DVisualizer.py +224 -0
  59. advisor/ui/visualizers/__init__.py +8 -0
  60. advisor/ui/visualizers/coordinate_visualizer.py +203 -0
  61. advisor/ui/visualizers/scattering_visualizer.py +301 -0
  62. advisor/ui/visualizers/structure_factor_visualizer.py +426 -0
  63. advisor/ui/visualizers/structure_factor_visualizer_2d.py +235 -0
  64. advisor/ui/visualizers/unitcell_visualizer.py +518 -0
  65. advisor_scattering-0.5.0.dist-info/METADATA +122 -0
  66. advisor_scattering-0.5.0.dist-info/RECORD +69 -0
  67. advisor_scattering-0.5.0.dist-info/WHEEL +5 -0
  68. advisor_scattering-0.5.0.dist-info/entry_points.txt +3 -0
  69. advisor_scattering-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,426 @@
1
+ """Structure Factor Visualizer for 3D HKL space plotting."""
2
+
3
+ import numpy as np
4
+ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
5
+ from matplotlib.figure import Figure
6
+
7
+
8
+ class StructureFactorVisualizer3D(FigureCanvas):
9
+ """3D visualizer for structure factors in reciprocal space"""
10
+
11
+ def __init__(self, width=8, height=6, dpi=100):
12
+ """Initialize the 3D structure factor visualizer
13
+
14
+ Args:
15
+ width: Figure width in inches
16
+ height: Figure height in inches
17
+ dpi: Figure resolution
18
+ """
19
+ # Create figure with proper backend configuration
20
+ self.fig = Figure(figsize=(width, height), dpi=dpi, tight_layout=True)
21
+ super().__init__(self.fig)
22
+
23
+ # Initialize state
24
+ self._initialized = False
25
+ self._colorbar = None
26
+
27
+ # Create 3D subplot
28
+ self._create_3d_plot()
29
+
30
+ # Plane highlight state
31
+ self._active_plane = None # one of {"H","K","L"} or None
32
+ self._plane_alpha_active = 0.85
33
+ self._plane_alpha_inactive = 0.4
34
+ self._H_val = None
35
+ self._K_val = None
36
+ self._L_val = None
37
+
38
+ def _create_3d_plot(self):
39
+ """Create the 3D subplot and set up basic appearance"""
40
+ try:
41
+ # Clear any existing subplots
42
+ self.fig.clear()
43
+
44
+ # Create 3D subplot
45
+ self.axes = self.fig.add_subplot(111, projection="3d")
46
+ # Containers for plane artists
47
+ self._plane_h = None
48
+ self._plane_k = None
49
+ self._plane_l = None
50
+ self._plane_custom = None
51
+
52
+ # Set basic labels and title
53
+ self.axes.set_xlabel("H (r.l.u.)", fontsize=10)
54
+ self.axes.set_ylabel("K (r.l.u.)", fontsize=10)
55
+ self.axes.set_zlabel("L (r.l.u.)", fontsize=10)
56
+ #self.axes.set_title("Structure Factors in Reciprocal Space", fontsize=12)
57
+
58
+ # Set default axis limits (0, 5) with ±0.5 padding
59
+ self.axes.set_xlim(-0.5, 5.5)
60
+ self.axes.set_ylim(-0.5, 5.5)
61
+ self.axes.set_zlim(-0.5, 5.5)
62
+
63
+ # Set integer ticks only
64
+ from matplotlib.ticker import MaxNLocator
65
+
66
+ self.axes.xaxis.set_major_locator(MaxNLocator(integer=True))
67
+ self.axes.yaxis.set_major_locator(MaxNLocator(integer=True))
68
+ self.axes.zaxis.set_major_locator(MaxNLocator(integer=True))
69
+
70
+ # Set background color
71
+ self.axes.xaxis.pane.fill = False
72
+ self.axes.yaxis.pane.fill = False
73
+ self.axes.zaxis.pane.fill = False
74
+
75
+ # Make grid lines less prominent
76
+ self.axes.grid(True, alpha=0.3)
77
+
78
+ # Set default viewing angle (rotate 180° around Z compared to prior)
79
+ self.axes.view_init(elev=12.5, azim=200)
80
+
81
+ except Exception as e:
82
+ print(f"Error creating 3D plot: {e}")
83
+
84
+ def initialize(self, params: dict = None):
85
+ """Initialize the visualizer
86
+
87
+ Args:
88
+ params: Optional parameters (not used for structure factor visualization)
89
+ """
90
+ self._initialized = True
91
+ self._create_3d_plot()
92
+ self.draw()
93
+ return True
94
+
95
+ # --- Plane overlay API ---
96
+ def set_plane_values(self, H=None, K=None, L=None):
97
+ """Update translucent planes at constant H, K, L. Any None leaves it unchanged.
98
+
99
+ Args:
100
+ H: fixed H value for K-L plane (x = H)
101
+ K: fixed K value for H-L plane (y = K)
102
+ L: fixed L value for H-K plane (z = L)
103
+ """
104
+ try:
105
+ ax = self.axes
106
+ # Determine display extents from current limits
107
+ x_min, x_max = ax.get_xlim()
108
+ y_min, y_max = ax.get_ylim()
109
+ z_min, z_max = ax.get_zlim()
110
+
111
+ import numpy as np
112
+
113
+ # Helper to draw/update a plane
114
+ def draw_plane(current, new_artist_name, const_val, axis: str):
115
+ if const_val is None:
116
+ return current
117
+ # Remove existing
118
+ if current is not None:
119
+ try:
120
+ current.remove()
121
+ except Exception:
122
+ pass
123
+ # Choose alpha based on active plane selection
124
+ alpha = (
125
+ self._plane_alpha_active
126
+ if (self._active_plane is not None and axis == self._active_plane)
127
+ else self._plane_alpha_inactive
128
+ )
129
+ color = {
130
+ "H": (1.0, 0.8, 0.86, alpha), # pinkish
131
+ "K": (0.80, 0.92, 0.773, alpha), # greenish
132
+ "L": (0.635, 0.812, 0.996, alpha), # bluish
133
+ }[axis]
134
+
135
+ if axis == "H":
136
+ X = np.full((2, 2), const_val)
137
+ Y = np.array([[y_min, y_min], [y_max, y_max]])
138
+ Z = np.array([[z_min, z_max], [z_min, z_max]])
139
+ elif axis == "K":
140
+ X = np.array([[x_min, x_min], [x_max, x_max]])
141
+ Y = np.full((2, 2), const_val)
142
+ Z = np.array([[z_min, z_max], [z_min, z_max]])
143
+ else: # L
144
+ X = np.array([[x_min, x_max], [x_min, x_max]])
145
+ Y = np.array([[y_min, y_min], [y_max, y_max]])
146
+ Z = np.full((2, 2), const_val)
147
+
148
+ artist = ax.plot_surface(X, Y, Z, color=color, linewidth=0, shade=False)
149
+ return artist
150
+
151
+ self._plane_h = draw_plane(self._plane_h, "_plane_h", H, "H")
152
+ self._plane_k = draw_plane(self._plane_k, "_plane_k", K, "K")
153
+ self._plane_l = draw_plane(self._plane_l, "_plane_l", L, "L")
154
+
155
+ # Store current plane constants for future redraws
156
+ if H is not None:
157
+ self._H_val = H
158
+ if K is not None:
159
+ self._K_val = K
160
+ if L is not None:
161
+ self._L_val = L
162
+
163
+ self.draw()
164
+ except Exception as e:
165
+ print(f"Error updating planes: {e}")
166
+
167
+ def is_initialized(self):
168
+ """Check if the visualizer is initialized"""
169
+ return self._initialized
170
+
171
+ def set_active_plane(
172
+ self, axis: str, alpha_active: float = None, alpha_inactive: float = None
173
+ ):
174
+ """Highlight one of the planes by adjusting alpha values.
175
+
176
+ Args:
177
+ axis: one of "H", "K", or "L"
178
+ alpha_active: optional override for active plane alpha
179
+ alpha_inactive: optional override for inactive plane alpha
180
+ """
181
+ try:
182
+ axis = (axis or "").upper()
183
+ if axis not in {"H", "K", "L"}:
184
+ return
185
+ if alpha_active is not None:
186
+ self._plane_alpha_active = float(alpha_active)
187
+ if alpha_inactive is not None:
188
+ self._plane_alpha_inactive = float(alpha_inactive)
189
+ self._active_plane = axis
190
+ # Redraw existing planes to apply new alphas
191
+ # Re-issue current values for any planes we know
192
+ if self._H_val is not None:
193
+ self.set_plane_values(H=self._H_val)
194
+ if self._K_val is not None:
195
+ self.set_plane_values(K=self._K_val)
196
+ if self._L_val is not None:
197
+ self.set_plane_values(L=self._L_val)
198
+ except Exception as e:
199
+ print(f"Error setting active plane: {e}")
200
+
201
+ # --- Custom plane overlay (spanned by two vectors) ---
202
+ def set_custom_plane(
203
+ self,
204
+ vector_u: tuple,
205
+ vector_v: tuple,
206
+ u_min: float = -5.0,
207
+ u_max: float = 5.0,
208
+ v_min: float = -5.0,
209
+ v_max: float = 5.0,
210
+ steps: int = 2,
211
+ center: tuple = (0, 0, 0),
212
+ ):
213
+ """Overlay a translucent plane spanned by two vectors in HKL space.
214
+
215
+ Args:
216
+ vector_u: (h, k, l) tuple for the first spanning vector
217
+ vector_v: (h, k, l) tuple for the second spanning vector
218
+ u_min/u_max: parameter range along vector_u
219
+ v_min/v_max: parameter range along vector_v
220
+ steps: grid resolution along each parameter (2 draws a quad)
221
+ center: (h, k, l) tuple shifting the plane center in HKL space
222
+ """
223
+ try:
224
+ # Remove existing custom plane
225
+ if self._plane_custom is not None:
226
+ try:
227
+ self._plane_custom.remove()
228
+ except Exception:
229
+ pass
230
+ self._plane_custom = None
231
+
232
+ # Build parametric grid
233
+ import numpy as np
234
+
235
+ u_vals = np.linspace(u_min, u_max, max(2, int(steps)))
236
+ v_vals = np.linspace(v_min, v_max, max(2, int(steps)))
237
+ U, V = np.meshgrid(u_vals, v_vals)
238
+
239
+ uh, uk, ul = vector_u
240
+ vh, vk, vl = vector_v
241
+
242
+ ch, ck, cl = center
243
+
244
+ X = ch + (U * uh + V * vh)
245
+ Y = ck + (U * uk + V * vk)
246
+ Z = cl + (U * ul + V * vl)
247
+
248
+ color = (0.9, 0.9, 0.2, 0.35) # yellowish translucent
249
+ self._plane_custom = self.axes.plot_surface(
250
+ X, Y, Z, color=color, linewidth=1, shade=False
251
+ )
252
+ self.draw()
253
+ except Exception as e:
254
+ print(f"Error drawing custom plane: {e}")
255
+
256
+ def visualize_structure_factors(self, hkl_list, sf_values):
257
+ """Visualize structure factors as 3D scatter plot
258
+
259
+ Args:
260
+ hkl_list: List of [h, k, l] indices, shape (N, 3)
261
+ sf_values: Array of structure factor magnitudes, shape (N,)
262
+ """
263
+ try:
264
+ # Validate inputs
265
+ if len(hkl_list) == 0 or len(sf_values) == 0:
266
+ print("Warning: Empty HKL list or structure factor values")
267
+ return False
268
+
269
+ if len(hkl_list) != len(sf_values):
270
+ print(
271
+ f"Error: HKL list length ({len(hkl_list)}) != SF values length ({len(sf_values)})"
272
+ )
273
+ return False
274
+
275
+ # Remove existing colorbar if present
276
+ if self._colorbar is not None:
277
+ self._colorbar.remove()
278
+ self._colorbar = None
279
+
280
+ # Clear and recreate the 3D subplot
281
+ self.fig.clear()
282
+ self.axes = self.fig.add_subplot(111, projection="3d")
283
+
284
+ # Convert to numpy arrays for easier handling
285
+ hkl_array = np.array(hkl_list)
286
+ sf_array = np.array(sf_values)
287
+
288
+ # Extract H, K, L coordinates
289
+ h_coords = hkl_array[:, 0]
290
+ k_coords = hkl_array[:, 1]
291
+ l_coords = hkl_array[:, 2]
292
+
293
+ # Calculate dot sizes based on structure factor magnitude
294
+ # Scale factors for visibility
295
+ min_size = 0 # Increased minimum dot size for better visibility
296
+ max_size = 300 # Increased maximum dot size
297
+
298
+ print(f"SF array max: {sf_array.max()}, min: {sf_array.min()}")
299
+
300
+ # Normalize structure factor values for sizing
301
+ if sf_array.max() > 0:
302
+ normalized_sf = sf_array / sf_array.max()
303
+ dot_sizes = min_size + (max_size - min_size) * normalized_sf
304
+ else:
305
+ dot_sizes = np.full(len(sf_array), min_size)
306
+
307
+ mask = dot_sizes < 1
308
+ h_coords_plot = h_coords[~mask]
309
+ k_coords_plot = k_coords[~mask]
310
+ l_coords_plot = l_coords[~mask]
311
+ sf_array_plot = sf_array[~mask]
312
+ dot_sizes_plot = dot_sizes[~mask]
313
+ # Create scatter plot
314
+ scatter = self.axes.scatter(
315
+ h_coords_plot,
316
+ k_coords_plot,
317
+ l_coords_plot,
318
+ s=dot_sizes_plot,
319
+ c=sf_array_plot,
320
+ cmap="viridis",
321
+ alpha=0.9, # More opaque for better visibility
322
+ )
323
+
324
+ print(f"Created scatter plot with {len(h_coords)} points")
325
+
326
+ # Add text labels next to each dot
327
+ is_label_visible = False
328
+ if is_label_visible:
329
+ for i, (h, k, l) in enumerate(zip(h_coords, k_coords, l_coords)):
330
+ if mask[i]:
331
+ color_font = "silver"
332
+ else:
333
+ color_font = "black"
334
+ # Create label text (e.g., "010" for [0,1,0])
335
+ label = f"{h}{k}{l}"
336
+
337
+ # Add text annotation with slight offset
338
+ self.axes.text(
339
+ h + 0.1,
340
+ k + 0.1,
341
+ l + 0.1, # Small offset from dot position
342
+ label,
343
+ fontsize=8,
344
+ ha="left",
345
+ va="bottom",
346
+ color=color_font,
347
+ )
348
+
349
+ print(f"Added text labels for {len(hkl_list)} points")
350
+
351
+ # Add colorbar
352
+ self._colorbar = self.fig.colorbar(
353
+ scatter, ax=self.axes, label="|Structure Factor|", shrink=0.6, pad=0.1
354
+ )
355
+
356
+ # Set axis labels and title
357
+ self.axes.set_xlabel("H (r.l.u.)", fontsize=10)
358
+ self.axes.set_ylabel("K (r.l.u.)", fontsize=10)
359
+ self.axes.set_zlabel("L (r.l.u.)", fontsize=10)
360
+ self.axes.set_title("Structure Factors in Reciprocal Space", fontsize=12)
361
+
362
+ # Set axis limits with default range (0, 5) and adjust if needed
363
+ default_max = 5.5
364
+
365
+ # Calculate required ranges based on data with ±0.5 margin
366
+ default_min = -0.5
367
+ h_min = min(h_coords.min(), 0) - 0.5
368
+ k_min = min(k_coords.min(), 0) - 0.5
369
+ l_min = min(l_coords.min(), 0) - 0.5
370
+ h_max = max(h_coords.max() + 0.5, default_max)
371
+ k_max = max(k_coords.max() + 0.5, default_max)
372
+ l_max = max(l_coords.max() + 0.5, default_max)
373
+
374
+ # Set limits from calculated min to max
375
+ self.axes.set_xlim(h_min if not np.isnan(h_min) else default_min, h_max)
376
+ self.axes.set_ylim(k_min if not np.isnan(k_min) else default_min, k_max)
377
+ self.axes.set_zlim(l_min if not np.isnan(l_min) else default_min, l_max)
378
+
379
+ # Set integer ticks only
380
+ from matplotlib.ticker import MaxNLocator
381
+
382
+ self.axes.xaxis.set_major_locator(MaxNLocator(integer=True))
383
+ self.axes.yaxis.set_major_locator(MaxNLocator(integer=True))
384
+ self.axes.zaxis.set_major_locator(MaxNLocator(integer=True))
385
+
386
+ print(f"Set axis limits: X(0, {h_max}), Y(0, {k_max}), Z(0, {l_max})")
387
+
388
+ # Improve 3D viewing angle (rotated 180° around Z)
389
+ self.axes.view_init(elev=12.5, azim=200)
390
+
391
+ # Enable grid with low alpha
392
+ self.axes.grid(True, alpha=0.3)
393
+
394
+ # Update the canvas
395
+ self.draw()
396
+
397
+ print(f"Successfully plotted {len(hkl_list)} structure factors")
398
+ return True
399
+
400
+ except Exception as e:
401
+ print(f"Error in visualize_structure_factors: {e}")
402
+ return False
403
+
404
+ def clear_plot(self):
405
+ """Clear the current plot"""
406
+ try:
407
+ # Remove existing colorbar if present
408
+ if self._colorbar is not None:
409
+ self._colorbar.remove()
410
+ self._colorbar = None
411
+
412
+ # Recreate the 3D plot
413
+ self._create_3d_plot()
414
+ self.draw()
415
+ except Exception as e:
416
+ print(f"Error clearing plot: {e}")
417
+
418
+ def visualize(self):
419
+ """Create an empty visualization (placeholder)"""
420
+ try:
421
+ self._create_3d_plot()
422
+ self.draw()
423
+ return True
424
+ except Exception as e:
425
+ print(f"Error in visualize: {e}")
426
+ return False
@@ -0,0 +1,235 @@
1
+ """2D Structure Factor Visualizer for sliced HK/HL/KL planes"""
2
+
3
+ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
4
+ from matplotlib.figure import Figure
5
+ import numpy as np
6
+
7
+
8
+ class StructureFactorVisualizer2D(FigureCanvas):
9
+ """2D visualizer that shows a sliced plane of HKL with fixed index.
10
+
11
+ The canvas renders a 2D scatter where marker size and color scale with
12
+ the magnitude of the structure factor.
13
+ """
14
+
15
+ def __init__(self, width: float = 5.0, height: float = 4.0, dpi: int = 100):
16
+ self.fig = Figure(figsize=(float(width), float(height)), dpi=int(dpi), tight_layout=True)
17
+ super().__init__(self.fig)
18
+ self.axes = self.fig.add_subplot(111)
19
+ self._colorbar = None
20
+ self._initialized = True
21
+
22
+ def clear_plot(self):
23
+ try:
24
+ if self._colorbar is not None:
25
+ self._colorbar.remove()
26
+ self._colorbar = None
27
+ self.fig.clear()
28
+ self.axes = self.fig.add_subplot(111)
29
+ # Default range 0..5 with ±0.5 padding
30
+ self.axes.set_xlim(-0.5, 5.5)
31
+ self.axes.set_ylim(-0.5, 5.5)
32
+ self.draw()
33
+ except Exception:
34
+ # Keep UI responsive even if clear fails
35
+ pass
36
+
37
+ def visualize_uv_plane_points(
38
+ self,
39
+ uv_points,
40
+ sf_values,
41
+ vector_u_label: str,
42
+ vector_v_label: str,
43
+ vector_center=None,
44
+ value_max=None,
45
+ ):
46
+ """Render points in a user-defined plane parameterized by integers u,v.
47
+
48
+ Args:
49
+ uv_points: list of dicts with keys {'u','v','H','K','L'} for labels
50
+ sf_values: 1D array-like of |F| values per point
51
+ vector_u_label: text label for u-axis (e.g., "[h k l] of U")
52
+ vector_v_label: text label for v-axis
53
+ vector_center: a vector for center of the plane (e.g. [0,0,0])
54
+ """
55
+ try:
56
+ import numpy as np
57
+
58
+ if len(uv_points) == 0:
59
+ return False
60
+ u = np.array([p['u'] for p in uv_points])
61
+ v = np.array([p['v'] for p in uv_points])
62
+ f = np.asarray(sf_values)
63
+ if value_max is None:
64
+ value_max = f.max() if len(f) > 0 else 1.0
65
+
66
+ if len(u) != len(v) or len(u) != len(f):
67
+ return False
68
+
69
+ # Reset axes
70
+ if self._colorbar is not None:
71
+ self._colorbar.remove()
72
+ self._colorbar = None
73
+ self.fig.clear()
74
+ ax = self.fig.add_subplot(111)
75
+ self.axes = ax
76
+
77
+ # Normalize sizes
78
+ min_size, max_size = 0, 300
79
+ if value_max > 0:
80
+ sizes = min_size + (max_size - min_size) * (f / value_max)
81
+ else:
82
+ sizes = np.full_like(f, min_size, dtype=float)
83
+
84
+ mask = sizes < 1
85
+ sizes = sizes[~mask]
86
+ u_plot = u[~mask]
87
+ v_plot = v[~mask]
88
+ f_plot = f[~mask]
89
+
90
+ sc = ax.scatter(u_plot, v_plot, c=f_plot, s=sizes, cmap="viridis", alpha=0.9, vmax=value_max)
91
+
92
+ # Labels per point with HKL
93
+ for i, p in enumerate(uv_points):
94
+ if mask[i]:
95
+ color_font = "silver"
96
+ else:
97
+ color_font = "black"
98
+ ax.text(
99
+ p['u'] + 0.05,
100
+ p['v'] + 0.05,
101
+ f"{p['H']} {p['K']} {p['L']}",
102
+ fontsize=7,
103
+ color=color_font,
104
+ )
105
+
106
+ ax.set_xlabel(f"u along {vector_u_label}")
107
+ ax.set_ylabel(f"v along {vector_v_label}")
108
+ if vector_center is not None:
109
+ cx, cy, cz = vector_center
110
+ subtitle = f" centered at [{cx} {cy} {cz}]"
111
+ else:
112
+ subtitle = ""
113
+
114
+ # Limits and integer ticks
115
+ u_min, u_max = u.min(), u.max()
116
+ v_min, v_max = v.min(), v.max()
117
+ ax.set_xlim(u_min - 0.5, u_max + 0.5)
118
+ ax.set_ylim(v_min - 0.5, v_max + 0.5)
119
+ try:
120
+ from matplotlib.ticker import MaxNLocator
121
+ ax.xaxis.set_major_locator(MaxNLocator(integer=True))
122
+ ax.yaxis.set_major_locator(MaxNLocator(integer=True))
123
+ except Exception:
124
+ pass
125
+ ax.grid(True, alpha=0.3)
126
+ self.draw()
127
+ return True
128
+ except Exception:
129
+ return False
130
+
131
+ def visualize_plane(
132
+ self,
133
+ x_values,
134
+ y_values,
135
+ sf_values,
136
+ x_label: str,
137
+ y_label: str,
138
+ fixed_name: str,
139
+ fixed_value: int,
140
+ value_max = None,
141
+ ):
142
+ """Render a 2D plane plot.
143
+
144
+ Args:
145
+ x_values: 1D array-like for x-axis (e.g., H)
146
+ y_values: 1D array-like for y-axis (e.g., K)
147
+ sf_values: 1D array-like magnitudes |F|
148
+ x_label: which HKL index the x-axis represents ("H"|"K"|"L")
149
+ y_label: which HKL index the y-axis represents ("H"|"K"|"L")
150
+ fixed_name: the fixed HKL index ("H"|"K"|"L")
151
+ fixed_value: the integer value of the fixed index
152
+ Returns:
153
+ bool indicating success
154
+ """
155
+ try:
156
+ x = np.asarray(x_values)
157
+ y = np.asarray(y_values)
158
+ f = np.asarray(sf_values)
159
+ if value_max is None:
160
+ value_max = f.max()
161
+ if len(x) == 0 or len(y) == 0 or len(f) == 0 or len(x) != len(y) or len(x) != len(f):
162
+ return False
163
+
164
+ # Reset axes
165
+ if self._colorbar is not None:
166
+ self._colorbar.remove()
167
+ self._colorbar = None
168
+ self.fig.clear()
169
+ ax = self.fig.add_subplot(111)
170
+ self.axes = ax
171
+
172
+ # Normalize for size
173
+ min_size, max_size = 0, 300
174
+ if value_max > 0:
175
+ sizes = min_size + (max_size - min_size) * (f / value_max)
176
+ else:
177
+ sizes = np.full_like(f, min_size, dtype=float)
178
+
179
+ mask = sizes < 1
180
+ sizes = sizes[~mask]
181
+ x_plot = x[~mask]
182
+ y_plot = y[~mask]
183
+ f_plot = f[~mask]
184
+
185
+ sc = ax.scatter(x_plot, y_plot, c=f_plot, s=sizes, cmap="viridis", alpha=0.9, vmax=value_max)
186
+
187
+ # Add labels for each point (e.g., integer coordinates)
188
+ x_key = x_label.upper()
189
+ y_key = y_label.upper()
190
+ f_key = fixed_name.upper()
191
+
192
+ for i, (xi, yi) in enumerate(zip(x, y)):
193
+ if mask[i]:
194
+ color_font = "silver"
195
+ else:
196
+ color_font = "black"
197
+ values = {"H": None, "K": None, "L": None}
198
+ values[x_key] = int(round(float(xi)))
199
+ values[y_key] = int(round(float(yi)))
200
+ values[f_key] = int(fixed_value)
201
+ ax.text(
202
+ xi + 0.05,
203
+ yi + 0.05,
204
+ f"{values['H']} {values['K']} {values['L']}",
205
+ fontsize=7,
206
+ color=color_font,
207
+ )
208
+
209
+ # Colorbar intentionally removed for cleaner 2D view
210
+
211
+ ax.set_xlabel(f"{x_label} (r.l.u.)")
212
+ ax.set_ylabel(f"{y_label} (r.l.u.)")
213
+ ax.set_title(f"{x_label}{y_label} plane | {fixed_name} = {fixed_value}")
214
+
215
+ # Set limits and integer ticks
216
+ # Default limits 0..5 with auto-expand if needed
217
+ x_max = max(x.max(), 5)
218
+ y_max = max(y.max(), 5)
219
+ ax.set_xlim(-0.5, x_max + 0.5)
220
+ ax.set_ylim(-0.5, y_max + 0.5)
221
+ try:
222
+ from matplotlib.ticker import MaxNLocator
223
+
224
+ ax.xaxis.set_major_locator(MaxNLocator(integer=True))
225
+ ax.yaxis.set_major_locator(MaxNLocator(integer=True))
226
+ except Exception:
227
+ pass
228
+
229
+ ax.grid(True, alpha=0.3)
230
+ self.draw()
231
+ return True
232
+ except Exception:
233
+ return False
234
+
235
+