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

CHAP/runner.py CHANGED
@@ -21,25 +21,35 @@ class RunConfig():
21
21
  'outputdir': '.',
22
22
  'interactive': False,
23
23
  'log_level': 'INFO',
24
- 'profile': False}
24
+ 'profile': False,
25
+ 'spawn': 0}
25
26
 
26
- def __init__(self, config={}):
27
- """RunConfig constructor
27
+ def __init__(self, config={}, comm=None):
28
+ """RunConfig constructor.
28
29
 
29
- :param config: Pipeline configuration options
30
- :type config: dict
30
+ :param config: Pipeline configuration options,
31
+ defaults to `{}`.
32
+ :type config: dict, optional
33
+ :param comm: MPI communicator, defaults to `None`.
34
+ :type comm: mpi4py.MPI.Comm, optional
31
35
  """
32
36
  # System modules
33
37
  from tempfile import NamedTemporaryFile
34
38
 
39
+ # Make sure os.makedirs is only called from the root node
40
+ if comm is None:
41
+ rank = 0
42
+ else:
43
+ rank = comm.Get_rank()
35
44
  for opt in self.opts:
36
45
  setattr(self, opt, config.get(opt, self.opts[opt]))
37
46
 
38
47
  # Check if root exists (create it if not) and is readable
39
- if not os.path.isdir(self.root):
40
- os.makedirs(self.root)
41
- if not os.access(self.root, os.R_OK):
42
- raise OSError('root directory is not accessible for reading '
48
+ if not rank:
49
+ if not os.path.isdir(self.root):
50
+ os.makedirs(self.root)
51
+ if not os.access(self.root, os.R_OK):
52
+ raise OSError('root directory is not accessible for reading '
43
53
  f'({self.root})')
44
54
 
45
55
  # Check if inputdir exists and is readable
@@ -56,16 +66,21 @@ class RunConfig():
56
66
  if not os.path.isabs(self.outputdir):
57
67
  self.outputdir = os.path.realpath(
58
68
  os.path.join(self.root, self.outputdir))
59
- if not os.path.isdir(self.outputdir):
60
- os.makedirs(self.outputdir)
61
- try:
62
- tmpfile = NamedTemporaryFile(dir=self.outputdir)
63
- except:
64
- raise OSError('output directory is not accessible for writing '
65
- f'({self.outputdir})')
69
+ if not rank:
70
+ if not os.path.isdir(self.outputdir):
71
+ os.makedirs(self.outputdir)
72
+ try:
73
+ tmpfile = NamedTemporaryFile(dir=self.outputdir)
74
+ except:
75
+ raise OSError('output directory is not accessible for writing '
76
+ f'({self.outputdir})')
66
77
 
67
78
  self.log_level = self.log_level.upper()
68
79
 
80
+ # Make sure os.makedirs completes before continuing all nodes
81
+ if comm is not None:
82
+ comm.barrier()
83
+
69
84
  def parser():
70
85
  """Return an argument parser for the `CHAP` CLI. This parser has
71
86
  one argument: the input CHAP configuration file.
@@ -77,50 +92,93 @@ def parser():
77
92
  return parser
78
93
 
79
94
  def main():
80
- """Main function"""
95
+ """Main function."""
96
+ # Third party modules
97
+ try:
98
+ from mpi4py import MPI
99
+ have_mpi = True
100
+ comm = MPI.COMM_WORLD
101
+ except:
102
+ have_mpi = False
103
+ comm = None
104
+
81
105
  args = parser().parse_args()
82
106
 
83
- # read input config file
107
+ # Read the input config file
84
108
  configfile = args.config
85
109
  with open(configfile) as file:
86
110
  config = safe_load(file)
87
- run_config = RunConfig(config.get('config', {}))
111
+
112
+ # Check if run was a worker spawned by another Processor
113
+ run_config = RunConfig(config.get('config', {}), comm)
114
+ if have_mpi and run_config.spawn:
115
+ sub_comm = MPI.Comm.Get_parent()
116
+ common_comm = sub_comm.Merge(True)
117
+ # Read worker specific input config file
118
+ if run_config.spawn > 0:
119
+ with open(f'{configfile}_{common_comm.Get_rank()}') as file:
120
+ config = safe_load(file)
121
+ run_config = RunConfig(config.get('config', {}), common_comm)
122
+ else:
123
+ with open(f'{configfile}_{sub_comm.Get_rank()}') as file:
124
+ config = safe_load(file)
125
+ run_config = RunConfig(config.get('config', {}), comm)
126
+ else:
127
+ common_comm = comm
128
+
129
+ # Get the pipeline configurations
88
130
  pipeline_config = config.get('pipeline', [])
89
131
 
90
- # profiling setup
132
+ # Profiling setup
91
133
  if run_config.profile:
92
134
  from cProfile import runctx # python profiler
93
135
  from pstats import Stats # profiler statistics
94
- cmd = 'runner(run_config, pipeline_config)'
136
+ cmd = 'runner(run_config, pipeline_config, common_comm)'
95
137
  runctx(cmd, globals(), locals(), 'profile.dat')
96
138
  info = Stats('profile.dat')
97
139
  info.sort_stats('cumulative')
98
140
  info.print_stats()
99
141
  else:
100
- runner(run_config, pipeline_config)
142
+ runner(run_config, pipeline_config, common_comm)
143
+
144
+ # Disconnect the spawned worker
145
+ if have_mpi and run_config.spawn:
146
+ common_comm.barrier()
147
+ sub_comm.Disconnect()
101
148
 
102
- def runner(run_config, pipeline_config):
103
- """Main runner funtion
149
+ def runner(run_config, pipeline_config, comm=None):
150
+ """Main runner funtion.
104
151
 
105
- :param run_config: CHAP run configuration
106
- :type run_config: RunConfig
107
- :param pipeline_config: CHAP Pipeline configuration
152
+ :param run_config: CHAP run configuration.
153
+ :type run_config: CHAP.runner.RunConfig
154
+ :param pipeline_config: CHAP Pipeline configuration.
108
155
  :type pipeline_config: dict
156
+ :param comm: MPI communicator, defaults to `None`.
157
+ :type comm: mpi4py.MPI.Comm, optional
158
+ :return: The pipeline's returned data field.
109
159
  """
160
+ # System modules
161
+ from time import time
162
+
110
163
  # logging setup
111
164
  logger, log_handler = setLogger(run_config.log_level)
112
165
  logger.info(f'Input pipeline configuration: {pipeline_config}\n')
113
166
 
114
- # run pipeline
115
- run(pipeline_config,
167
+ # Run the pipeline
168
+ t0 = time()
169
+ data = run(pipeline_config,
116
170
  run_config.inputdir, run_config.outputdir, run_config.interactive,
117
- logger, run_config.log_level, log_handler)
171
+ logger, run_config.log_level, log_handler, comm)
172
+ logger.info(f'Executed "run" in {time()-t0:.3f} seconds')
173
+ return data
118
174
 
119
- def setLogger(log_level="INFO"):
120
- """
121
- Helper function to set CHAP logger
175
+ def setLogger(log_level='INFO'):
176
+ """Helper function to set CHAP logger.
122
177
 
123
- :param log_level: logger level, default INFO
178
+ :param log_level: Logger level, defaults to `"INFO"`.
179
+ :type log_level: str
180
+ :return: The CHAP logger and logging handler.
181
+ :rtype: logging.Logger, logging.StreamHandler
124
182
  """
125
183
  logger = logging.getLogger(__name__)
126
184
  log_level = getattr(logging, log_level.upper())
@@ -133,22 +191,46 @@ def setLogger(log_level="INFO"):
133
191
 
134
192
  def run(
135
193
  pipeline_config, inputdir=None, outputdir=None, interactive=False,
136
- logger=None, log_level=None, log_handler=None):
137
- """
138
- Run given pipeline_config
194
+ logger=None, log_level=None, log_handler=None, comm=None):
195
+ """Run a given pipeline_config.
139
196
 
140
- :param pipeline_config: CHAP pipeline config
197
+ :param pipeline_config: CHAP Pipeline configuration.
198
+ :type pipeline_config: dict
199
+ :param inputdir: Input directory, defaults to `None'`.
200
+ :type inputdir: str, optional
201
+ :param outputdir: Output directory, defaults to `None'`.
202
+ :type outputdir: str, optional
203
+ :param interactive: Allows for user interactions,
204
+ defaults to `False`.
205
+ :type interactive: bool, optional
206
+ :param logger: CHAP logger, defaults to `None`.
207
+ :type logger: logging.Logger, optional
208
+ :param log_level: Logger level, defaults to `None`.
209
+ :type log_level: str, optional
210
+ :param log_handler: logging handler, defaults to `None`.
211
+ :type log_handler: logging.StreamHandler, optional
212
+ :param comm: MPI communicator, defaults to `None`.
213
+ :type comm: mpi4py.MPI.Comm, optional
214
+ :return: The `data` field of the first item in the returned
215
+ list of pipeline items.
141
216
  """
142
217
  # System modules
143
218
  from tempfile import NamedTemporaryFile
144
219
 
220
+ # Make sure os.makedirs is only called from the root node
221
+ if comm is None:
222
+ rank = 0
223
+ else:
224
+ rank = comm.Get_rank()
225
+
145
226
  objects = []
146
227
  kwds = []
147
228
  for item in pipeline_config:
148
- # load individual object with given name from its module
229
+ # Load individual object with given name from its module
149
230
  kwargs = {'inputdir': inputdir,
150
231
  'outputdir': outputdir,
151
- 'interactive': interactive}
232
+ 'interactive': interactive,
233
+ 'comm': comm}
152
234
  if isinstance(item, dict):
153
235
  name = list(item.keys())[0]
154
236
  item_args = item[name]
@@ -156,39 +238,41 @@ def run(
156
238
  # "outputdir" and "interactive" with the item's arguments
157
239
  # joining "inputdir" and "outputdir" and giving precedence
158
240
  # for "interactive" in the latter
159
- if 'inputdir' in item_args:
160
- newinputdir = os.path.normpath(os.path.join(
161
- kwargs['inputdir'], item_args.pop('inputdir')))
162
- if not os.path.isdir(newinputdir):
163
- raise OSError(
164
- f'input directory does not exist ({newinputdir})')
165
- if not os.access(newinputdir, os.R_OK):
166
- raise OSError('input directory is not accessible for '
167
- f'reading ({newinputdir})')
168
- kwargs['inputdir'] = newinputdir
169
- if 'outputdir' in item_args:
170
- newoutputdir = os.path.normpath(os.path.join(
171
- kwargs['outputdir'], item_args.pop('outputdir')))
172
- if not os.path.isdir(newoutputdir):
173
- os.makedirs(newoutputdir)
174
- try:
175
- tmpfile = NamedTemporaryFile(dir=newoutputdir)
176
- except:
177
- raise OSError('output directory is not accessible for '
178
- f'writing ({newoutputdir})')
179
- kwargs['outputdir'] = newoutputdir
180
- kwargs = {**kwargs, **item_args}
241
+ if item_args is not None:
242
+ if 'inputdir' in item_args:
243
+ newinputdir = os.path.normpath(os.path.join(
244
+ kwargs['inputdir'], item_args.pop('inputdir')))
245
+ if not os.path.isdir(newinputdir):
246
+ raise OSError(
247
+ f'input directory does not exist ({newinputdir})')
248
+ if not os.access(newinputdir, os.R_OK):
249
+ raise OSError('input directory is not accessible for '
250
+ f'reading ({newinputdir})')
251
+ kwargs['inputdir'] = newinputdir
252
+ if 'outputdir' in item_args:
253
+ newoutputdir = os.path.normpath(os.path.join(
254
+ kwargs['outputdir'], item_args.pop('outputdir')))
255
+ if not rank:
256
+ if not os.path.isdir(newoutputdir):
257
+ os.makedirs(newoutputdir)
258
+ try:
259
+ tmpfile = NamedTemporaryFile(dir=newoutputdir)
260
+ except:
261
+ raise OSError('output directory is not accessible '
262
+ f'for writing ({newoutputdir})')
263
+ kwargs['outputdir'] = newoutputdir
264
+ kwargs = {**kwargs, **item_args}
181
265
  else:
182
266
  name = item
183
267
  if "users" in name:
184
- # load users module. This is required in CHAPaaS which can
268
+ # Load users module. This is required in CHAPaaS which can
185
269
  # have common area for users module. Otherwise, we will be
186
270
  # required to have invidual user's PYTHONPATHs to load user
187
271
  # processors.
188
272
  try:
189
273
  import users
190
274
  except ImportError:
191
- if logger:
275
+ if logger is not None:
192
276
  logger.error(f'Unable to load {name}')
193
277
  continue
194
278
  clsName = name.split('.')[-1]
@@ -199,23 +283,28 @@ def run(
199
283
  modName, clsName = name.split('.')
200
284
  module = __import__(f'CHAP.{modName}', fromlist=[clsName])
201
285
  obj = getattr(module, clsName)()
202
- if log_level:
286
+ if log_level is not None:
203
287
  obj.logger.setLevel(log_level)
204
- if log_handler:
288
+ if log_handler is not None:
205
289
  obj.logger.addHandler(log_handler)
206
- if logger:
290
+ if logger is not None:
207
291
  logger.info(f'Loaded {obj}')
208
292
  objects.append(obj)
209
293
  kwds.append(kwargs)
210
294
  pipeline = Pipeline(objects, kwds)
211
- if log_level:
295
+ if log_level is not None:
212
296
  pipeline.logger.setLevel(log_level)
213
- if log_handler:
297
+ if log_handler is not None:
214
298
  pipeline.logger.addHandler(log_handler)
215
- if logger:
299
+ if logger is not None:
216
300
  logger.info(f'Loaded {pipeline} with {len(objects)} items\n')
217
301
  logger.info(f'Calling "execute" on {pipeline}')
218
- pipeline.execute()
302
+
303
+ # Make sure os.makedirs completes before continuing all nodes
304
+ if comm is not None:
305
+ comm.barrier()
306
+
307
+ return pipeline.execute()[0]['data']
219
308
 
220
309
 
221
310
  if __name__ == '__main__':
CHAP/tomo/models.py CHANGED
@@ -36,7 +36,7 @@ class Detector(BaseModel):
36
36
  columns: conint(gt=0)
37
37
  pixel_size: conlist(
38
38
  item_type=confloat(gt=0, allow_inf_nan=False),
39
- min_items=1, max_items=2)
39
+ min_length=1, max_length=2)
40
40
  lens_magnification: confloat(gt=0, allow_inf_nan=False) = 1.0
41
41
 
42
42
 
@@ -53,8 +53,8 @@ class TomoReduceConfig(BaseModel):
53
53
  :type delta_theta: float, optional
54
54
  """
55
55
  img_row_bounds: Optional[
56
- conlist(item_type=conint(ge=-1), min_items=2, max_items=2)]
57
- delta_theta: Optional[confloat(gt=0, allow_inf_nan=False)]
56
+ conlist(item_type=conint(ge=-1), min_length=2, max_length=2)] = None
57
+ delta_theta: Optional[confloat(gt=0, allow_inf_nan=False)] = None
58
58
 
59
59
 
60
60
  class TomoFindCenterConfig(BaseModel):
@@ -84,19 +84,19 @@ class TomoFindCenterConfig(BaseModel):
84
84
  reconstruction in pixels, defaults to no filtering performed.
85
85
  :type ring_width: float, optional
86
86
  """
87
- center_stack_index: Optional[conint(ge=0)]
87
+ center_stack_index: Optional[conint(ge=0)] = None
88
88
  center_rows: Optional[conlist(
89
- item_type=conint(ge=0), min_items=2, max_items=2)]
89
+ item_type=conint(ge=0), min_length=2, max_length=2)] = None
90
90
  center_offsets: Optional[conlist(
91
91
  item_type=confloat(allow_inf_nan=False),
92
- min_items=2, max_items=2)]
93
- center_offset_min: Optional[confloat(allow_inf_nan=False)]
94
- center_offset_max: Optional[confloat(allow_inf_nan=False)]
92
+ min_length=2, max_length=2)] = None
93
+ center_offset_min: Optional[confloat(allow_inf_nan=False)] = None
94
+ center_offset_max: Optional[confloat(allow_inf_nan=False)] = None
95
95
  center_search_range: Optional[conlist(
96
96
  item_type=confloat(allow_inf_nan=False),
97
- min_items=1, max_items=3)]
98
- gaussian_sigma: Optional[confloat(ge=0, allow_inf_nan=False)]
99
- ring_width: Optional[confloat(ge=0, allow_inf_nan=False)]
97
+ min_length=1, max_length=3)] = None
98
+ gaussian_sigma: Optional[confloat(ge=0, allow_inf_nan=False)] = None
99
+ ring_width: Optional[confloat(ge=0, allow_inf_nan=False)] = None
100
100
 
101
101
 
102
102
  class TomoReconstructConfig(BaseModel):
@@ -126,15 +126,15 @@ class TomoReconstructConfig(BaseModel):
126
126
  :type ring_width: float, optional
127
127
  """
128
128
  x_bounds: Optional[
129
- conlist(item_type=conint(ge=-1), min_items=2, max_items=2)]
129
+ conlist(item_type=conint(ge=-1), min_length=2, max_length=2)] = None
130
130
  y_bounds: Optional[
131
- conlist(item_type=conint(ge=-1), min_items=2, max_items=2)]
131
+ conlist(item_type=conint(ge=-1), min_length=2, max_length=2)] = None
132
132
  z_bounds: Optional[
133
- conlist(item_type=conint(ge=-1), min_items=2, max_items=2)]
133
+ conlist(item_type=conint(ge=-1), min_length=2, max_length=2)] = None
134
134
  secondary_iters: conint(ge=0) = 0
135
- gaussian_sigma: Optional[confloat(ge=0, allow_inf_nan=False)]
136
- remove_stripe_sigma: Optional[confloat(ge=0, allow_inf_nan=False)]
137
- ring_width: Optional[confloat(ge=0, allow_inf_nan=False)]
135
+ gaussian_sigma: Optional[confloat(ge=0, allow_inf_nan=False)] = None
136
+ remove_stripe_sigma: Optional[confloat(ge=0, allow_inf_nan=False)] = None
137
+ ring_width: Optional[confloat(ge=0, allow_inf_nan=False)] = None
138
138
 
139
139
 
140
140
  class TomoCombineConfig(BaseModel):
@@ -150,11 +150,11 @@ class TomoCombineConfig(BaseModel):
150
150
  :type z_bounds: list[int], optional
151
151
  """
152
152
  x_bounds: Optional[
153
- conlist(item_type=conint(ge=-1), min_items=2, max_items=2)]
153
+ conlist(item_type=conint(ge=-1), min_length=2, max_length=2)] = None
154
154
  y_bounds: Optional[
155
- conlist(item_type=conint(ge=-1), min_items=2, max_items=2)]
155
+ conlist(item_type=conint(ge=-1), min_length=2, max_length=2)] = None
156
156
  z_bounds: Optional[
157
- conlist(item_type=conint(ge=-1), min_items=2, max_items=2)]
157
+ conlist(item_type=conint(ge=-1), min_length=2, max_length=2)] = None
158
158
 
159
159
 
160
160
  class TomoSimConfig(BaseModel):
@@ -186,19 +186,19 @@ class TomoSimConfig(BaseModel):
186
186
  :type beam_intensity: float, optional
187
187
  :ivar background_intensity: Background intensity in counts,
188
188
  defaults to 20.
189
- :type background_intensity:: float, optional
189
+ :type background_intensity: float, optional
190
190
  :ivar slit_size: Vertical beam height in mm, defaults to 1.0.
191
- :type slit_size:: float, optional
191
+ :type slit_size: float, optional
192
192
  """
193
193
  station: Literal['id1a3', 'id3a', 'id3b']
194
- detector: Detector.construct()
194
+ detector: Detector.model_construct()
195
195
  sample_type: Literal[
196
196
  'square_rod', 'square_pipe', 'hollow_cube', 'hollow_brick',
197
197
  'hollow_pyramid']
198
198
  sample_size: conlist(
199
199
  item_type=confloat(gt=0, allow_inf_nan=False),
200
- min_items=1, max_items=3)
201
- wall_thickness: Optional[confloat(ge=0, allow_inf_nan=False)]
200
+ min_length=1, max_length=3)
201
+ wall_thickness: Optional[confloat(ge=0, allow_inf_nan=False)] = None
202
202
  mu: Optional[confloat(gt=0, allow_inf_nan=False)] = 0.05
203
203
  theta_step: confloat(gt=0, allow_inf_nan=False)
204
204
  beam_intensity: Optional[confloat(gt=0, allow_inf_nan=False)] = 1.e9