MNutils 1.2.4__tar.gz → 1.3.0__tar.gz

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 (43) hide show
  1. {mnutils-1.2.4 → mnutils-1.3.0}/PKG-INFO +3 -3
  2. {mnutils-1.2.4 → mnutils-1.3.0}/README.md +2 -2
  3. {mnutils-1.2.4 → mnutils-1.3.0}/pyproject.toml +1 -1
  4. {mnutils-1.2.4 → mnutils-1.3.0}/pyproject.toml.orig +1 -1
  5. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/GEExam.py +12 -6
  6. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/GESeries.py +96 -167
  7. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/__init__.py +1 -2
  8. mnutils-1.2.4/src/mnutils/fitting/AMARES.py +0 -831
  9. mnutils-1.2.4/src/mnutils/fitting/__init__.py +0 -16
  10. {mnutils-1.2.4 → mnutils-1.3.0}/LICENSE +0 -0
  11. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/_scripts.py +0 -0
  12. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/matlab/__init__.py +0 -0
  13. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/matlab/setup_matlab.py +0 -0
  14. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/__init__.py +0 -0
  15. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/_widgets/__init__.py +0 -0
  16. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/_widgets/_html.py +0 -0
  17. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/_widgets/_shared/__init__.py +0 -0
  18. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/_widgets/_shared/dom.js +0 -0
  19. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/_widgets/_shared/theme.css +0 -0
  20. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/_widgets/image_grid.css +0 -0
  21. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/_widgets/image_grid.js +0 -0
  22. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/_widgets/image_grid.py +0 -0
  23. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/_widgets/mrsi_inspector.css +0 -0
  24. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/_widgets/mrsi_inspector.js +0 -0
  25. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/_widgets/mrsi_inspector.py +0 -0
  26. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/_widgets/slice_viewer.css +0 -0
  27. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/_widgets/slice_viewer.js +0 -0
  28. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/_widgets/slice_viewer.py +0 -0
  29. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/images.py +0 -0
  30. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/saving.py +0 -0
  31. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/plotting/spectra.py +0 -0
  32. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/py.typed +0 -0
  33. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/rcparams.py +0 -0
  34. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/testing/__init__.py +0 -0
  35. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/testing/_spectra.py +0 -0
  36. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/testing/synthetic.py +0 -0
  37. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/utils/__init__.py +0 -0
  38. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/utils/data_loaders.py +0 -0
  39. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/utils/file_helpers.py +0 -0
  40. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/utils/images.py +0 -0
  41. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/utils/nifti.py +0 -0
  42. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/utils/partial_volume.py +0 -0
  43. {mnutils-1.2.4 → mnutils-1.3.0}/src/mnutils/utils/spectra.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MNutils
3
- Version: 1.2.4
3
+ Version: 1.3.0
4
4
  Summary: Utility functions for multinuclear MR data processing
5
5
  Keywords: mri,mrs,xarray,spectroscopy,nmr
6
6
  Author: Sebastian Bauer
@@ -117,8 +117,8 @@ already know a series's type, construct it directly (e.g. `MRSSeries(DATA_FOLDER
117
117
  the exam-level overview entirely.
118
118
 
119
119
  From there, spectral fitting is handled by [**xmris**](https://github.com/andrewendlinger/xmris)
120
- — MNUtils deliberately doesn't reimplement fitting, it wraps xmris's AMARES implementation
121
- (itself built on pyAMARES) so a series's spectrum can go straight into a fit:
120
+ — MNUtils deliberately doesn't reimplement fitting, a series hands its FIDs straight to xmris's
121
+ AMARES implementation (itself built on pyAMARES) and returns the xmris result:
122
122
 
123
123
  ```bash
124
124
  uv add "xmris[fitting]"
@@ -80,8 +80,8 @@ already know a series's type, construct it directly (e.g. `MRSSeries(DATA_FOLDER
80
80
  the exam-level overview entirely.
81
81
 
82
82
  From there, spectral fitting is handled by [**xmris**](https://github.com/andrewendlinger/xmris)
83
- — MNUtils deliberately doesn't reimplement fitting, it wraps xmris's AMARES implementation
84
- (itself built on pyAMARES) so a series's spectrum can go straight into a fit:
83
+ — MNUtils deliberately doesn't reimplement fitting, a series hands its FIDs straight to xmris's
84
+ AMARES implementation (itself built on pyAMARES) and returns the xmris result:
85
85
 
86
86
  ```bash
87
87
  uv add "xmris[fitting]"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "MNutils"
3
- version = "1.2.4"
3
+ version = "1.3.0"
4
4
  description = "Utility functions for multinuclear MR data processing"
5
5
  readme = "README.md"
6
6
  license = "BSD-3-Clause"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "MNutils"
3
- version = "1.2.4"
3
+ version = "1.3.0"
4
4
 
5
5
  authors = [
6
6
  {name = "Sebastian Bauer", email = "28663646+bastigw@users.noreply.github.com"}
@@ -29,6 +29,7 @@ class ExamBase:
29
29
  BASE_FOLDER: str | Path,
30
30
  DATA_FOLDER: str | Path | None = None,
31
31
  OUTPUT_FOLDER: str | Path | None = None,
32
+ show_overview: bool = False,
32
33
  ) -> None:
33
34
  self.BASE_FOLDER = Path(BASE_FOLDER)
34
35
  if DATA_FOLDER is None:
@@ -42,7 +43,9 @@ class ExamBase:
42
43
  else:
43
44
  self.OUTPUT_FOLDER = Path(OUTPUT_FOLDER)
44
45
 
45
- self.exam_overview = file_helpers.get_exam_overview(self.DATA_FOLDER, print_overview=True)
46
+ self.exam_overview = file_helpers.get_exam_overview(
47
+ self.DATA_FOLDER, print_overview=show_overview
48
+ )
46
49
  self.series = self.exam_overview.index.tolist()
47
50
  self.series_dict = self._create_series_dict()
48
51
 
@@ -170,8 +173,9 @@ class DMIExam(ExamBase):
170
173
  DATA_FOLDER: str | Path | None = None,
171
174
  OUTPUT_FOLDER: str | Path | None = None,
172
175
  ANATOMICAL_series: str | int | None = None,
176
+ show_overview: bool = False,
173
177
  ) -> None:
174
- super().__init__(BASE_FOLDER, DATA_FOLDER, OUTPUT_FOLDER)
178
+ super().__init__(BASE_FOLDER, DATA_FOLDER, OUTPUT_FOLDER, show_overview)
175
179
 
176
180
  if isinstance(ANATOMICAL_series, int):
177
181
  self.ANATOMICAL_series = ANATOMICAL_series
@@ -248,9 +252,10 @@ class MS_DMIExam(DMIExam):
248
252
  DATA_FOLDER: str | Path | None = None,
249
253
  OUTPUT_FOLDER: str | Path | None = None,
250
254
  ANATOMICAL_series: str | int | None = None,
255
+ show_overview: bool = False,
251
256
  ) -> None:
252
- super().__init__(BASE_FOLDER, DATA_FOLDER, OUTPUT_FOLDER, ANATOMICAL_series)
253
- logger.info("MS DMI Exam initialized.")
257
+ super().__init__(BASE_FOLDER, DATA_FOLDER, OUTPUT_FOLDER, ANATOMICAL_series, show_overview)
258
+ logger.debug("MS DMI Exam initialized.")
254
259
 
255
260
  self.MRSI: GESeries.MRSISeries
256
261
  self.pre_MRS: GESeries.MRSSeries
@@ -309,8 +314,9 @@ class DMIinjExam(DMIExam):
309
314
  DATA_FOLDER: str | Path | None = None,
310
315
  OUTPUT_FOLDER: str | Path | None = None,
311
316
  ANATOMICAL_series: str | int | None = None,
317
+ show_overview: bool = False,
312
318
  ) -> None:
313
- super().__init__(BASE_FOLDER, DATA_FOLDER, OUTPUT_FOLDER, ANATOMICAL_series)
319
+ super().__init__(BASE_FOLDER, DATA_FOLDER, OUTPUT_FOLDER, ANATOMICAL_series, show_overview)
314
320
 
315
321
  self.WASHIN_series = self._ids_with_description("MRS_washin")[0]
316
322
 
@@ -322,7 +328,7 @@ class DMIinjExam(DMIExam):
322
328
  self.MRSI_series, self.WASHIN_series, "MRSI"
323
329
  )
324
330
 
325
- logger.info("DMI Intravenous Injection Exam initialized.")
331
+ logger.debug("DMI Intravenous Injection Exam initialized.")
326
332
 
327
333
  self.washin: GESeries.MRSWashinSeries
328
334
  self.pre_MRS: GESeries.MRSSeries
@@ -7,13 +7,12 @@ from pathlib import Path
7
7
  import h5py
8
8
  import numpy as np
9
9
  import numpy.typing as npt
10
- import pandas as pd
11
10
  import xarray as xr
12
11
  import xmris # noqa: F401, necessary for xarray accessor to work
13
12
  from loguru import logger
14
13
  from nibabel import nifti1, spatialimages
15
14
 
16
- from . import fitting, plotting
15
+ from . import plotting
17
16
  from .utils import data_loaders, file_helpers, nifti, spectra
18
17
 
19
18
 
@@ -730,6 +729,7 @@ class MRSISeries(RawMRISeries):
730
729
 
731
730
  self.fitted_metabolite_maps: dict = {}
732
731
  self.goodness_of_fit_maps: dict = {}
732
+ self.fit_results: xr.Dataset | None = None
733
733
  if load_processed_data:
734
734
  self.load_processed_data()
735
735
 
@@ -849,59 +849,59 @@ class MRSISeries(RawMRISeries):
849
849
  x: int,
850
850
  y: int,
851
851
  slice: int,
852
- init_params: dict,
853
- fit_params: dict | None = None,
854
- ):
855
- """Fit the FID at voxel (x, y, slice) with AMARES."""
856
- if fit_params is None:
857
- fit_params = {}
858
- fid = self.fids.sel(i=x, j=y, k=slice)
852
+ prior_knowledge,
853
+ **fit_kwargs,
854
+ ) -> xr.Dataset:
855
+ """Fit the FID at voxel (x, y, slice) with AMARES (via ``xmris``).
859
856
 
860
- # Include series information in init_params
861
- passed_init_params = {
862
- "MHz": self.centre_frequency,
863
- "sw": self.bandwidth,
864
- "deadtime": self.deadtime,
865
- }
866
- passed_init_params.update(init_params)
867
-
868
- fit_result = fitting.AMARES.fit_single_fid(
869
- fid,
870
- passed_init_params,
871
- fit_params,
872
- )
873
- return fit_result
857
+ Parameters
858
+ ----------
859
+ x, y, slice : int
860
+ Voxel indices.
861
+ prior_knowledge : Mapping | pandas.DataFrame | str | Path
862
+ Prior knowledge, forwarded to ``xmris`` ``fit_amares``.
863
+ **fit_kwargs
864
+ Further keyword arguments for ``fit_amares`` (``method``, ``g_global``, ...).
865
+
866
+ Returns
867
+ -------
868
+ xarray.Dataset
869
+ The ``fit_amares`` result for this voxel.
870
+ """
871
+ fid = self.fids.sel(i=x, j=y, k=slice)
872
+ return fid.xmr.fit_amares(prior_knowledge, **fit_kwargs)
874
873
 
875
874
  def fit_all_voxels(
876
875
  self,
877
- init_params: dict,
878
- batch_fitting_params: dict | None = None,
879
- bad_fit_chisqr_threshold: float = -1.0,
876
+ prior_knowledge,
877
+ snr_threshold: float = 1.5,
880
878
  testing_mode: bool = False,
881
- ) -> npt.NDArray:
879
+ **fit_kwargs,
880
+ ) -> xr.Dataset:
882
881
  """Fit all voxels above the SNR threshold with AMARES and store the maps.
883
882
 
884
883
  Parameters
885
884
  ----------
886
- init_params : dict
887
- Initial fit parameters, merged with series-derived MHz/sw/deadtime.
888
- batch_fitting_params : dict, optional
889
- Forwarded to ``fitting.AMARES.fit_multiple_fids``.
890
- bad_fit_chisqr_threshold : float, optional
891
- Chi-squared threshold above which a fit is flagged as bad. Defaults to -1.0
892
- (disabled).
885
+ prior_knowledge : Mapping | pandas.DataFrame | str | Path
886
+ Prior knowledge, forwarded to ``xmris`` ``fit_amares``.
887
+ snr_threshold : float, optional
888
+ Voxels with ``SNR_map`` below this are not fitted. Defaults to 1.5.
893
889
  testing_mode : bool, optional
894
890
  If True, limit fitting to the first 100 voxels above the SNR threshold.
891
+ **fit_kwargs
892
+ Further keyword arguments for ``fit_amares`` (``method``, ``g_global``, ...).
893
+ ``num_workers`` defaults to ``-1`` (every core) here, unlike ``xmris``'s
894
+ in-process default of 1, because a voxel grid is exactly where a pool pays off.
895
895
 
896
896
  Returns
897
897
  -------
898
- numpy.ndarray
899
- Flattened voxel indices of fits flagged as bad.
898
+ xarray.Dataset
899
+ The ``fit_amares`` result on the full ``(i, j, k)`` grid; voxels that were not
900
+ fitted are NaN. Also stored as ``fit_results`` and summarised in
901
+ ``fitted_metabolite_maps`` and ``goodness_of_fit_maps``.
900
902
  """
901
- if batch_fitting_params is None:
902
- batch_fitting_params = {}
903
- SNR_mask = self.get_SNR_mask()
904
- # If in testing mode, limit to first 100 voxels with SNR above threshold
903
+ grid = ("i", "j", "k")
904
+ SNR_mask = self.get_SNR_mask(snr_threshold)
905
905
  if testing_mode:
906
906
  flat_SNR_mask = SNR_mask.stack(voxel=SNR_mask.dims)
907
907
  indices = np.where(flat_SNR_mask)[0][:100]
@@ -909,69 +909,28 @@ class MRSISeries(RawMRISeries):
909
909
  SNR_mask[indices] = True
910
910
  SNR_mask = SNR_mask.unstack("voxel")
911
911
 
912
- # Include series information in init_params
913
- passed_init_params = {
914
- "MHz": self.centre_frequency,
915
- "sw": self.bandwidth,
916
- "deadtime": self.deadtime,
917
- }
918
- passed_init_params.update(init_params)
919
-
920
- results = fitting.AMARES.fit_multiple_fids(
921
- self.fids.where(SNR_mask).stack(voxel=SNR_mask.dims).dropna("voxel"),
922
- batch_fitting_params=batch_fitting_params,
923
- init_params=passed_init_params,
924
- )
925
-
926
- # Extract results back into original shape
927
- extracted_results = fitting.AMARES.extract_from_fit_results(
928
- results, bad_fit_chisqr_threshold
912
+ fit_kwargs.setdefault("num_workers", -1)
913
+ stacked = self.fids.where(SNR_mask).stack(voxel=SNR_mask.dims).dropna("voxel")
914
+ # fit_amares stacks the non-time dims itself, so hand it a plain voxel dim
915
+ fitted = stacked.reset_index("voxel").xmr.fit_amares(prior_knowledge, **fit_kwargs)
916
+ results = (
917
+ fitted.set_index(voxel=list(grid))
918
+ .unstack("voxel")
919
+ .reindex(i=self.fids["i"], j=self.fids["j"], k=self.fids["k"])
929
920
  )
921
+ self.fit_results = results
930
922
 
931
- # Save extracted results into processing data folder with additional attributes
932
- additional_attributes: dict[str, str | int] = {
933
- "SERIES_ID": self.SERIES_ID,
934
- "exam_number": self.exam_number,
935
- "ACQUISITION_TIME": self.scan_datetime.isoformat(),
936
- "SAVED_ON": datetime.now().isoformat(),
937
- }
938
- additional_datasets = {
939
- "SNR_mask": SNR_mask.values,
940
- }
941
-
942
- fitting.AMARES.save_extracted_results(
943
- self.OUTPUT_FOLDER
944
- / "processing_data"
945
- / "dev"
946
- / f"Series{self.SERIES_ID:02d}_extracted_results.h5",
947
- extracted_results,
948
- additional_attributes,
949
- additional_datasets=additional_datasets,
950
- prepend_datetime=True,
923
+ amplitude = results["amplitude"]
924
+ for metabolite in amplitude["metabolite"].values:
925
+ self.fitted_metabolite_maps[str(metabolite)] = (
926
+ amplitude.sel(metabolite=metabolite).transpose(*grid).to_numpy()
927
+ )
928
+ residual_rms = np.sqrt((np.abs(results["residuals"]) ** 2).mean("time"))
929
+ self.goodness_of_fit_maps["residual_rms"] = residual_rms.transpose(*grid).to_numpy()
930
+ self.goodness_of_fit_maps["fit_status"] = (
931
+ results["fit_status"].transpose(*grid).astype(float).to_numpy()
951
932
  )
952
-
953
- combined_fit_results = extracted_results.combined_fit_results
954
- fitted_metabolites_temp = np.full((*self.dims, combined_fit_results.columns.size), np.nan)
955
- fitted_metabolites_temp[SNR_mask] = combined_fit_results.loc[
956
- (slice(None), "amplitude", "value"), :
957
- ].to_numpy()
958
- for i, metabolite in enumerate(combined_fit_results.columns):
959
- self.fitted_metabolite_maps[metabolite] = fitted_metabolites_temp[:, :, :, i]
960
-
961
- goodness_of_fit = extracted_results.goodness_of_fit
962
- gof_metrics_temp = np.full((*self.dims, goodness_of_fit.columns.size), np.nan)
963
- gof_metrics_temp[SNR_mask] = goodness_of_fit.to_numpy()
964
- for i, metric in enumerate(goodness_of_fit.columns):
965
- self.goodness_of_fit_maps[metric] = gof_metrics_temp[:, :, :, i]
966
-
967
- # The badfit_ids correspond to the flattened indices of the fitted voxels
968
- # Create mapping from badfit_ids to original voxel flattened indices
969
- if extracted_results.badfit_ids.size == 0:
970
- original_badfit_ids = np.array([], dtype=int)
971
- else:
972
- original_badfit_ids = np.where(SNR_mask.flatten())[0][extracted_results.badfit_ids]
973
-
974
- return original_badfit_ids
933
+ return results
975
934
 
976
935
  def save_processed_data(self, output_folder: Path | None = None) -> Path:
977
936
  """Save fitted maps, FIDs, and identifying attributes to an HDF5 file.
@@ -1515,15 +1474,27 @@ class MRSSeries(RawMRISeries):
1515
1474
 
1516
1475
  def fit_average_fid(
1517
1476
  self,
1518
- init_params: dict,
1519
- fit_params: dict | None = None,
1477
+ prior_knowledge,
1520
1478
  normalisation_factor: np.complexfloating | None = None,
1521
- filter_by_ppm: tuple[float, float] | None = None,
1522
- ):
1523
- """Fit the averaged FID (avg_fid) with AMARES, normalizing it first."""
1524
- if fit_params is None:
1525
- fit_params = {}
1526
- # Normalize avg_fid
1479
+ **fit_kwargs,
1480
+ ) -> xr.Dataset:
1481
+ """Fit the averaged FID (avg_fid) with AMARES (via ``xmris``), normalizing it first.
1482
+
1483
+ Parameters
1484
+ ----------
1485
+ prior_knowledge : Mapping | pandas.DataFrame | str | Path
1486
+ Prior knowledge, forwarded to ``xmris`` ``fit_amares``.
1487
+ normalisation_factor : complex, optional
1488
+ Scaling shared across series so amplitudes are comparable. Defaults to the
1489
+ maximum of the averaged FID, with a warning.
1490
+ **fit_kwargs
1491
+ Further keyword arguments for ``fit_amares``.
1492
+
1493
+ Returns
1494
+ -------
1495
+ xarray.Dataset
1496
+ The ``fit_amares`` result.
1497
+ """
1527
1498
  if normalisation_factor is None:
1528
1499
  scaling_factor = np.max(self.avg_fid) * self.number_of_excitations
1529
1500
  logger.warning(
@@ -1538,16 +1509,8 @@ class MRSSeries(RawMRISeries):
1538
1509
  f"Number of excitations: {self.number_of_excitations}"
1539
1510
  )
1540
1511
 
1541
- avg_fid = self.avg_fid / scaling_factor
1542
-
1543
- fit_result = fitting.AMARES.fit_single_fid(
1544
- avg_fid,
1545
- init_params,
1546
- fit_params,
1547
- raw_header=self.header,
1548
- filter_by_ppm=filter_by_ppm,
1549
- )
1550
- return fit_result
1512
+ avg_fid = (self.avg_fid / scaling_factor).assign_attrs(self.avg_fid.attrs)
1513
+ return avg_fid.xmr.fit_amares(prior_knowledge, **fit_kwargs)
1551
1514
 
1552
1515
 
1553
1516
  class MRSWashinSeries(MRSSeries):
@@ -1555,19 +1518,7 @@ class MRSWashinSeries(MRSSeries):
1555
1518
 
1556
1519
  def __init__(self, DATA_FOLDER: Path, SERIES_ID: int) -> None:
1557
1520
  super().__init__(DATA_FOLDER, SERIES_ID)
1558
- self.fit_results: list = []
1559
-
1560
- @property
1561
- def extracted_results(self):
1562
- """The fit results extracted into a FitResults, or an empty one if unfit."""
1563
- if not self.fit_results:
1564
- logger.info(
1565
- "No fit results present. Please run fit_grouped_by_duration() "
1566
- "first. Returning empty results."
1567
- )
1568
- return fitting.AMARES.FitResults(np.asarray([]), pd.DataFrame(), pd.DataFrame())
1569
- else:
1570
- return self.extract_from_fit_results()
1521
+ self.fit_results: xr.Dataset | None = None
1571
1522
 
1572
1523
  def plot_washin(self, **kwargs):
1573
1524
  """Plot the duration-averaged spectra, one line per minute."""
@@ -1581,14 +1532,19 @@ class MRSWashinSeries(MRSSeries):
1581
1532
 
1582
1533
  def fit_grouped_by_duration(
1583
1534
  self,
1584
- init_params: dict,
1585
- fit_params: dict | None = None,
1535
+ prior_knowledge,
1586
1536
  normalisation_factor: np.complexfloating | None = None,
1587
1537
  group_duration: int = 60,
1588
- ) -> list:
1589
- """Average FIDs by duration and fit each group's FID with AMARES."""
1590
- if fit_params is None:
1591
- fit_params = {}
1538
+ **fit_kwargs,
1539
+ ) -> xr.Dataset:
1540
+ """Average FIDs by duration and fit all groups with AMARES (via ``xmris``).
1541
+
1542
+ Returns
1543
+ -------
1544
+ xarray.Dataset
1545
+ The ``fit_amares`` result, with one entry per duration group along the group
1546
+ dimension. Also stored as ``fit_results``.
1547
+ """
1592
1548
  if normalisation_factor is None:
1593
1549
  logger.warning(
1594
1550
  "No normalisation_factor provided. This can lead to different "
@@ -1610,33 +1566,6 @@ class MRSWashinSeries(MRSSeries):
1610
1566
  f"Number of excitations: {self.number_of_excitations}"
1611
1567
  )
1612
1568
 
1613
- fit_results = []
1614
- for idx in range(averaged_fids.shape[0]):
1615
- fid = averaged_fids[idx, :]
1616
- # Normalize fid
1617
- fid = fid / scaling_factor
1618
-
1619
- fit_result = fitting.AMARES.fit_single_fid(
1620
- fid,
1621
- init_params,
1622
- fit_params,
1623
- raw_header=self.header,
1624
- )
1625
- fit_results.append(fit_result)
1626
-
1627
- self.fit_results = fit_results
1628
- return fit_results
1629
-
1630
- def extract_from_fit_results(
1631
- self, index_key: str = "group_id", index_values: list | None = None
1632
- ) -> fitting.AMARES.FitResults:
1633
- """Restructure self.fit_results into a combined FitResults."""
1634
- restructured_results = []
1635
- for res in self.fit_results:
1636
- data = (res.result_multiplets, res.out_obj)
1637
- restructured_results.append(data)
1638
-
1639
- extracted_results = fitting.AMARES.extract_from_fit_results(
1640
- restructured_results, index_key=index_key, index_values=index_values
1641
- )
1642
- return extracted_results
1569
+ scaled_fids = (averaged_fids / scaling_factor).assign_attrs(averaged_fids.attrs)
1570
+ self.fit_results = scaled_fids.xmr.fit_amares(prior_knowledge, **fit_kwargs)
1571
+ return self.fit_results
@@ -19,7 +19,7 @@ matplotlib's::
19
19
 
20
20
  from importlib.metadata import PackageNotFoundError, version
21
21
 
22
- from . import GEExam, GESeries, fitting, plotting, testing, utils
22
+ from . import GEExam, GESeries, plotting, testing, utils
23
23
  from .GEExam import DMIExam, DMIinjExam, ExamBase, MS_DMIExam
24
24
  from .GESeries import (
25
25
  MRISeries,
@@ -40,7 +40,6 @@ __all__ = [
40
40
  # subpackages / modules
41
41
  "GEExam",
42
42
  "GESeries",
43
- "fitting",
44
43
  "plotting",
45
44
  "testing",
46
45
  "utils",