xarpes 0.3.3__py3-none-any.whl → 0.4.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.
xarpes/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = '0.3.3'
1
+ __version__ = '0.4.0'
2
2
 
3
3
  from .spectral import *
4
4
  from .distributions import *
xarpes/functions.py CHANGED
@@ -254,65 +254,119 @@ def download_examples():
254
254
  import shutil
255
255
  import io
256
256
  import jupytext
257
+ import tempfile
258
+ import re
257
259
 
258
- # Main xARPES repo (examples now live in /examples here)
259
- repo_url = 'https://github.com/xARPES/xARPES'
260
- output_dir = '.' # Directory from which the function is called
260
+ # Main xARPES repo (examples live under /examples there)
261
+ repo_url = "https://github.com/xARPES/xARPES"
262
+ output_dir = "." # Directory from which the function is called
261
263
 
262
264
  # Target 'examples' directory in the user's current location
263
- final_examples_path = os.path.join(output_dir, 'examples')
265
+ final_examples_path = os.path.join(output_dir, "examples")
264
266
  if os.path.exists(final_examples_path):
265
267
  print("Warning: 'examples' folder already exists. "
266
268
  "No download will be performed.")
267
269
  return 1 # Exit the function if 'examples' directory exists
268
270
 
269
- # Proceed with download if 'examples' directory does not exist
270
- repo_parts = repo_url.replace('https://github.com/', '').rstrip('/')
271
- zip_url = f'https://github.com/{repo_parts}/archive/refs/heads/main.zip'
272
-
273
- # Make the HTTP request to download the zip file
274
- print(f'Downloading {zip_url}')
275
- response = requests.get(zip_url)
276
- if response.status_code == 200:
277
- zip_file_bytes = io.BytesIO(response.content)
278
-
279
- with zipfile.ZipFile(zip_file_bytes, 'r') as zip_ref:
280
- zip_ref.extractall(output_dir)
281
-
282
- # Path to the extracted main folder (e.g. xARPES-main)
283
- main_folder_path = os.path.join(
284
- output_dir,
285
- repo_parts.split('/')[-1] + '-main'
286
- )
287
- examples_path = os.path.join(main_folder_path, 'examples')
288
-
289
- # Move the 'examples' directory to the target location
290
- if os.path.exists(examples_path):
291
- shutil.move(examples_path, final_examples_path)
292
- print(f"'examples' subdirectory moved to {final_examples_path}")
293
-
294
- # Convert all .Rmd files in the examples directory to .ipynb
295
- # and delete the .Rmd files
296
- for dirpath, dirnames, filenames in os.walk(final_examples_path):
297
- for filename in filenames:
298
- if filename.endswith('.Rmd'):
299
- full_path = os.path.join(dirpath, filename)
300
- jupytext.write(
301
- jupytext.read(full_path),
302
- full_path.replace('.Rmd', '.ipynb')
303
- )
304
- os.remove(full_path) # Deletes .Rmd file afterwards
305
- print(f'Converted and deleted {full_path}')
306
-
307
- # Remove the rest of the extracted content
308
- shutil.rmtree(main_folder_path)
309
- print(f'Cleaned up temporary files in {main_folder_path}')
310
- return 0
271
+ # --- Determine version from xarpes.__init__.__version__ -----------------
272
+ try:
273
+ # Import inside the function, avoiding circular imports at import time
274
+ import xarpes as _xarpes
275
+ raw_version = getattr(_xarpes, "__version__", None)
276
+ except Exception as exc:
277
+ print(f"Warning: could not import xarpes to determine version: {exc}")
278
+ raw_version = None
279
+
280
+ tag_version = None
281
+ if raw_version is not None:
282
+ raw_version = str(raw_version)
283
+ # Strip dev/local suffixes so that '0.3.3.dev1' or '0.3.3+0.gHASH'
284
+ # maps to the tag 'v0.3.3'. If you use plain '0.3.3' already, this is
285
+ # a no-op.
286
+ m = re.match(r"(\d+\.\d+\.\d+)", raw_version)
287
+ if m:
288
+ tag_version = m.group(1)
289
+ else:
290
+ tag_version = raw_version
291
+
292
+ print(f"Determined xARPES version from __init__: {raw_version} "
293
+ f"(using tag version '{tag_version}').")
311
294
  else:
312
- print('Failed to download the repository. Status code: '
313
- f'{response.status_code}')
295
+ print("Warning: xarpes.__version__ is not defined; will skip "
296
+ "tag-based download and try the main branch only.")
297
+
298
+ # --- Build refs and use for–else to try them in order -------------------
299
+ repo_parts = repo_url.replace("https://github.com/", "").rstrip("/")
300
+
301
+ refs_to_try = []
302
+ if tag_version is not None:
303
+ refs_to_try.append(f"tags/v{tag_version}") # version-matched examples
304
+ refs_to_try.append("heads/main") # fallback: latest examples
305
+
306
+ response = None
307
+ for ref in refs_to_try:
308
+ zip_url = f"https://github.com/{repo_parts}/archive/refs/{ref}.zip"
309
+ print(f"Attempting to download examples from '{ref}':\n {zip_url}")
310
+ response = requests.get(zip_url)
311
+
312
+ if response.status_code == 200:
313
+ if ref.startswith("tags/"):
314
+ print(f"Successfully downloaded examples from tagged release "
315
+ f"'v{tag_version}'.")
316
+ else:
317
+ print("Tagged release not available; using latest examples "
318
+ "from the 'main' branch instead.")
319
+ break
320
+ else:
321
+ print("Failed to download from this ref. HTTP status code: "
322
+ f"{response.status_code}")
323
+ else:
324
+ # for–else: only executed if we never hit 'break'
325
+ print("Error: could not download examples from any ref "
326
+ f"(tried: {', '.join(refs_to_try)}).")
314
327
  return 1
315
328
 
329
+ # At this point, 'response' holds a successful download
330
+ zip_file_bytes = io.BytesIO(response.content)
331
+
332
+ # --- Extract into a temporary directory to avoid polluting CWD ----------
333
+ with tempfile.TemporaryDirectory() as tmpdir:
334
+ with zipfile.ZipFile(zip_file_bytes, "r") as zip_ref:
335
+ zip_ref.extractall(tmpdir)
336
+ # First member gives us the top-level directory in the archive,
337
+ # typically something like 'xARPES-0.3.3/' or 'xARPES-main/'.
338
+ first_member = zip_ref.namelist()[0]
339
+
340
+ top_level_dir = first_member.split("/")[0]
341
+ main_folder_path = os.path.join(tmpdir, top_level_dir)
342
+ examples_path = os.path.join(main_folder_path, "examples")
343
+
344
+ if not os.path.exists(examples_path):
345
+ print("Error: downloaded archive does not contain an 'examples' "
346
+ "directory.")
347
+ return 1
348
+
349
+ # Move the 'examples' directory to the target location in the CWD
350
+ shutil.move(examples_path, final_examples_path)
351
+ print(f"'examples' subdirectory moved to {final_examples_path}")
352
+
353
+ # Convert all .Rmd files in the examples directory to .ipynb
354
+ # and delete the .Rmd files
355
+ for dirpath, dirnames, filenames in os.walk(final_examples_path):
356
+ for filename in filenames:
357
+ if filename.endswith(".Rmd"):
358
+ full_path = os.path.join(dirpath, filename)
359
+ jupytext.write(
360
+ jupytext.read(full_path),
361
+ full_path.replace(".Rmd", ".ipynb")
362
+ )
363
+ os.remove(full_path) # Deletes .Rmd file afterwards
364
+ print(f"Converted and deleted {full_path}")
365
+
366
+ # Temporary directory is cleaned up automatically
367
+ print("Cleaned up temporary files.")
368
+ return 0
369
+
316
370
 
317
371
  def set_script_dir():
318
372
  r"""This function sets the directory such that the xARPES code can be
xarpes/plotting.py CHANGED
@@ -12,14 +12,19 @@
12
12
  """Functions related to plotting."""
13
13
 
14
14
  from functools import wraps
15
+ from IPython import get_ipython
15
16
  import matplotlib.pyplot as plt
16
17
  import matplotlib as mpl
17
18
 
18
- def plot_settings(name='default'):
19
+
20
+ def plot_settings(name='default', register_pre_run=True):
21
+ """Configure default plotting style for xARPES."""
22
+
19
23
  mpl.rc('xtick', labelsize=10, direction='in')
20
24
  mpl.rc('ytick', labelsize=10, direction='in')
21
25
  plt.rcParams['legend.frameon'] = False
22
26
  lw = dict(default=2.0, large=4.0)[name]
27
+
23
28
  mpl.rcParams.update({
24
29
  'lines.linewidth': lw,
25
30
  'lines.markersize': 3,
@@ -30,6 +35,31 @@ def plot_settings(name='default'):
30
35
  'axes.ymargin': 0.15,
31
36
  })
32
37
 
38
+ if register_pre_run:
39
+ _maybe_register_pre_run_close_all()
40
+
41
+
42
+ def _maybe_register_pre_run_close_all():
43
+ """Register a pre_run_cell hook once, and only inside Jupyter."""
44
+
45
+ # Create the function attribute on first call
46
+ if not hasattr(_maybe_register_pre_run_close_all, "_registered"):
47
+ _maybe_register_pre_run_close_all._registered = False
48
+
49
+ if _maybe_register_pre_run_close_all._registered:
50
+ return
51
+
52
+ ip = get_ipython()
53
+ if ip is None or ip.__class__.__name__ != "ZMQInteractiveShell":
54
+ return
55
+
56
+ def _close_all(_info):
57
+ plt.close('all')
58
+
59
+ ip.events.register('pre_run_cell', _close_all)
60
+ _maybe_register_pre_run_close_all._registered = True
61
+
62
+
33
63
  def get_ax_fig_plt(ax=None, **kwargs):
34
64
  r"""Helper function used in plot functions supporting an optional `Axes`
35
65
  argument.
xarpes/spectral.py CHANGED
@@ -16,7 +16,7 @@ from igor2 import binarywave
16
16
  from .plotting import get_ax_fig_plt, add_fig_kwargs
17
17
  from .functions import fit_leastsq, extend_function
18
18
  from .distributions import FermiDirac, Linear
19
- from .constants import uncr, pref, dtor, kilo
19
+ from .constants import uncr, pref, dtor, kilo, stdv
20
20
 
21
21
  class BandMap:
22
22
  r"""
@@ -346,27 +346,41 @@ class BandMap:
346
346
  return mdcs, angle_range_out, self.angle_resolution, \
347
347
  enel_range_out, self.hnuminphi
348
348
 
349
-
350
349
  @add_fig_kwargs
351
350
  def plot(self, abscissa='momentum', ordinate='electron_energy',
352
- self_energies=None, ax=None, **kwargs):
351
+ self_energies=None, ax=None, markersize=None,
352
+ plot_dispersions='none', **kwargs):
353
353
  r"""
354
- Plot the band map. Optionally attach a collection of self-energies,
354
+ Plot the band map. Optionally overlay a collection of self-energies,
355
355
  e.g. a CreateSelfEnergies instance or any iterable of self-energy
356
- objects. They are stored on `self` for later overlay plotting.
356
+ objects. Self-energies are *not* stored internally; they are used
357
+ only for this plotting call.
358
+
359
+ When self-energies are present and ``abscissa='momentum'``, their
360
+ MDC maxima are overlaid with 95 % confidence intervals.
361
+
362
+ The `plot_dispersions` argument controls bare-band plotting:
363
+
364
+ - "full" : use the full momentum range of the map (default)
365
+ - "none" : do not plot bare dispersions
366
+ - "kink" : for each self-energy, use the min/max of its own
367
+ momentum range (typically its MDC maxima), with
368
+ `len(self.angles)` points.
369
+ - "domain" : for SpectralQuadratic, use only the left or right
370
+ domain relative to `center_wavevector`, based on the self-energy
371
+ attribute `side` ("left" / "right"); for other cases this behaves
372
+ as "full".
357
373
  """
374
+ import warnings
375
+
376
+ plot_disp_mode = plot_dispersions
377
+ valid_disp_modes = ('full', 'none', 'kink', 'domain')
378
+ if plot_disp_mode not in valid_disp_modes:
379
+ raise ValueError(
380
+ f"Invalid plot_dispersions '{plot_disp_mode}'. "
381
+ f"Valid options: {valid_disp_modes}."
382
+ )
358
383
 
359
- # Optionally store self-energies on the instance
360
- if self_energies is not None:
361
- # You can wrap here if you like, e.g.:
362
- # from .containers import CreateSelfEnergies
363
- # if not isinstance(self_energies, CreateSelfEnergies):
364
- # self_energies = CreateSelfEnergies(self_energies)
365
- self._self_energies = self_energies
366
- elif not hasattr(self, "_self_energies"):
367
- self._self_energies = None
368
-
369
- # Validate options early
370
384
  valid_abscissa = ('angle', 'momentum')
371
385
  valid_ordinate = ('kinetic_energy', 'electron_energy')
372
386
 
@@ -381,57 +395,247 @@ class BandMap:
381
395
  f"Valid options: {valid_ordinate}"
382
396
  )
383
397
 
398
+ if self_energies is not None:
399
+
400
+ # MDC maxima are defined in momentum space, not angle space
401
+ if abscissa == 'angle':
402
+ raise ValueError(
403
+ "MDC maxima cannot be plotted against angles; they are "
404
+ "defined in momentum space. Use abscissa='momentum' "
405
+ "when passing self-energies."
406
+ )
407
+
384
408
  ax, fig, plt = get_ax_fig_plt(ax=ax)
385
409
 
386
410
  Angl, Ekin = np.meshgrid(self.angles, self.ekin)
387
- mesh = None # sentinel to detect missing branch
388
411
 
389
412
  if abscissa == 'angle':
390
413
  ax.set_xlabel('Angle ($\\degree$)')
391
414
  if ordinate == 'kinetic_energy':
392
415
  mesh = ax.pcolormesh(
393
- Angl, Ekin, self.intensities, shading='auto',
394
- cmap=plt.get_cmap('bone').reversed(), **kwargs
395
- )
416
+ Angl, Ekin, self.intensities,
417
+ shading='auto',
418
+ cmap=plt.get_cmap('bone').reversed())
396
419
  ax.set_ylabel('$E_{\\mathrm{kin}}$ (eV)')
397
420
  elif ordinate == 'electron_energy':
398
421
  Enel = Ekin - self.hnuminphi
399
422
  mesh = ax.pcolormesh(
400
- Angl, Enel, self.intensities, shading='auto',
401
- cmap=plt.get_cmap('bone').reversed(), **kwargs
402
- )
423
+ Angl, Enel, self.intensities,
424
+ shading='auto',
425
+ cmap=plt.get_cmap('bone').reversed())
403
426
  ax.set_ylabel('$E-\\mu$ (eV)')
404
427
 
405
428
  elif abscissa == 'momentum':
406
- Mome = np.sqrt(Ekin / pref) * np.sin(Angl * dtor)
407
429
  ax.set_xlabel(r'$k_{//}$ ($\mathrm{\AA}^{-1}$)')
408
- if ordinate == 'kinetic_energy':
409
- mesh = ax.pcolormesh(
410
- Mome, Ekin, self.intensities, shading='auto',
411
- cmap=plt.get_cmap('bone').reversed(), **kwargs
430
+
431
+ with warnings.catch_warnings(record=True) as wlist:
432
+ warnings.filterwarnings(
433
+ "always",
434
+ message=(
435
+ "The input coordinates to pcolormesh are "
436
+ "interpreted as cell centers, but are not "
437
+ "monotonically increasing or decreasing."
438
+ ),
439
+ category=UserWarning,
412
440
  )
413
- ax.set_ylabel('$E_{\\mathrm{kin}}$ (eV)')
414
- elif ordinate == 'electron_energy':
415
- Enel = Ekin - self.hnuminphi
416
- mesh = ax.pcolormesh(
417
- Mome, Enel, self.intensities, shading='auto',
418
- cmap=plt.get_cmap('bone').reversed(), **kwargs
441
+
442
+ Mome = np.sqrt(Ekin / pref) * np.sin(Angl * dtor)
443
+ mome_min = np.min(Mome)
444
+ mome_max = np.max(Mome)
445
+ full_disp_momenta = np.linspace(
446
+ mome_min, mome_max, len(self.angles)
419
447
  )
420
- ax.set_ylabel('$E-\\mu$ (eV)')
421
448
 
422
- # If no branch set 'mesh', fail with a clear message
423
- if mesh is None:
424
- raise RuntimeError(
425
- "No plot produced for the combination: "
426
- f"abscissa='{abscissa}', ordinate='{ordinate}'. "
427
- f"Valid abscissa: {valid_abscissa}; "
428
- f"valid ordinate: {valid_ordinate}."
429
- )
449
+ if ordinate == 'kinetic_energy':
450
+ mesh = ax.pcolormesh(
451
+ Mome, Ekin, self.intensities,
452
+ shading='auto',
453
+ cmap=plt.get_cmap('bone').reversed())
454
+ ax.set_ylabel('$E_{\\mathrm{kin}}$ (eV)')
455
+ elif ordinate == 'electron_energy':
456
+ Enel = Ekin - self.hnuminphi
457
+ mesh = ax.pcolormesh(
458
+ Mome, Enel, self.intensities,
459
+ shading='auto',
460
+ cmap=plt.get_cmap('bone').reversed())
461
+ ax.set_ylabel('$E-\\mu$ (eV)')
462
+
463
+ y_lims = ax.get_ylim()
464
+
465
+ if any("cell centers" in str(w.message) for w in wlist):
466
+ warnings.warn(
467
+ "Conversion from angle to momenta causes warping of the "
468
+ "cell centers. \n Cell edges of the mesh plot may look "
469
+ "irregular.",
470
+ UserWarning,
471
+ stacklevel=2,
472
+ )
430
473
 
431
- plt.colorbar(mesh, ax=ax, label='counts (-)')
432
- return fig
474
+ if abscissa == 'momentum' and self_energies is not None:
475
+ for self_energy in self_energies:
476
+
477
+ mdc_maxima = getattr(self_energy, "mdc_maxima", None)
478
+
479
+ # If this self-energy doesn't contain maxima, don't plot
480
+ if mdc_maxima is None:
481
+ continue
482
+
483
+ # Reserve a colour from the axes cycle for this self-energy,
484
+ # and use it consistently for MDC maxima and dispersion.
485
+ line_color = ax._get_lines.get_next_color()
486
+
487
+ peak_sigma = getattr(
488
+ self_energy, "peak_positions_sigma", None
489
+ )
490
+ xerr = stdv * peak_sigma if peak_sigma is not None else None
491
+
492
+ if ordinate == 'kinetic_energy':
493
+ y_vals = self_energy.ekin_range
494
+ else:
495
+ y_vals = self_energy.enel_range
496
+
497
+ x_vals = mdc_maxima
498
+ label = getattr(self_energy, "label", None)
499
+
500
+ # First plot the MDC maxima, using the reserved colour
501
+ if xerr is not None:
502
+ ax.errorbar(
503
+ x_vals, y_vals, xerr=xerr, fmt='o',
504
+ linestyle='', label=label,
505
+ markersize=markersize,
506
+ color=line_color, ecolor=line_color,
507
+ )
508
+ else:
509
+ ax.plot(
510
+ x_vals, y_vals, linestyle='',
511
+ marker='o', label=label,
512
+ markersize=markersize,
513
+ color=line_color,
514
+ )
515
+
516
+ # Bare-band dispersion for SpectralLinear / SpectralQuadratic
517
+ spec_class = getattr(
518
+ self_energy, "_class",
519
+ self_energy.__class__.__name__,
520
+ )
521
+
522
+ if (plot_disp_mode != 'none'
523
+ and spec_class in ("SpectralLinear",
524
+ "SpectralQuadratic")):
525
+
526
+ # Determine momentum grid for the dispersion
527
+ if plot_disp_mode == 'kink':
528
+ x_arr = np.asarray(x_vals)
529
+ mask = np.isfinite(x_arr)
530
+ if not np.any(mask):
531
+ # No valid k-points to define a range
532
+ continue
533
+ k_min = np.min(x_arr[mask])
534
+ k_max = np.max(x_arr[mask])
535
+ disp_momenta = np.linspace(
536
+ k_min, k_max, len(self.angles)
537
+ )
538
+ elif (plot_disp_mode == 'domain'
539
+ and spec_class == "SpectralQuadratic"):
540
+ side = getattr(self_energy, "side", None)
541
+ if side == 'left':
542
+ disp_momenta = np.linspace(
543
+ mome_min, self_energy.center_wavevector,
544
+ len(self.angles)
545
+ )
546
+ elif side == 'right':
547
+ disp_momenta = np.linspace(
548
+ self_energy.center_wavevector, mome_max,
549
+ len(self.angles)
550
+ )
551
+ else:
552
+ # Fallback: no valid side, use full range
553
+ disp_momenta = full_disp_momenta
554
+ else:
555
+ # 'full' or 'domain' for SpectralLinear
556
+ disp_momenta = full_disp_momenta
557
+
558
+ # --- Robust parameter checks before computing base_disp ---
559
+ if spec_class == "SpectralLinear":
560
+ fermi_vel = getattr(
561
+ self_energy, "fermi_velocity", None
562
+ )
563
+ fermi_k = getattr(
564
+ self_energy, "fermi_wavevector", None
565
+ )
566
+ if fermi_vel is None or fermi_k is None:
567
+ missing = []
568
+ if fermi_vel is None:
569
+ missing.append("fermi_velocity")
570
+ if fermi_k is None:
571
+ missing.append("fermi_wavevector")
572
+ raise TypeError(
573
+ "Cannot plot bare dispersion for "
574
+ "SpectralLinear: "
575
+ f"{', '.join(missing)} is None."
576
+ )
577
+
578
+ base_disp = (
579
+ fermi_vel * (disp_momenta - fermi_k)
580
+ )
581
+
582
+ else: # SpectralQuadratic
583
+ bare_mass = getattr(
584
+ self_energy, "bare_mass", None
585
+ )
586
+ center_k = getattr(
587
+ self_energy, "center_wavevector", None
588
+ )
589
+ fermi_k = getattr(
590
+ self_energy, "fermi_wavevector", None
591
+ )
433
592
 
593
+ missing = []
594
+ if bare_mass is None:
595
+ missing.append("bare_mass")
596
+ if center_k is None:
597
+ missing.append("center_wavevector")
598
+ if fermi_k is None:
599
+ missing.append("fermi_wavevector")
600
+
601
+ if missing:
602
+ raise TypeError(
603
+ "Cannot plot bare dispersion for "
604
+ "SpectralQuadratic: "
605
+ f"{', '.join(missing)} is None."
606
+ )
607
+
608
+ dk = disp_momenta - center_k
609
+ base_disp = (
610
+ pref * (dk ** 2 - fermi_k ** 2) / bare_mass
611
+ )
612
+ # --- end parameter checks and base_disp construction ---
613
+
614
+ if ordinate == 'electron_energy':
615
+ disp_vals = base_disp
616
+ else: # kinetic energy
617
+ disp_vals = base_disp + self.hnuminphi
618
+
619
+ band_label = getattr(self_energy, "label", None)
620
+ if band_label is not None:
621
+ band_label = f"{band_label} (bare)"
622
+
623
+ ax.plot(
624
+ disp_momenta, disp_vals,
625
+ label=band_label,
626
+ linestyle='--',
627
+ color=line_color,
628
+ )
629
+
630
+ handles, labels = ax.get_legend_handles_labels()
631
+ if any(labels):
632
+ ax.legend()
434
633
 
634
+ ax.set_ylim(y_lims)
635
+
636
+ plt.colorbar(mesh, ax=ax, label='counts (-)')
637
+ return fig
638
+
435
639
  @add_fig_kwargs
436
640
  def fit_fermi_edge(self, hnuminphi_guess, background_guess=0.0,
437
641
  integrated_weight_guess=1.0, angle_min=-np.inf,
@@ -1233,14 +1437,25 @@ class MDCs:
1233
1437
  label = getattr(dist, 'label', str(dist))
1234
1438
  individual_labels.append(label)
1235
1439
 
1236
- # ---- collect parameters for this distribution
1440
+ # ---- collect parameters for this distribution
1237
1441
  # (Aggregated over slices)
1238
1442
  cls = getattr(dist, 'class_name', None)
1239
1443
  wanted = param_spec.get(cls, ())
1240
1444
 
1241
1445
  # ensure dicts exist
1242
1446
  label_bucket = aggregated_properties.setdefault(label, {})
1243
- class_bucket = label_bucket.setdefault(cls, {'label': label, '_class': cls})
1447
+ class_bucket = label_bucket.setdefault(
1448
+ cls, {'label': label, '_class': cls}
1449
+ )
1450
+
1451
+ # store center_wavevector (scalar) for SpectralQuadratic
1452
+ if (
1453
+ cls == 'SpectralQuadratic'
1454
+ and hasattr(dist, 'center_wavevector')
1455
+ ):
1456
+ class_bucket.setdefault(
1457
+ 'center_wavevector', dist.center_wavevector
1458
+ )
1244
1459
 
1245
1460
  # ensure keys for both values and sigmas
1246
1461
  for pname in wanted:
@@ -1560,8 +1775,8 @@ class MDCs:
1560
1775
  return final_result
1561
1776
 
1562
1777
 
1563
- def expose_parameters(self, select_label, fermi_wavevector=None,
1564
- fermi_velocity=None, bare_mass=None, side=None):
1778
+ def expose_parameters(self, select_label, fermi_wavevector=None,
1779
+ fermi_velocity=None, bare_mass=None, side=None):
1565
1780
  r"""
1566
1781
  Select and return fitted parameters for a given component label, plus a
1567
1782
  flat export dictionary containing values **and** 1σ uncertainties.
@@ -1575,7 +1790,8 @@ class MDCs:
1575
1790
  fermi_velocity : float, optional
1576
1791
  Optional Fermi velocity to include.
1577
1792
  bare_mass : float, optional
1578
- Optional bare mass to include (used for SpectralQuadratic dispersions).
1793
+ Optional bare mass to include (used for SpectralQuadratic
1794
+ dispersions).
1579
1795
  side : {'left','right'}, optional
1580
1796
  Optional side selector for SpectralQuadratic dispersions.
1581
1797
 
@@ -1588,30 +1804,41 @@ class MDCs:
1588
1804
  label : str
1589
1805
  Label of the selected distribution.
1590
1806
  selected_properties : dict or list of dict
1591
- Nested dictionary (or list thereof) containing <param> and
1592
- <param>_sigma arrays.
1807
+ Nested dictionary (or list thereof) containing <param> and
1808
+ <param>_sigma arrays. For SpectralQuadratic components, a
1809
+ scalar `center_wavevector` is also present.
1593
1810
  exported_parameters : dict
1594
- Flat dictionary of parameters and their uncertainties, plus optional
1595
- Fermi quantities and `side`.
1811
+ Flat dictionary of parameters and their uncertainties, plus
1812
+ optional Fermi quantities and `side`. For SpectralQuadratic
1813
+ components, `center_wavevector` is included and taken directly
1814
+ from the fitted distribution.
1596
1815
  """
1597
1816
 
1598
1817
  if self._ekin_range is None:
1599
- raise AttributeError("ekin_range not yet set. Run `.fit_selection()` first.")
1818
+ raise AttributeError(
1819
+ "ekin_range not yet set. Run `.fit_selection()` first."
1820
+ )
1600
1821
 
1601
1822
  store = getattr(self, "_individual_properties", None)
1602
1823
  if not store or select_label not in store:
1603
- all_labels = (sorted(store.keys()) if isinstance(store, dict) else [])
1824
+ all_labels = (sorted(store.keys())
1825
+ if isinstance(store, dict) else [])
1604
1826
  raise ValueError(
1605
- f"Label '{select_label}' not found in available labels: {all_labels}"
1827
+ f"Label '{select_label}' not found in available labels: "
1828
+ f"{all_labels}"
1606
1829
  )
1607
1830
 
1608
- # Convert lists → numpy arrays within the selected label’s classes
1831
+ # Convert lists → numpy arrays within the selected label’s classes.
1832
+ # Keep scalar center_wavevector as a scalar.
1609
1833
  per_class_dicts = []
1610
1834
  for cls, bucket in store[select_label].items():
1611
1835
  dct = {}
1612
1836
  for k, v in bucket.items():
1613
1837
  if k in ("label", "_class"):
1614
1838
  dct[k] = v
1839
+ elif k == "center_wavevector":
1840
+ # keep scalar as-is, do not wrap in np.asarray
1841
+ dct[k] = v
1615
1842
  else:
1616
1843
  dct[k] = np.asarray(v)
1617
1844
  per_class_dicts.append(dct)
@@ -1628,20 +1855,23 @@ class MDCs:
1628
1855
  "side": side,
1629
1856
  }
1630
1857
 
1631
- # Collect parameters without prefixing by class
1858
+ # Collect parameters without prefixing by class. This will also include
1859
+ # center_wavevector from the fitted SpectralQuadratic class, and since
1860
+ # there is no function argument with that name, it cannot be overridden.
1632
1861
  if isinstance(selected_properties, dict):
1633
1862
  for key, val in selected_properties.items():
1634
1863
  if key not in ("label", "_class"):
1635
1864
  exported_parameters[key] = val
1636
1865
  else:
1637
- # If multiple classes, merge sequentially (last overwrites same-name keys)
1866
+ # If multiple classes, merge sequentially
1867
+ # (last overwrites same-name keys).
1638
1868
  for cls_bucket in selected_properties:
1639
1869
  for key, val in cls_bucket.items():
1640
1870
  if key not in ("label", "_class"):
1641
1871
  exported_parameters[key] = val
1642
1872
 
1643
- return self._ekin_range, self.hnuminphi, select_label, \
1644
- selected_properties, exported_parameters
1873
+ return (self._ekin_range, self.hnuminphi, select_label,
1874
+ selected_properties, exported_parameters)
1645
1875
 
1646
1876
 
1647
1877
  class SelfEnergy:
@@ -1697,6 +1927,7 @@ class SelfEnergy:
1697
1927
  self._peak_sigma = self._properties.get("peak_sigma")
1698
1928
  self._broadening = self._properties.get("broadening")
1699
1929
  self._broadening_sigma = self._properties.get("broadening_sigma")
1930
+ self._center_wavevector = self._properties.get("center_wavevector")
1700
1931
 
1701
1932
  # lazy caches
1702
1933
  self._peak_positions = None
@@ -1763,6 +1994,7 @@ class SelfEnergy:
1763
1994
  self._peak_positions = None
1764
1995
  self._real = None
1765
1996
  self._real_sigma = None
1997
+ self._mdc_maxima = None
1766
1998
 
1767
1999
  @property
1768
2000
  def fermi_wavevector(self):
@@ -1838,6 +2070,7 @@ class SelfEnergy:
1838
2070
  # invalidate dependent cache
1839
2071
  self._peak_positions = None
1840
2072
  self._real = None
2073
+ self._mdc_maxima = None
1841
2074
 
1842
2075
  @property
1843
2076
  def peak_sigma(self):
@@ -1870,6 +2103,11 @@ class SelfEnergy:
1870
2103
  self._properties["broadening_sigma"] = x
1871
2104
  self._imag_sigma = None
1872
2105
 
2106
+ @property
2107
+ def center_wavevector(self):
2108
+ """Read-only center wavevector (SpectralQuadratic, if present)."""
2109
+ return self._center_wavevector
2110
+
1873
2111
  # ---------------- derived outputs ----------------
1874
2112
  @property
1875
2113
  def peak_positions(self):
@@ -1936,7 +2174,7 @@ class SelfEnergy:
1936
2174
  "(SpectralLinear): set `fermi_velocity` first.")
1937
2175
  self._imag_sigma = np.abs(self._fermi_velocity) * \
1938
2176
  np.sqrt(self._ekin_range / pref) * self._broadening_sigma
1939
- else: # SpectralQuadratic
2177
+ else:
1940
2178
  if self._bare_mass is None:
1941
2179
  raise AttributeError("Cannot compute `imag_sigma` "
1942
2180
  "(SpectralQuadratic): set `bare_mass` first.")
@@ -1987,6 +2225,178 @@ class SelfEnergy:
1987
2225
  * np.abs(self.peak_positions / self._bare_mass)
1988
2226
  return self._real_sigma
1989
2227
 
2228
+ @property
2229
+ def mdc_maxima(self):
2230
+ """
2231
+ MDC maxima (lazy).
2232
+
2233
+ SpectralLinear:
2234
+ identical to peak_positions
2235
+
2236
+ SpectralQuadratic:
2237
+ peak_positions + center_wavevector
2238
+ """
2239
+ if getattr(self, "_mdc_maxima", None) is None:
2240
+ if self.peak_positions is None:
2241
+ return None
2242
+
2243
+ if self._class == "SpectralLinear":
2244
+ self._mdc_maxima = self.peak_positions
2245
+ elif self._class == "SpectralQuadratic":
2246
+ self._mdc_maxima = (
2247
+ self.peak_positions + self._center_wavevector
2248
+ )
2249
+
2250
+ return self._mdc_maxima
2251
+
2252
+ def _se_legend_labels(self):
2253
+ """Return (real_label, imag_label) for legend with safe subscripts."""
2254
+ se_label = getattr(self, "label", None)
2255
+
2256
+ if se_label is None:
2257
+ real_label = r"$\Sigma'(E)$"
2258
+ imag_label = r"$-\Sigma''(E)$"
2259
+ return real_label, imag_label
2260
+
2261
+ safe_label = str(se_label).replace("_", r"\_")
2262
+
2263
+ # If the label is empty after conversion, fall back
2264
+ if safe_label == "":
2265
+ real_label = r"$\Sigma'(E)$"
2266
+ imag_label = r"$-\Sigma''(E)$"
2267
+ return real_label, imag_label
2268
+
2269
+ real_label = rf"$\Sigma_{{\mathrm{{{safe_label}}}}}'(E)$"
2270
+ imag_label = rf"$-\Sigma_{{\mathrm{{{safe_label}}}}}''(E)$"
2271
+
2272
+ return real_label, imag_label
2273
+
2274
+ @add_fig_kwargs
2275
+ def plot_real(self, ax=None, **kwargs):
2276
+ r"""Plot the real part Σ' of the self-energy as a function of E-μ.
2277
+
2278
+ Parameters
2279
+ ----------
2280
+ ax : Matplotlib-Axes or None
2281
+ Axis to plot on. Created if not provided by the user.
2282
+ **kwargs :
2283
+ Additional keyword arguments passed to ``ax.errorbar``.
2284
+
2285
+ Returns
2286
+ -------
2287
+ fig : Matplotlib-Figure
2288
+ Figure containing the Σ'(E) plot.
2289
+ """
2290
+
2291
+ ax, fig, plt = get_ax_fig_plt(ax=ax)
2292
+
2293
+ x = self.enel_range
2294
+ y = self.real
2295
+ y_sigma = self.real_sigma
2296
+
2297
+ real_label, _ = self._se_legend_labels()
2298
+ kwargs.setdefault("label", real_label)
2299
+
2300
+ if y_sigma is not None:
2301
+ if np.isnan(y_sigma).any():
2302
+ print(
2303
+ "Warning: some Σ'(E) uncertainty values are missing. "
2304
+ "Error bars omitted at those energies."
2305
+ )
2306
+ kwargs.setdefault("yerr", stdv * y_sigma)
2307
+
2308
+ ax.errorbar(x, y, **kwargs)
2309
+ ax.set_xlabel(r"$E-\mu$ (eV)")
2310
+ ax.set_ylabel(r"$\Sigma'(E)$ (eV)")
2311
+ ax.legend()
2312
+
2313
+ return fig
2314
+
2315
+ @add_fig_kwargs
2316
+ def plot_imag(self, ax=None, **kwargs):
2317
+ r"""Plot the imaginary part -Σ'' of the self-energy vs. E-μ.
2318
+
2319
+ Parameters
2320
+ ----------
2321
+ ax : Matplotlib-Axes or None
2322
+ Axis to plot on. Created if not provided by the user.
2323
+ **kwargs :
2324
+ Additional keyword arguments passed to ``ax.errorbar``.
2325
+
2326
+ Returns
2327
+ -------
2328
+ fig : Matplotlib-Figure
2329
+ Figure containing the -Σ''(E) plot.
2330
+ """
2331
+
2332
+ ax, fig, plt = get_ax_fig_plt(ax=ax)
2333
+
2334
+ x = self.enel_range
2335
+ y = self.imag
2336
+ y_sigma = self.imag_sigma
2337
+
2338
+ _, imag_label = self._se_legend_labels()
2339
+ kwargs.setdefault("label", imag_label)
2340
+
2341
+ if y_sigma is not None:
2342
+ if np.isnan(y_sigma).any():
2343
+ print(
2344
+ "Warning: some -Σ''(E) uncertainty values are missing. "
2345
+ "Error bars omitted at those energies."
2346
+ )
2347
+ kwargs.setdefault("yerr", stdv * y_sigma)
2348
+
2349
+ ax.errorbar(x, y, **kwargs)
2350
+ ax.set_xlabel(r"$E-\mu$ (eV)")
2351
+ ax.set_ylabel(r"$-\Sigma''(E)$ (eV)")
2352
+ ax.legend()
2353
+
2354
+ return fig
2355
+
2356
+ @add_fig_kwargs
2357
+ def plot_both(self, ax=None, **kwargs):
2358
+ r"""Plot Σ'(E) and -Σ''(E) vs. E-μ on the same axis."""
2359
+
2360
+ ax, fig, plt = get_ax_fig_plt(ax=ax)
2361
+
2362
+ x = self.enel_range
2363
+ real = self.real
2364
+ imag = self.imag
2365
+ real_sigma = self.real_sigma
2366
+ imag_sigma = self.imag_sigma
2367
+
2368
+ real_label, imag_label = self._se_legend_labels()
2369
+
2370
+ # --- plot Σ'
2371
+ kw_real = dict(kwargs)
2372
+ if real_sigma is not None:
2373
+ if np.isnan(real_sigma).any():
2374
+ print(
2375
+ "Warning: some Σ'(E) uncertainty values are missing. "
2376
+ "Error bars omitted at those energies."
2377
+ )
2378
+ kw_real.setdefault("yerr", stdv * real_sigma)
2379
+ kw_real.setdefault("label", real_label)
2380
+ ax.errorbar(x, real, **kw_real)
2381
+
2382
+ # --- plot -Σ''
2383
+ kw_imag = dict(kwargs)
2384
+ if imag_sigma is not None:
2385
+ if np.isnan(imag_sigma).any():
2386
+ print(
2387
+ "Warning: some -Σ''(E) uncertainty values are missing. "
2388
+ "Error bars omitted at those energies."
2389
+ )
2390
+ kw_imag.setdefault("yerr", stdv * imag_sigma)
2391
+ kw_imag.setdefault("label", imag_label)
2392
+ ax.errorbar(x, imag, **kw_imag)
2393
+
2394
+ ax.set_xlabel(r"$E-\mu$ (eV)")
2395
+ ax.set_ylabel(r"$\Sigma'(E),\ -\Sigma''(E)$ (eV)")
2396
+ ax.legend()
2397
+
2398
+ return fig
2399
+
1990
2400
 
1991
2401
  class CreateSelfEnergies:
1992
2402
  r"""
@@ -2063,5 +2473,4 @@ class CreateSelfEnergies:
2063
2473
  r"""
2064
2474
  Return a {label: self_energy} dictionary for convenient access.
2065
2475
  """
2066
- return {se.label: se for se in self.self_energies}
2067
-
2476
+ return {se.label: se for se in self.self_energies}
@@ -1,12 +1,13 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: xarpes
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Extraction from angle resolved photoemission spectra
5
5
  Author: xARPES Developers
6
6
  Requires-Python: >=3.7.0
7
7
  Description-Content-Type: text/markdown
8
8
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
9
9
  Classifier: Programming Language :: Python :: 3
10
+ License-File: LICENSE
10
11
  Requires-Dist: igor2>=0.5.8
11
12
  Requires-Dist: jupyterlab
12
13
  Requires-Dist: jupytext
@@ -15,7 +16,8 @@ Requires-Dist: numpy
15
16
  Requires-Dist: scipy
16
17
  Requires-Dist: lmfit
17
18
  Requires-Dist: pyqt5
18
- Requires-Dist: ipympl
19
+ Requires-Dist: ipympl>=0.9.3
20
+ Requires-Dist: ipywidgets>=8.1.5
19
21
  Requires-Dist: ipykernel<6.32.0
20
22
  Project-URL: Documentation, https://xarpes.github.io
21
23
 
@@ -31,7 +33,7 @@ This project is currently undergoing **beta testing**. Some of the functionaliti
31
33
 
32
34
  # Contributing
33
35
 
34
- Contributions to the code are most welcome. xARPES is intended to co-develop alongside the increasing complexity of experimental ARPES data sets. Contributions can be made by forking the code and creating a pull request. Importing of file formats from different beamlines is particularly encouraged.
36
+ Contributions to the code are most welcome. xARPES is intended to co-develop alongside the increasing complexity of experimental ARPES data sets. Contributions can be made by forking the code and creating a pull request. Importing of file formats from different beamlines is particularly encouraged. Files useful for developers can be found in `/dev_tools`, such as the `Rmd2py.py` script for the development of examples.
35
37
 
36
38
  # Installation
37
39
 
@@ -40,6 +42,8 @@ xARPES installation can be divided into graphical package manager instructions,
40
42
  - via conda-forge, out-of-the-box or editable installation, sourcing the [conda-forge package](https://anaconda.org/conda-forge/xarpes).
41
43
  - via Pip, out-of-the-box or editable installation, sourcing the [PyPI package](https://pypi.org/project/xarpes).
42
44
 
45
+ We strongly recommend installing xARPES in a (conda/pip) virtual environment, and to activate the environment each time before activating xARPES.
46
+
43
47
  ## Graphical package manager installation
44
48
 
45
49
  Most IDEs and scientific Python distributions include a GUI-based package manager.
@@ -82,11 +86,13 @@ Example for Linux:
82
86
  wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
83
87
  bash Miniconda3-latest-Linux-x86_64.sh
84
88
 
85
- Create and activate an environment:
89
+ Answer `y` to questions. Create and activate a new environment:
86
90
 
87
- conda create -n <my_env> -c defaults -c conda-forge
91
+ conda create -n <my_env> -c conda-forge
88
92
  conda activate <my_env>
89
93
 
94
+ Where `<my_env>` must be replaced by your desired name. Package compatibility ssues may arise if conda installs from different channels. This can be prevented by appending `--strict-channel-priority` to the creation command.
95
+
90
96
  ### Installing xARPES
91
97
 
92
98
  #### Option A — Out-of-the-box installation (from conda-forge)
@@ -116,8 +122,8 @@ Install venv if necessary:
116
122
 
117
123
  Create and activate a virtual environment:
118
124
 
119
- python3 -m venv <my_venv>
120
- source <my_venv>/bin/activate
125
+ python3 -m venv <my_env>
126
+ source <my_env>/bin/activate
121
127
 
122
128
  Upgrade pip:
123
129
 
@@ -146,6 +152,8 @@ After installation of xARPES, the `examples/` folder can be downloaded to the cu
146
152
 
147
153
  python -c "import xarpes; xarpes.download_examples()"
148
154
 
155
+ This attempts to download the examples from the version corresponding encountered in `__init__.py`. If no corresponding tagged version can be downloaded, the code attempts to download the latest examples instead.
156
+
149
157
  # Execution
150
158
 
151
159
  It is recommended to use JupyterLab to analyse data. JupyterLab is launched using:
@@ -0,0 +1,11 @@
1
+ xarpes/__init__.py,sha256=c_dJwE9MtE67k6ms-2ljYZxPwTHEHDUDtOiI_g6s9QE,125
2
+ xarpes/constants.py,sha256=vQxxFeCdGIxMpdh5XGbeRbn7-HF1d5snWkR09d8spGc,587
3
+ xarpes/distributions.py,sha256=svzhvf994_5gndJA1M04SW4MVfHEVwiAumbhO5Jj22s,23434
4
+ xarpes/functions.py,sha256=4s2atkWyPUb1ipJApRDVMsow_47kt25wvSwLtfqD2fs,14261
5
+ xarpes/plotting.py,sha256=3nwq-6q3i3hUG_tMv6y-62v2FceB2LuayDeE_fSdUr0,6499
6
+ xarpes/spectral.py,sha256=2AmqiyAk3xYJpUj1zPwbdA1Q62mGAVvXJAWmyiGHb6s,93908
7
+ xarpes-0.4.0.dist-info/entry_points.txt,sha256=917UR-cqFTMMI_vMqIbk7boYSuFX_zHwQlXKcj9vlCE,79
8
+ xarpes-0.4.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
9
+ xarpes-0.4.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
10
+ xarpes-0.4.0.dist-info/METADATA,sha256=cBUTzdc-CF8QCIag1q7kgSISydC2o1Mo4ZlAHMe87n0,6526
11
+ xarpes-0.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: flit 3.6.0
2
+ Generator: flit 3.12.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,11 +0,0 @@
1
- xarpes/__init__.py,sha256=fx6uw8FrJ2VesH-h-V0mQiUciW7oka3RL8jNgO0mtbs,125
2
- xarpes/constants.py,sha256=vQxxFeCdGIxMpdh5XGbeRbn7-HF1d5snWkR09d8spGc,587
3
- xarpes/distributions.py,sha256=svzhvf994_5gndJA1M04SW4MVfHEVwiAumbhO5Jj22s,23434
4
- xarpes/functions.py,sha256=gE76z-Y9UI1KNUUtADyLziLU1UJ43E1CHLDi_khj0bc,12007
5
- xarpes/plotting.py,sha256=W-5WaKjBtg8PIxTypqja2R29mgWkQ844lgRWci0nhn0,5679
6
- xarpes/spectral.py,sha256=ze6rPKrOg5jMoTL3Pv2MNdDGu5wOMzJJ2OF_iyrslRc,78580
7
- xarpes-0.3.3.dist-info/entry_points.txt,sha256=917UR-cqFTMMI_vMqIbk7boYSuFX_zHwQlXKcj9vlCE,79
8
- xarpes-0.3.3.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
9
- xarpes-0.3.3.dist-info/WHEEL,sha256=jPMR_Dzkc4X4icQtmz81lnNY_kAsfog7ry7qoRvYLXw,81
10
- xarpes-0.3.3.dist-info/METADATA,sha256=sK0m3UVDAhHi5a5K29zmrz8eTBZLCvYqPkWLAXnnk7s,5741
11
- xarpes-0.3.3.dist-info/RECORD,,