advisor-scattering 0.5.2__py3-none-any.whl → 0.5.3__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.
@@ -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 optimization algorithm to find the theta and phi/chi angles that satisfy the condition
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
- tuple: Five values containing the calculated results:
177
- - tth_result (float/list): Scattering angle value(s) in degrees
178
- - theta_result (float/list): Sample theta rotation value(s) in degrees
179
- - phi_result (float/list): Sample phi rotation value(s) in degrees
180
- - chi_result (float/list): Sample chi rotation value(s) in degrees
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
- tth_result, theta_result, phi_result, chi_result = calculate_angles(
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
- return tth_result, theta_result, phi_result, chi_result, momentum[0]
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-7,
253
- num_steps=3000,
254
- learning_rate=100,
255
+ target_objective=1e-10,
256
+ max_restarts=20,
255
257
  ):
256
258
  """Calculate scattering angles with chi angle (in degrees) fixed.
257
259
 
258
- Uses optimization algorithm to find the theta and phi angles that satisfy the condition
259
- for the given HKL indices while keeping chi fixed at the specified value. There could be more
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 criterion for optimization. Defaults to 1e-5.
270
- num_steps (int, optional): Maximum number of optimization steps. Defaults to 1000.
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 20.
272
272
 
273
273
  Returns:
274
- tuple: Four lists containing the calculated values:
275
- - tth_result (list): Scattering angle values in degrees
276
- - theta_result (list): Sample theta rotation values in degrees
277
- - phi_result (list): Sample phi rotation values in degrees
278
- - chi_result (list): Fixed chi values in degrees (all equal to chi_fixed)
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
- def objective_function(k_cal, k_target):
282
- """objective function for gradient decent"""
283
- return np.linalg.norm(k_cal - k_target)/np.linalg.norm(k_target)
284
-
285
- def get_k_cal(lab, theta_, phi_, chi_):
286
- lab.rotate(theta_, phi_, chi_)
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
- return k_cal
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
- if (phi > 90) or (phi < -90):
295
- return False
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
- theta_best = None
299
- phi_best = None
300
- _is_valid_solution = False
314
+ solutions = [] # List of (theta, phi) tuples
301
315
 
302
- while not _is_valid_solution:
303
- lab = Lab()
304
- theta = np.random.uniform(0, 180)
305
- phi = np.random.uniform(-90, 90)
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
- lab.initialize(
308
- a, b, c, alpha, beta, gamma, roll, pitch, yaw, theta, phi, chi_fixed
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
- k_cal = get_k_cal(lab, theta, phi, chi_fixed)
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
- theta_best = theta
333
- phi_best = phi
334
- _is_valid_solution = is_valid_solution(phi_best)
335
-
336
- theta_result = np.round(theta_best, 1)
337
- phi_result = np.round(phi_best, 1)
338
- tth_result = np.round(process_angle(tth), 1)
339
- chi_result = np.round(chi_fixed, 1)
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 tth_result, theta_result, phi_result, chi_result
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-7,
360
- num_steps=3000,
361
- learning_rate=100,
386
+ target_objective=1e-10,
387
+ max_restarts=20,
362
388
  ):
363
389
  """Calculate scattering angles with phi angle fixed.
364
390
 
365
- Uses optimization algorithm to find the theta and chi angles that satisfy the condition
366
- for the given HKL indices while keeping phi fixed at the specified value. There could be more
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 criterion for optimization. Defaults to 1e-5.
377
- num_steps (int, optional): Maximum number of optimization steps. Defaults to 1000.
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 20.
379
403
 
380
404
  Returns:
381
- tuple: Four lists containing the calculated values:
382
- - tth_result (list): Scattering angle values in degrees
383
- - theta_result (list): Sample theta rotation values in degrees
384
- - phi_result (list): Fixed phi values in degrees (all equal to phi_fixed)
385
- - chi_result (list): Sample chi rotation values in degrees
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
- def objective_function(k_cal, k_target):
389
- """objective function for gradient decent"""
390
- return np.linalg.norm(k_cal - k_target)
391
-
392
- def get_k_cal(lab, theta_, phi_, chi_):
393
- lab.rotate(theta_, phi_, chi_)
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
- return k_cal
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
- if (chi > 90) or (chi < -90):
402
- return False
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
- theta_best = None
406
- chi_best = None
407
- _is_valid_solution = False
408
- while not _is_valid_solution:
409
- lab = Lab()
410
- theta = np.random.uniform(0, 180)
411
- chi = np.random.uniform(-90, 90)
412
-
413
- lab.initialize(
414
- a, b, c, alpha, beta, gamma, roll, pitch, yaw, theta, phi_fixed, chi
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
- k_cal = get_k_cal(lab, theta, phi_fixed, chi)
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
- theta_best = theta
438
- chi_best = chi
439
- _is_valid_solution = is_valid_solution(chi_best)
440
-
441
- # round up to 0.1, discard duplicates, theta and chi should match the order of the list
442
- theta_result = np.round(theta_best, 1)
443
- chi_result = np.round(chi_best, 1)
444
- tth_result = np.round(process_angle(tth), 1)
445
- phi_result = np.round(phi_fixed, 1)
446
- return tth_result, theta_result, phi_result, chi_result
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):