ChessAnalysisPipeline 0.0.14__py3-none-any.whl → 0.0.15__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.

Potentially problematic release.


This version of ChessAnalysisPipeline might be problematic. Click here for more details.

@@ -0,0 +1,6 @@
1
+ """This subpackage contains pieces for communication with FOXDEN services.
2
+ """
3
+
4
+ from CHAP.foxden.processor import (
5
+ FoxdenProvenanceProcessor,
6
+ )
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env python
2
+ #-*- coding: utf-8 -*-
3
+ #pylint: disable=
4
+ """
5
+ File : processor.py
6
+ Author : Valentin Kuznetsov <vkuznet AT gmail dot com>
7
+ Description: Processor module for FOXDEN services
8
+ """
9
+
10
+ # system modules
11
+ from time import time
12
+
13
+ # local modules
14
+ from CHAP import Processor
15
+ from CHAP.foxden.writer import FoxdenWriter
16
+
17
+
18
+ class FoxdenProvenanceProcessor(Processor):
19
+ """A Processor to communicate with FOXDEN provenance server."""
20
+ # def __init__(self):
21
+ # self.writer = FoxdenWriter()
22
+
23
+ def process(self, data, url, dryRun=False, verbose=False):
24
+ """process data API"""
25
+
26
+ t0 = time()
27
+ self.logger.info(f'Executing "process" with url {url} data {data} dryrun {dryRun}')
28
+ writer = FoxdenWriter()
29
+
30
+ # data = self.writer.write(data, url, dryRun)
31
+ data = writer.write(data, url, dryRun=dryRun)
32
+
33
+ self.logger.info(f'Finished "process" in {time()-t0:.3f} seconds\n')
34
+
35
+ return data
36
+
37
+
38
+ if __name__ == '__main__':
39
+ # local modules
40
+ from CHAP.processor import main
41
+
42
+ main()
CHAP/foxden/writer.py ADDED
@@ -0,0 +1,65 @@
1
+ """FOXDE command line writer."""
2
+
3
+ # system modules
4
+ import os
5
+
6
+ # Local modules
7
+ from CHAP.writer import main
8
+
9
+ class FoxdenWriter():
10
+ """FOXDEN writer writes data to specific FOXDEN service
11
+ """
12
+
13
+ def write(self, data, url, method="POST", headers={}, timeout=10, dryRun=False):
14
+ """Write the input data as text to a file.
15
+
16
+ :param data: input data
17
+ :type data: list[PipelineData]
18
+ :param url: url of service
19
+ :type url: str
20
+ :param method: HTTP method to use, POST for creation and PUT for update
21
+ :type method: str
22
+ :param headers: HTTP headers to use
23
+ :type headers: dictionary
24
+ :param timeout: timeout of HTTP request
25
+ :type timeout: str
26
+ :param dryRun: dryRun option to verify HTTP workflow
27
+ :type dryRun: boolean
28
+ :return: contents of the input data
29
+ :rtype: object
30
+ """
31
+ import requests
32
+ if 'Content-Type' not in headers:
33
+ headers['Content-type'] = 'application/json'
34
+ if 'Accept' not in headers:
35
+ headers['Accept'] = 'application/json'
36
+ if dryRun:
37
+ print("### HTTP writer call", url, headers, data)
38
+ return []
39
+ token = ""
40
+ fname = os.getenv("CHESS_WRITE_TOKEN")
41
+ if not fname:
42
+ msg = f'CHESS_WRITE_TOKEN env variable is not set'
43
+ raise Exception(msg)
44
+ with open(fname, 'r') as istream:
45
+ token = istream.read()
46
+ if token:
47
+ headers["Authorization"] = f"Bearer {token}"
48
+ else:
49
+ msg = f'No valid write token found in CHESS_WRITE_TOKEN env variable'
50
+ raise Exception(msg)
51
+
52
+ # make actual HTTP request to FOXDEN service
53
+ if method.lower() == 'post':
54
+ resp = requests.post(url, headers=headers, timeout=timeout, data=data)
55
+ elif method.lower() == 'put':
56
+ resp = requests.put(url, headers=headers, timeout=timeout, data=data)
57
+ else:
58
+ msg = f"unsupporteed method {method}"
59
+ raise Exception(msg)
60
+ data = resp.content
61
+ return data
62
+
63
+
64
+ if __name__ == '__main__':
65
+ main()
CHAP/pipeline.py CHANGED
@@ -227,7 +227,7 @@ class MultiplePipelineItem(PipelineItem):
227
227
  outputdir = os.path.normpath(os.path.join(
228
228
  args['outputdir'], item_args.pop('outputdir')))
229
229
  if not os.path.isdir(outputdir):
230
- os.mkdir(outputdir)
230
+ os.makedirs(outputdir)
231
231
  try:
232
232
  tmpfile = NamedTemporaryFile(dir=outputdir)
233
233
  except:
CHAP/runner.py CHANGED
@@ -37,7 +37,7 @@ class RunConfig():
37
37
 
38
38
  # Check if root exists (create it if not) and is readable
39
39
  if not os.path.isdir(self.root):
40
- os.mkdir(self.root)
40
+ os.makedirs(self.root)
41
41
  if not os.access(self.root, os.R_OK):
42
42
  raise OSError('root directory is not accessible for reading '
43
43
  f'({self.root})')
@@ -57,7 +57,7 @@ class RunConfig():
57
57
  self.outputdir = os.path.realpath(
58
58
  os.path.join(self.root, self.outputdir))
59
59
  if not os.path.isdir(self.outputdir):
60
- os.mkdir(self.outputdir)
60
+ os.makedirs(self.outputdir)
61
61
  try:
62
62
  tmpfile = NamedTemporaryFile(dir=self.outputdir)
63
63
  except:
@@ -127,7 +127,7 @@ def setLogger(log_level="INFO"):
127
127
  logger.setLevel(log_level)
128
128
  log_handler = logging.StreamHandler()
129
129
  log_handler.setFormatter(logging.Formatter(
130
- '{name:20}: {message}', style='{'))
130
+ '{name:20}: {levelname}: {message}', style='{'))
131
131
  logger.addHandler(log_handler)
132
132
  return logger, log_handler
133
133
 
@@ -170,7 +170,7 @@ def run(
170
170
  newoutputdir = os.path.normpath(os.path.join(
171
171
  kwargs['outputdir'], item_args.pop('outputdir')))
172
172
  if not os.path.isdir(newoutputdir):
173
- os.mkdir(newoutputdir)
173
+ os.makedirs(newoutputdir)
174
174
  try:
175
175
  tmpfile = NamedTemporaryFile(dir=newoutputdir)
176
176
  except:
CHAP/tomo/models.py CHANGED
@@ -7,7 +7,6 @@ from typing import (
7
7
  )
8
8
  from pydantic import (
9
9
  BaseModel,
10
- StrictBool,
11
10
  conint,
12
11
  conlist,
13
12
  confloat,
@@ -168,9 +167,11 @@ class TomoSimConfig(BaseModel):
168
167
  :type detector: Detector
169
168
  :ivar sample_type: Sample type for the tomography simulator.
170
169
  :type sample_type: Literal['square_rod', 'square_pipe',
171
- 'hollow_cube', 'hollow_brick']
170
+ 'hollow_cube', 'hollow_brick', 'hollow_pyramid']
172
171
  :ivar sample_size: Size of each sample dimension in mm (internally
173
- converted to an integer number of pixels).
172
+ converted to an integer number of pixels). Enter three values
173
+ for sample_type == `'hollow_pyramid'`, the height and the side
174
+ at the respective bottom and the top of the pyramid.
174
175
  :type sample_size: list[float]
175
176
  :ivar wall_thickness: Wall thickness for pipe, cube, and brick in
176
177
  mm (internally converted to an integer number of pixels).
@@ -192,10 +193,11 @@ class TomoSimConfig(BaseModel):
192
193
  station: Literal['id1a3', 'id3a', 'id3b']
193
194
  detector: Detector.construct()
194
195
  sample_type: Literal[
195
- 'square_rod', 'square_pipe', 'hollow_cube', 'hollow_brick']
196
+ 'square_rod', 'square_pipe', 'hollow_cube', 'hollow_brick',
197
+ 'hollow_pyramid']
196
198
  sample_size: conlist(
197
199
  item_type=confloat(gt=0, allow_inf_nan=False),
198
- min_items=1, max_items=2)
200
+ min_items=1, max_items=3)
199
201
  wall_thickness: Optional[confloat(ge=0, allow_inf_nan=False)]
200
202
  mu: Optional[confloat(gt=0, allow_inf_nan=False)] = 0.05
201
203
  theta_step: confloat(gt=0, allow_inf_nan=False)
CHAP/tomo/processor.py CHANGED
@@ -8,7 +8,6 @@ Description: Module for Processors used only by tomography experiments
8
8
  """
9
9
 
10
10
  # System modules
11
- from os import mkdir
12
11
  from os import path as os_path
13
12
  from sys import exit as sys_exit
14
13
  from time import time
@@ -31,7 +30,6 @@ from CHAP.utils.general import (
31
30
  quick_imshow,
32
31
  nxcopy,
33
32
  )
34
- from CHAP.utils.fit import Fit
35
33
  from CHAP.processor import Processor
36
34
  from CHAP.reader import main
37
35
 
@@ -1395,28 +1393,28 @@ class Tomo:
1395
1393
 
1396
1394
  # Resize the combined tomography data stacks
1397
1395
  # - combined axis data order: row/-z,y,x
1398
- if self._interactive:
1396
+ if self._interactive or self._save_figs:
1399
1397
  x_bounds, y_bounds, z_bounds = self._resize_reconstructed_data(
1400
1398
  tomo_recon_combined, combine_data=True)
1401
1399
  else:
1402
1400
  x_bounds = tool_config.x_bounds
1403
1401
  if x_bounds is None:
1404
1402
  self._logger.warning(
1405
- 'x_bounds unspecified, reconstruct data for full x-range')
1403
+ 'x_bounds unspecified, combine data for full x-range')
1406
1404
  elif not is_int_pair(
1407
1405
  x_bounds, ge=0, le=tomo_shape[2]):
1408
1406
  raise ValueError(f'Invalid parameter x_bounds ({x_bounds})')
1409
1407
  y_bounds = tool_config.y_bounds
1410
1408
  if y_bounds is None:
1411
1409
  self._logger.warning(
1412
- 'y_bounds unspecified, reconstruct data for full y-range')
1410
+ 'y_bounds unspecified, combine data for full y-range')
1413
1411
  elif not is_int_pair(
1414
1412
  y_bounds, ge=0, le=tomo_shape[1]):
1415
1413
  raise ValueError(f'Invalid parameter y_bounds ({y_bounds})')
1416
1414
  z_bounds = tool_config.z_bounds
1417
1415
  if z_bounds is None:
1418
1416
  self._logger.warning(
1419
- 'z_bounds unspecified, reconstruct data for full z-range')
1417
+ 'z_bounds unspecified, combine data for full z-range')
1420
1418
  elif not is_int_pair(
1421
1419
  z_bounds, ge=0, le=tomo_shape[0]):
1422
1420
  raise ValueError(f'Invalid parameter z_bounds ({z_bounds})')
@@ -1706,18 +1704,46 @@ class Tomo:
1706
1704
  img_row_bounds = calibrate_center_rows
1707
1705
  else:
1708
1706
  if nxentry.instrument.source.attrs['station'] in ('id1a3', 'id3a'):
1707
+ # System modules
1708
+ from sys import float_info
1709
+
1710
+ # Third party modules
1711
+ from nexusformat.nexus import (
1712
+ NXdata,
1713
+ NXfield,
1714
+ )
1715
+
1716
+ # Local modules
1717
+ from CHAP.utils.fit import FitProcessor
1718
+
1709
1719
  pixel_size = float(nxentry.instrument.detector.row_pixel_size)
1710
1720
  # Try to get a fit from the bright field
1711
1721
  row_sum = np.sum(tbf, 1)
1712
- fit = Fit.fit_data(
1713
- row_sum, 'rectangle', x=np.array(range(len(row_sum))),
1714
- form='atan', guess=True)
1715
- parameters = fit.best_values
1722
+ num = len(row_sum)
1723
+ fit = FitProcessor()
1724
+ model = {'model': 'rectangle',
1725
+ 'parameters': [
1726
+ {'name': 'amplitude',
1727
+ 'value': row_sum.max()-row_sum.min(),
1728
+ 'min': 0.0},
1729
+ {'name': 'center1', 'value': 0.25*num,
1730
+ 'min': 0.0, 'max': num},
1731
+ {'name': 'sigma1', 'value': num/7.0,
1732
+ 'min': float_info.min},
1733
+ {'name': 'center2', 'value': 0.75*num,
1734
+ 'min': 0.0, 'max': num},
1735
+ {'name': 'sigma2', 'value': num/7.0,
1736
+ 'min': float_info.min}]}
1737
+ bounds_fit = fit.process(
1738
+ NXdata(NXfield(row_sum, 'y'),
1739
+ NXfield(np.array(range(num)), 'x')),
1740
+ {'models': [model], 'method': 'trf'})
1741
+ parameters = bounds_fit.best_values
1716
1742
  row_low_fit = parameters.get('center1', None)
1717
1743
  row_upp_fit = parameters.get('center2', None)
1718
1744
  sig_low = parameters.get('sigma1', None)
1719
1745
  sig_upp = parameters.get('sigma2', None)
1720
- have_fit = (fit.success and row_low_fit is not None
1746
+ have_fit = (bounds_fit.success and row_low_fit is not None
1721
1747
  and row_upp_fit is not None and sig_low is not None
1722
1748
  and sig_upp is not None
1723
1749
  and 0 <= row_low_fit < row_upp_fit <= row_sum.size
@@ -1787,7 +1813,7 @@ class Tomo:
1787
1813
  if calibrate_center_rows:
1788
1814
  title='Select two detector image row indices to '\
1789
1815
  'calibrate rotation axis (in range '\
1790
- f'[0, {first_image.shape[0]}])'
1816
+ f'[0, {first_image.shape[0]-1}])'
1791
1817
  else:
1792
1818
  title='Select detector image row bounds for data '\
1793
1819
  f'reduction (in range [0, {first_image.shape[0]}])'
@@ -2452,8 +2478,9 @@ class Tomo:
2452
2478
  f'{selected_center_offset:.2f}.png'))
2453
2479
  plt.close()
2454
2480
 
2481
+ del recon_planes
2482
+
2455
2483
  del sinogram
2456
- del recon_planes
2457
2484
 
2458
2485
  # Return the center location
2459
2486
  if self._interactive:
@@ -2834,16 +2861,16 @@ class Tomo:
2834
2861
  # Selecting x an y bounds (in z-plane)
2835
2862
  if x_bounds is None:
2836
2863
  if not self._interactive:
2837
- self._logger.warning('x_bounds unspecified, reconstruct '
2838
- 'data for full x-range')
2864
+ self._logger.warning('x_bounds unspecified, use data for '
2865
+ 'full x-range')
2839
2866
  x_bounds = (0, tomo_recon_stacks[0].shape[2])
2840
2867
  elif not is_int_pair(
2841
2868
  x_bounds, ge=0, le=tomo_recon_stacks[0].shape[2]):
2842
2869
  raise ValueError(f'Invalid parameter x_bounds ({x_bounds})')
2843
2870
  if y_bounds is None:
2844
2871
  if not self._interactive:
2845
- self._logger.warning('y_bounds unspecified, reconstruct '
2846
- 'data for full y-range')
2872
+ self._logger.warning('y_bounds unspecified, use data for '
2873
+ 'full y-range')
2847
2874
  y_bounds = (0, tomo_recon_stacks[0].shape[1])
2848
2875
  elif not is_int_pair(
2849
2876
  y_bounds, ge=0, le=tomo_recon_stacks[0].shape[1]):
@@ -2865,11 +2892,20 @@ class Tomo:
2865
2892
  tomosum = 0
2866
2893
  for i in range(num_tomo_stacks):
2867
2894
  tomosum = tomosum + np.sum(tomo_recon_stacks[i], axis=0)
2868
- fig, roi = select_roi_2d(
2895
+ if self._save_figs:
2896
+ if combine_data:
2897
+ filename = os_path.join(
2898
+ self._outputdir, 'combined_data_xy_roi.png')
2899
+ else:
2900
+ filename = os_path.join(
2901
+ self._outputdir, 'reconstructed_data_xy_roi.png')
2902
+ else:
2903
+ filename = None
2904
+ roi = select_roi_2d(
2869
2905
  tomosum, preselected_roi=preselected_roi,
2870
2906
  title_a='Reconstructed data summed over z',
2871
2907
  row_label='y', column_label='x',
2872
- interactive=self._interactive)
2908
+ interactive=self._interactive, filename=filename)
2873
2909
  if roi is None:
2874
2910
  x_bounds = (0, tomo_recon_stacks[0].shape[2])
2875
2911
  y_bounds = (0, tomo_recon_stacks[0].shape[1])
@@ -2878,12 +2914,6 @@ class Tomo:
2878
2914
  y_bounds = (int(roi[2]), int(roi[3]))
2879
2915
  self._logger.debug(f'x_bounds = {x_bounds}')
2880
2916
  self._logger.debug(f'y_bounds = {y_bounds}')
2881
- # Plot results
2882
- if self._save_figs:
2883
- fig.savefig(
2884
- os_path.join(
2885
- self._outputdir, 'reconstructed_data_xy_roi.png'))
2886
- plt.close()
2887
2917
 
2888
2918
  # Selecting z bounds (in xy-plane)
2889
2919
  # (only valid for a single image stack or when combining a stack)
@@ -2905,17 +2935,20 @@ class Tomo:
2905
2935
  tomosum = 0
2906
2936
  for i in range(num_tomo_stacks):
2907
2937
  tomosum = tomosum + np.sum(tomo_recon_stacks[i], axis=(1,2))
2908
- fig, z_bounds = select_roi_1d(
2938
+ if self._save_figs:
2939
+ if combine_data:
2940
+ filename = os_path.join(
2941
+ self._outputdir, 'combined_data_z_roi.png')
2942
+ else:
2943
+ filename = os_path.join(
2944
+ self._outputdir, 'reconstructed_data_z_roi.png')
2945
+ else:
2946
+ filename = None
2947
+ z_bounds = select_roi_1d(
2909
2948
  tomosum, preselected_roi=z_bounds,
2910
2949
  xlabel='z', ylabel='Reconstructed data summed over x and y',
2911
- interactive=self._interactive)
2950
+ interactive=self._interactive, filename=filename)
2912
2951
  self._logger.debug(f'z_bounds = {z_bounds}')
2913
- # Plot results
2914
- if self._save_figs:
2915
- fig.savefig(
2916
- os_path.join(
2917
- self._outputdir, 'reconstructed_data_z_roi.png'))
2918
- plt.close()
2919
2952
 
2920
2953
  return x_bounds, y_bounds, z_bounds
2921
2954
 
@@ -2957,6 +2990,9 @@ class TomoSimFieldProcessor(Processor):
2957
2990
  sample_size = config.sample_size
2958
2991
  if len(sample_size) == 1:
2959
2992
  sample_size = (sample_size[0], sample_size[0])
2993
+ if sample_type == 'hollow_pyramid' and len(sample_size) != 3:
2994
+ raise ValueError('Invalid combindation of sample_type '
2995
+ f'({sample_type}) and sample_size ({sample_size}')
2960
2996
  wall_thickness = config.wall_thickness
2961
2997
  mu = config.mu
2962
2998
  theta_step = config.theta_step
@@ -2993,15 +3029,22 @@ class TomoSimFieldProcessor(Processor):
2993
3029
 
2994
3030
  # Get the number of horizontal stacks bases on the diagonal
2995
3031
  # of the square and for now don't allow more than one
2996
- num_tomo_stack = 1 + int((sample_size[1]*np.sqrt(2)-pixel_size[1])
2997
- / (detector_size[1]*pixel_size[1]))
3032
+ if (sample_size) == 3:
3033
+ num_tomo_stack = 1 + int(
3034
+ (max(sample_size[1:2])*np.sqrt(2)-pixel_size[1])
3035
+ / (detector_size[1]*pixel_size[1]))
3036
+ else:
3037
+ num_tomo_stack = 1 + int((sample_size[1]*np.sqrt(2)-pixel_size[1])
3038
+ / (detector_size[1]*pixel_size[1]))
2998
3039
  if num_tomo_stack > 1:
2999
3040
  raise ValueError('Sample is too wide for the detector')
3000
3041
 
3001
3042
  # Create the x-ray path length through a solid square
3002
3043
  # crosssection for a set of rotation angles.
3003
- path_lengths_solid = self._create_pathlength_solid_square(
3004
- sample_size[1], thetas, pixel_size[1], detector_size[1])
3044
+ path_lengths_solid = None
3045
+ if sample_type != 'hollow_pyramid':
3046
+ path_lengths_solid = self._create_pathlength_solid_square(
3047
+ sample_size[1], thetas, pixel_size[1], detector_size[1])
3005
3048
 
3006
3049
  # Create the x-ray path length through a hollow square
3007
3050
  # crosssection for a set of rotation angles.
@@ -3027,7 +3070,12 @@ class TomoSimFieldProcessor(Processor):
3027
3070
  num_theta = len(thetas)
3028
3071
  vertical_shifts = []
3029
3072
  tomo_fields_stack = []
3030
- img_dim = (len(img_row_coords), path_lengths_solid.shape[1])
3073
+ len_img_y = (detector_size[1]+1)//2
3074
+ if len_img_y%2:
3075
+ len_img_y = 2*len_img_y - 1
3076
+ else:
3077
+ len_img_y = 2*len_img_y
3078
+ img_dim = (len(img_row_coords), len_img_y)
3031
3079
  intensities_solid = None
3032
3080
  intensities_hollow = None
3033
3081
  for n in range(num_tomo_stack):
@@ -3044,6 +3092,37 @@ class TomoSimFieldProcessor(Processor):
3044
3092
  beam_intensity * np.exp(-mu*path_lengths_hollow)
3045
3093
  for n in range(num_theta):
3046
3094
  tomo_field[n,:,:] = intensities_hollow[n]
3095
+ elif sample_type == 'hollow_pyramid':
3096
+ outer_indices = \
3097
+ np.where(abs(img_row_coords) <= sample_size[0]/2)[0]
3098
+ inner_indices = np.where(
3099
+ abs(img_row_coords) < sample_size[0]/2 - wall_thickness)[0]
3100
+ wall_indices = list(set(outer_indices)-set(inner_indices))
3101
+ ratio = abs(sample_size[1]-sample_size[2])/sample_size[0]
3102
+ baselength = max(sample_size[1:2])
3103
+ for i in wall_indices:
3104
+ path_lengths_solid = self._create_pathlength_solid_square(
3105
+ baselength - ratio*(
3106
+ img_row_coords[i] + 0.5*sample_size[0]),
3107
+ thetas, pixel_size[1], detector_size[1])
3108
+ intensities_solid = \
3109
+ beam_intensity * np.exp(-mu*path_lengths_solid)
3110
+ for n in range(num_theta):
3111
+ tomo_field[n,i] = intensities_solid[n]
3112
+ for i in inner_indices:
3113
+ path_lengths_hollow = (
3114
+ self._create_pathlength_solid_square(
3115
+ baselength - ratio*(
3116
+ img_row_coords[i] + 0.5*sample_size[0]),
3117
+ thetas, pixel_size[1], detector_size[1])
3118
+ - self._create_pathlength_solid_square(
3119
+ baselength - 2*wall_thickness - ratio*(
3120
+ img_row_coords[i] + 0.5*sample_size[0]),
3121
+ thetas, pixel_size[1], detector_size[1]))
3122
+ intensities_hollow = \
3123
+ beam_intensity * np.exp(-mu*path_lengths_hollow)
3124
+ for n in range(num_theta):
3125
+ tomo_field[n,i] = intensities_hollow[n]
3047
3126
  else:
3048
3127
  intensities_solid = \
3049
3128
  beam_intensity * np.exp(-mu*path_lengths_solid)
@@ -3136,7 +3215,7 @@ class TomoSimFieldProcessor(Processor):
3136
3215
  """
3137
3216
  # Get the column coordinates
3138
3217
  img_y_coords = pixel_size * (0.5 * (1 - detector_size%2)
3139
- + np.asarray(range(int(0.5 * (detector_size+1)))))
3218
+ + np.asarray(range((detector_size+1)//2)))
3140
3219
 
3141
3220
  # Get the path lenghts for position column coordinates
3142
3221
  lengths = np.zeros((len(thetas), len(img_y_coords)), dtype=np.float64)
CHAP/utils/__init__.py CHANGED
@@ -2,3 +2,4 @@
2
2
  CHESS scan data, collecting interactive user input, and finding
3
3
  lattice properties of materials (among others).
4
4
  """
5
+ from CHAP.utils.fit import FitProcessor