advisor-scattering 0.5.2__py3-none-any.whl → 0.9.1__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.
- advisor/controllers/app_controller.py +2 -1
- advisor/domain/__init__.py +4 -0
- advisor/domain/core/lab.py +9 -2
- advisor/domain/core/lattice.py +2 -5
- advisor/domain/core/sample.py +9 -2
- advisor/domain/orientation.py +219 -0
- advisor/domain/orientation_calculator.py +174 -0
- advisor/features/__init__.py +7 -4
- advisor/features/scattering_geometry/domain/brillouin_calculator.py +133 -63
- advisor/features/scattering_geometry/domain/core.py +187 -134
- advisor/features/scattering_geometry/ui/components/hk_angles_components.py +175 -79
- advisor/features/scattering_geometry/ui/components/hkl_scan_components.py +42 -17
- advisor/features/scattering_geometry/ui/components/hkl_to_angles_components.py +175 -79
- advisor/features/scattering_geometry/ui/scattering_geometry_tab.py +57 -48
- advisor/ui/dialogs/__init__.py +7 -0
- advisor/ui/dialogs/diffraction_test_dialog.py +264 -0
- advisor/ui/init_window.py +33 -0
- {advisor_scattering-0.5.2.dist-info → advisor_scattering-0.9.1.dist-info}/METADATA +4 -12
- {advisor_scattering-0.5.2.dist-info → advisor_scattering-0.9.1.dist-info}/RECORD +22 -18
- {advisor_scattering-0.5.2.dist-info → advisor_scattering-0.9.1.dist-info}/WHEEL +1 -1
- {advisor_scattering-0.5.2.dist-info → advisor_scattering-0.9.1.dist-info}/entry_points.txt +0 -0
- {advisor_scattering-0.5.2.dist-info → advisor_scattering-0.9.1.dist-info}/top_level.txt +0 -0
|
@@ -155,11 +155,9 @@ def _calculate_angles_tth_fixed(
|
|
|
155
155
|
|
|
156
156
|
1. Use fsolve to find the missing momentum transfer component (H, K, or L). IT IS POSSIBLE THAT
|
|
157
157
|
THERE ARE MULTIPLE SOLUTIONS, BUT HERE WE ONLY RETURN THE ONE CLOSE TO THE NEGATIVE VALUE.
|
|
158
|
-
2. Use
|
|
158
|
+
2. Use root-finding to find up to two theta and phi/chi angles that satisfy the condition
|
|
159
159
|
for the given HKL indices while keeping one angle fixed.
|
|
160
160
|
|
|
161
|
-
There could be more than one solution, so the function returns a list of solutions.
|
|
162
|
-
|
|
163
161
|
Args:
|
|
164
162
|
k_in (float): Incident wave vector magnitude, in 2π/Å
|
|
165
163
|
tth (float): Scattering angle in degrees
|
|
@@ -173,12 +171,13 @@ def _calculate_angles_tth_fixed(
|
|
|
173
171
|
fixed_angle (float, optional): Value of the fixed angle in degrees. Defaults to 0.0.
|
|
174
172
|
|
|
175
173
|
Returns:
|
|
176
|
-
|
|
177
|
-
-
|
|
178
|
-
-
|
|
179
|
-
-
|
|
180
|
-
-
|
|
174
|
+
dict: Dictionary containing:
|
|
175
|
+
- tth (list): Scattering angle values in degrees
|
|
176
|
+
- theta (list): Sample theta rotation values in degrees
|
|
177
|
+
- phi (list): Sample phi rotation values in degrees
|
|
178
|
+
- chi (list): Sample chi rotation values in degrees
|
|
181
179
|
- momentum (float): Solved momentum transfer component (H, K, or L depending on which was None)
|
|
180
|
+
- number_of_solutions (int): Number of distinct solutions found
|
|
182
181
|
"""
|
|
183
182
|
# initial k_vec_lab when sample has not rotated
|
|
184
183
|
k_magnitude_target = calculate_k_magnitude(k_in, tth)
|
|
@@ -215,7 +214,7 @@ def _calculate_angles_tth_fixed(
|
|
|
215
214
|
|
|
216
215
|
calculate_angles = _calculate_angles_factory(fixed_angle_name)
|
|
217
216
|
|
|
218
|
-
|
|
217
|
+
result = calculate_angles(
|
|
219
218
|
k_in,
|
|
220
219
|
H,
|
|
221
220
|
K,
|
|
@@ -231,7 +230,11 @@ def _calculate_angles_tth_fixed(
|
|
|
231
230
|
yaw,
|
|
232
231
|
fixed_angle,
|
|
233
232
|
)
|
|
234
|
-
|
|
233
|
+
|
|
234
|
+
# Add momentum to the result
|
|
235
|
+
result["momentum"] = momentum[0]
|
|
236
|
+
|
|
237
|
+
return result
|
|
235
238
|
|
|
236
239
|
|
|
237
240
|
def _calculate_angles_chi_fixed(
|
|
@@ -249,15 +252,13 @@ def _calculate_angles_chi_fixed(
|
|
|
249
252
|
pitch,
|
|
250
253
|
yaw,
|
|
251
254
|
chi_fixed,
|
|
252
|
-
target_objective=1e-
|
|
253
|
-
|
|
254
|
-
learning_rate=100,
|
|
255
|
+
target_objective=1e-10,
|
|
256
|
+
max_restarts=40,
|
|
255
257
|
):
|
|
256
258
|
"""Calculate scattering angles with chi angle (in degrees) fixed.
|
|
257
259
|
|
|
258
|
-
Uses
|
|
259
|
-
for the given HKL indices while keeping chi fixed at the specified value.
|
|
260
|
-
than one solution, so the function returns a list of solutions.
|
|
260
|
+
Uses root-finding (fsolve) to find up to two theta and phi angle solutions that satisfy
|
|
261
|
+
the condition for the given HKL indices while keeping chi fixed at the specified value.
|
|
261
262
|
|
|
262
263
|
Args:
|
|
263
264
|
k_in (float): Incident wave vector magnitude, in 2π/Å
|
|
@@ -266,79 +267,105 @@ def _calculate_angles_chi_fixed(
|
|
|
266
267
|
alpha, beta, gamma (float): sample rotation angles in degrees
|
|
267
268
|
roll, pitch, yaw (float): Lattice rotation Euler angles in degrees. We use ZYX convention.
|
|
268
269
|
chi_fixed (float): Fixed chi angle in degrees
|
|
269
|
-
target_objective (float, optional): Convergence
|
|
270
|
-
|
|
271
|
-
learning_rate (float, optional): Learning rate for the gradient descent. Defaults to 100.
|
|
270
|
+
target_objective (float, optional): Convergence tolerance for fsolve. Defaults to 1e-10.
|
|
271
|
+
max_restarts (int, optional): Maximum number of random restarts. Defaults to 40.
|
|
272
272
|
|
|
273
273
|
Returns:
|
|
274
|
-
|
|
275
|
-
-
|
|
276
|
-
-
|
|
277
|
-
-
|
|
278
|
-
-
|
|
274
|
+
dict: Dictionary containing:
|
|
275
|
+
- tth (list): Scattering angle values in degrees
|
|
276
|
+
- theta (list): Sample theta rotation values in degrees
|
|
277
|
+
- phi (list): Sample phi rotation values in degrees
|
|
278
|
+
- chi (list): Fixed chi values in degrees
|
|
279
|
+
- number_of_solutions (int): Number of distinct solutions found (1 or 2)
|
|
279
280
|
"""
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
281
|
+
# Initialize lab ONCE - reuse for all iterations
|
|
282
|
+
lab = Lab()
|
|
283
|
+
lab.initialize(a, b, c, alpha, beta, gamma, roll, pitch, yaw, 0, 0, chi_fixed)
|
|
284
|
+
|
|
285
|
+
# Compute k_target (constant throughout optimization)
|
|
286
|
+
lab.rotate(45, 1, chi_fixed)
|
|
287
|
+
a_star, b_star, c_star = lab.get_reciprocal_space_vectors()
|
|
288
|
+
k_initial = H * a_star + K * b_star + L * c_star
|
|
289
|
+
k_magnitude = np.linalg.norm(k_initial)
|
|
290
|
+
tth = calculate_tth_from_k_magnitude(k_in, k_magnitude)
|
|
291
|
+
k_target = calculate_k_vector_in_lab(k_in, tth)
|
|
292
|
+
|
|
293
|
+
def equations(angles):
|
|
294
|
+
"""Return residuals: k_cal - k_target (2 components, 2 unknowns)."""
|
|
295
|
+
theta, phi = angles
|
|
296
|
+
lab.rotate(theta, phi, chi_fixed)
|
|
287
297
|
a_star_vec, b_star_vec, c_star_vec = lab.get_reciprocal_space_vectors()
|
|
288
298
|
k_cal = H * a_star_vec + K * b_star_vec + L * c_star_vec
|
|
289
|
-
|
|
299
|
+
# 2 equations for 2 unknowns (3rd is redundant due to |k_cal|=|k_target|)
|
|
300
|
+
return [k_cal[0] - k_target[0], k_cal[1] - k_target[1]]
|
|
290
301
|
|
|
291
302
|
def is_valid_solution(phi):
|
|
292
303
|
if phi is None:
|
|
293
304
|
return False
|
|
294
|
-
|
|
295
|
-
|
|
305
|
+
return -90 <= phi <= 90
|
|
306
|
+
|
|
307
|
+
def is_distinct_solution(theta_new, phi_new, existing_solutions, tolerance=1.0):
|
|
308
|
+
"""Check if a solution is distinct from existing ones (differs by more than tolerance degrees)."""
|
|
309
|
+
for theta_exist, phi_exist in existing_solutions:
|
|
310
|
+
if abs(theta_new - theta_exist) < tolerance and abs(phi_new - phi_exist) < tolerance:
|
|
311
|
+
return False
|
|
296
312
|
return True
|
|
297
313
|
|
|
298
|
-
|
|
299
|
-
phi_best = None
|
|
300
|
-
_is_valid_solution = False
|
|
314
|
+
solutions = [] # List of (theta, phi) tuples
|
|
301
315
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
316
|
+
# Try different starting points to find up to 2 distinct solutions
|
|
317
|
+
for _ in range(max_restarts):
|
|
318
|
+
theta0 = np.random.uniform(0, 180)
|
|
319
|
+
phi0 = np.random.uniform(-90, 90)
|
|
306
320
|
|
|
307
|
-
|
|
308
|
-
|
|
321
|
+
solution, info, ier, msg = fsolve(
|
|
322
|
+
equations,
|
|
323
|
+
x0=[theta0, phi0],
|
|
324
|
+
full_output=True,
|
|
325
|
+
xtol=target_objective,
|
|
309
326
|
)
|
|
310
327
|
|
|
311
|
-
|
|
312
|
-
k_magnitude = np.linalg.norm(k_cal)
|
|
313
|
-
tth = calculate_tth_from_k_magnitude(k_in, k_magnitude)
|
|
314
|
-
k_target = calculate_k_vector_in_lab(k_in, tth)
|
|
315
|
-
objective = objective_function(k_cal, k_target)
|
|
316
|
-
for i in range(num_steps):
|
|
317
|
-
step_size = objective * learning_rate
|
|
318
|
-
theta_new = theta + np.random.uniform(-step_size, step_size)
|
|
319
|
-
phi_new = phi + np.random.uniform(-step_size, step_size)
|
|
320
|
-
k_cal = get_k_cal(lab, theta_new, phi_new, chi_fixed)
|
|
321
|
-
objective_new = objective_function(k_cal, k_target)
|
|
322
|
-
if objective_new < objective:
|
|
323
|
-
theta = theta_new
|
|
324
|
-
phi = phi_new
|
|
325
|
-
objective = objective_new
|
|
326
|
-
if objective < target_objective:
|
|
327
|
-
break
|
|
328
|
-
# Normalize angles to (-180, 180] range
|
|
328
|
+
theta, phi = solution
|
|
329
329
|
theta = process_angle(theta)
|
|
330
330
|
phi = process_angle(phi)
|
|
331
331
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
332
|
+
# Check if fsolve converged (ier=1) and solution is valid
|
|
333
|
+
if ier == 1 and is_valid_solution(phi):
|
|
334
|
+
# Verify solution quality
|
|
335
|
+
residual = np.linalg.norm(equations([theta, phi]))
|
|
336
|
+
if residual < 1e-6:
|
|
337
|
+
# Check if this is a distinct solution
|
|
338
|
+
if is_distinct_solution(theta, phi, solutions):
|
|
339
|
+
solutions.append((theta, phi))
|
|
340
|
+
# Stop if we found 2 solutions
|
|
341
|
+
if len(solutions) >= 2:
|
|
342
|
+
break
|
|
343
|
+
|
|
344
|
+
# Build result lists
|
|
345
|
+
tth_result = process_angle(tth)
|
|
346
|
+
|
|
347
|
+
if len(solutions) == 0:
|
|
348
|
+
# No valid solution found - return last attempted values
|
|
349
|
+
return {
|
|
350
|
+
"tth": [tth_result],
|
|
351
|
+
"theta": [theta],
|
|
352
|
+
"phi": [phi],
|
|
353
|
+
"chi": [chi_fixed],
|
|
354
|
+
"number_of_solutions": 0,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
theta_list = [sol[0] for sol in solutions]
|
|
358
|
+
phi_list = [sol[1] for sol in solutions]
|
|
359
|
+
tth_list = [tth_result] * len(solutions)
|
|
360
|
+
chi_list = [chi_fixed] * len(solutions)
|
|
340
361
|
|
|
341
|
-
return
|
|
362
|
+
return {
|
|
363
|
+
"tth": tth_list,
|
|
364
|
+
"theta": theta_list,
|
|
365
|
+
"phi": phi_list,
|
|
366
|
+
"chi": chi_list,
|
|
367
|
+
"number_of_solutions": len(solutions),
|
|
368
|
+
}
|
|
342
369
|
|
|
343
370
|
|
|
344
371
|
def _calculate_angles_phi_fixed(
|
|
@@ -356,15 +383,13 @@ def _calculate_angles_phi_fixed(
|
|
|
356
383
|
pitch,
|
|
357
384
|
yaw,
|
|
358
385
|
phi_fixed,
|
|
359
|
-
target_objective=1e-
|
|
360
|
-
|
|
361
|
-
learning_rate=100,
|
|
386
|
+
target_objective=1e-10,
|
|
387
|
+
max_restarts=40,
|
|
362
388
|
):
|
|
363
389
|
"""Calculate scattering angles with phi angle fixed.
|
|
364
390
|
|
|
365
|
-
Uses
|
|
366
|
-
for the given HKL indices while keeping phi fixed at the specified value.
|
|
367
|
-
than one solution, so the function returns a list of solutions.
|
|
391
|
+
Uses root-finding (fsolve) to find up to two theta and chi angle solutions that satisfy
|
|
392
|
+
the condition for the given HKL indices while keeping phi fixed at the specified value.
|
|
368
393
|
|
|
369
394
|
Args:
|
|
370
395
|
k_in (float): Incident wave vector magnitude, in 2π/Å
|
|
@@ -373,77 +398,105 @@ def _calculate_angles_phi_fixed(
|
|
|
373
398
|
alpha, beta, gamma (float): sample rotation angles in degrees
|
|
374
399
|
roll, pitch, yaw (float): Lattice rotation Euler angles in degrees. We use ZYX convention.
|
|
375
400
|
phi_fixed (float): Fixed phi angle in degrees
|
|
376
|
-
target_objective (float, optional): Convergence
|
|
377
|
-
|
|
378
|
-
learning_rate (float, optional): Learning rate for the gradient descent. Defaults to 100.
|
|
401
|
+
target_objective (float, optional): Convergence tolerance for fsolve. Defaults to 1e-10.
|
|
402
|
+
max_restarts (int, optional): Maximum number of random restarts. Defaults to 40.
|
|
379
403
|
|
|
380
404
|
Returns:
|
|
381
|
-
|
|
382
|
-
-
|
|
383
|
-
-
|
|
384
|
-
-
|
|
385
|
-
-
|
|
405
|
+
dict: Dictionary containing:
|
|
406
|
+
- tth (list): Scattering angle values in degrees
|
|
407
|
+
- theta (list): Sample theta rotation values in degrees
|
|
408
|
+
- phi (list): Fixed phi values in degrees
|
|
409
|
+
- chi (list): Sample chi rotation values in degrees
|
|
410
|
+
- number_of_solutions (int): Number of distinct solutions found (1 or 2)
|
|
386
411
|
"""
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
412
|
+
# Initialize lab ONCE - reuse for all iterations
|
|
413
|
+
lab = Lab()
|
|
414
|
+
lab.initialize(a, b, c, alpha, beta, gamma, roll, pitch, yaw, 0, phi_fixed, 0)
|
|
415
|
+
|
|
416
|
+
# Compute k_target (constant throughout optimization)
|
|
417
|
+
lab.rotate(45, phi_fixed, 1)
|
|
418
|
+
a_star, b_star, c_star = lab.get_reciprocal_space_vectors()
|
|
419
|
+
k_initial = H * a_star + K * b_star + L * c_star
|
|
420
|
+
k_magnitude = np.linalg.norm(k_initial)
|
|
421
|
+
tth = calculate_tth_from_k_magnitude(k_in, k_magnitude)
|
|
422
|
+
k_target = calculate_k_vector_in_lab(k_in, tth)
|
|
423
|
+
|
|
424
|
+
def equations(angles):
|
|
425
|
+
"""Return residuals: k_cal - k_target (2 components, 2 unknowns)."""
|
|
426
|
+
theta, chi = angles
|
|
427
|
+
lab.rotate(theta, phi_fixed, chi)
|
|
394
428
|
a_star_vec, b_star_vec, c_star_vec = lab.get_reciprocal_space_vectors()
|
|
395
429
|
k_cal = H * a_star_vec + K * b_star_vec + L * c_star_vec
|
|
396
|
-
|
|
397
|
-
|
|
430
|
+
# 2 equations for 2 unknowns (3rd is redundant due to |k_cal|=|k_target|)
|
|
431
|
+
return [k_cal[0] - k_target[0], k_cal[1] - k_target[1]]
|
|
432
|
+
|
|
398
433
|
def is_valid_solution(chi):
|
|
399
434
|
if chi is None:
|
|
400
435
|
return False
|
|
401
|
-
|
|
402
|
-
|
|
436
|
+
return -90 <= chi <= 90
|
|
437
|
+
|
|
438
|
+
def is_distinct_solution(theta_new, chi_new, existing_solutions, tolerance=1.0):
|
|
439
|
+
"""Check if a solution is distinct from existing ones (differs by more than tolerance degrees)."""
|
|
440
|
+
for theta_exist, chi_exist in existing_solutions:
|
|
441
|
+
if abs(theta_new - theta_exist) < tolerance and abs(chi_new - chi_exist) < tolerance:
|
|
442
|
+
return False
|
|
403
443
|
return True
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
444
|
+
|
|
445
|
+
solutions = [] # List of (theta, chi) tuples
|
|
446
|
+
|
|
447
|
+
# Try different starting points to find up to 2 distinct solutions
|
|
448
|
+
for _ in range(max_restarts):
|
|
449
|
+
theta0 = np.random.uniform(0, 180)
|
|
450
|
+
chi0 = np.random.uniform(-90, 90)
|
|
451
|
+
|
|
452
|
+
solution, info, ier, msg = fsolve(
|
|
453
|
+
equations,
|
|
454
|
+
x0=[theta0, chi0],
|
|
455
|
+
full_output=True,
|
|
456
|
+
xtol=target_objective,
|
|
415
457
|
)
|
|
416
458
|
|
|
417
|
-
|
|
418
|
-
k_magnitude = np.linalg.norm(k_cal)
|
|
419
|
-
tth = calculate_tth_from_k_magnitude(k_in, k_magnitude)
|
|
420
|
-
k_target = calculate_k_vector_in_lab(k_in, tth)
|
|
421
|
-
objective = objective_function(k_cal, k_target)
|
|
422
|
-
for i in range(num_steps):
|
|
423
|
-
step_size = objective * learning_rate
|
|
424
|
-
theta_new = theta + np.random.uniform(-step_size, step_size)
|
|
425
|
-
chi_new = chi + np.random.uniform(-step_size, step_size)
|
|
426
|
-
k_cal = get_k_cal(lab, theta_new, phi_fixed, chi_new)
|
|
427
|
-
objective_new = objective_function(k_cal, k_target)
|
|
428
|
-
if objective_new < objective:
|
|
429
|
-
theta = theta_new
|
|
430
|
-
chi = chi_new
|
|
431
|
-
objective = objective_new
|
|
432
|
-
if objective < target_objective:
|
|
433
|
-
break
|
|
434
|
-
# Normalize angles to (0, 360) range
|
|
459
|
+
theta, chi = solution
|
|
435
460
|
theta = process_angle(theta)
|
|
436
461
|
chi = process_angle(chi)
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
462
|
+
|
|
463
|
+
# Check if fsolve converged (ier=1) and solution is valid
|
|
464
|
+
if ier == 1 and is_valid_solution(chi):
|
|
465
|
+
# Verify solution quality
|
|
466
|
+
residual = np.linalg.norm(equations([theta, chi]))
|
|
467
|
+
if residual < 1e-6:
|
|
468
|
+
# Check if this is a distinct solution
|
|
469
|
+
if is_distinct_solution(theta, chi, solutions):
|
|
470
|
+
solutions.append((theta, chi))
|
|
471
|
+
# Stop if we found 2 solutions
|
|
472
|
+
if len(solutions) >= 2:
|
|
473
|
+
break
|
|
474
|
+
|
|
475
|
+
# Build result lists
|
|
476
|
+
tth_result = process_angle(tth)
|
|
477
|
+
|
|
478
|
+
if len(solutions) == 0:
|
|
479
|
+
# No valid solution found - return last attempted values
|
|
480
|
+
return {
|
|
481
|
+
"tth": [tth_result],
|
|
482
|
+
"theta": [theta],
|
|
483
|
+
"phi": [phi_fixed],
|
|
484
|
+
"chi": [chi],
|
|
485
|
+
"number_of_solutions": 0,
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
theta_list = [sol[0] for sol in solutions]
|
|
489
|
+
chi_list = [sol[1] for sol in solutions]
|
|
490
|
+
tth_list = [tth_result] * len(solutions)
|
|
491
|
+
phi_list = [phi_fixed] * len(solutions)
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
"tth": tth_list,
|
|
495
|
+
"theta": theta_list,
|
|
496
|
+
"phi": phi_list,
|
|
497
|
+
"chi": chi_list,
|
|
498
|
+
"number_of_solutions": len(solutions),
|
|
499
|
+
}
|
|
447
500
|
|
|
448
501
|
|
|
449
502
|
def _calculate_hkl(k_in, tth, theta, phi, chi, a_vec_lab, b_vec_lab, c_vec_lab):
|