| Total Complexity | 51 |
| Total Lines | 263 |
| Duplicated Lines | 32.7 % |
| Changes | 3 | ||
| Bugs | 0 | Features | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like PulseExtractor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | # -*- coding: utf-8 -*- |
||
| 62 | class PulseExtractor(PulseExtractorBase): |
||
| 63 | """ |
||
| 64 | Management class to automatically combine and interface extraction methods and associated |
||
| 65 | parameters from extractor classes defined in several modules. |
||
| 66 | |||
| 67 | Extractor class to import from must comply to the following rules: |
||
| 68 | 1) Exclusive inheritance from PulseExtractorBase class |
||
| 69 | 2) No direct access to PulsedMeasurementLogic instance except through properties defined in |
||
| 70 | base class (read-only access) |
||
| 71 | 3) Extraction methods must be bound instance methods |
||
| 72 | 4) Extraction methods must be named starting with "ungated_" or "gated_" accordingly |
||
| 73 | 5) Extraction methods must have as first argument "count_data" |
||
| 74 | 6) Apart from "count_data" extraction methods must have exclusively keyword arguments with |
||
| 75 | default values of the right data type. (e.g. differentiate between 42 (int) and 42.0 (float)) |
||
| 76 | 7) Make sure that no two extraction methods in any module share a keyword argument of different |
||
| 77 | default data type. |
||
| 78 | 8) The keyword "method" must not be used in the extraction method parameters |
||
| 79 | |||
| 80 | See BasicPulseExtractor class for an example usage. |
||
| 81 | """ |
||
| 82 | |||
| 83 | def __init__(self, pulsedmeasurementlogic): |
||
| 84 | # Init base class |
||
| 85 | super().__init__(pulsedmeasurementlogic) |
||
| 86 | |||
| 87 | # Dictionaries holding references to the extraction methods |
||
| 88 | self._gated_extraction_methods = dict() |
||
| 89 | self._ungated_extraction_methods = dict() |
||
| 90 | # dictionary containing all possible parameters that can be used by the extraction methods |
||
| 91 | self._parameters = dict() |
||
| 92 | # Currently selected extraction method |
||
| 93 | self._current_extraction_method = None |
||
| 94 | |||
| 95 | # import path for extraction modules from default directory (logic.pulse_extraction_methods) |
||
| 96 | path_list = [os.path.join(get_main_dir(), 'logic', 'pulsed', 'pulse_extraction_methods')] |
||
| 97 | # import path for extraction modules from non-default directory if a path has been given |
||
| 98 | if isinstance(pulsedmeasurementlogic.extraction_import_path, str): |
||
| 99 | path_list.append(pulsedmeasurementlogic.extraction_import_path) |
||
| 100 | |||
| 101 | # Import extraction modules and get a list of extractor classes |
||
| 102 | extractor_classes = self.__import_external_extractors(paths=path_list) |
||
| 103 | |||
| 104 | # create an instance of each class and put them in a temporary list |
||
| 105 | extractor_instances = [cls(pulsedmeasurementlogic) for cls in extractor_classes] |
||
| 106 | |||
| 107 | # add references to all extraction methods in each instance to a dict |
||
| 108 | self.__populate_method_dicts(instance_list=extractor_instances) |
||
| 109 | |||
| 110 | # populate "_parameters" dictionary from extraction method signatures |
||
| 111 | self.__populate_parameter_dict() |
||
| 112 | |||
| 113 | # Set default extraction method |
||
| 114 | if self.is_gated: |
||
| 115 | self._current_extraction_method = sorted(self._gated_extraction_methods)[0] |
||
| 116 | else: |
||
| 117 | self._current_extraction_method = sorted(self._ungated_extraction_methods)[0] |
||
| 118 | |||
| 119 | # Update from parameter_dict if handed over |
||
| 120 | if isinstance(pulsedmeasurementlogic.extraction_parameters, dict): |
||
| 121 | # Delete unused parameters |
||
| 122 | params = [p for p in pulsedmeasurementlogic.extraction_parameters if |
||
| 123 | p not in self._parameters and p != 'method'] |
||
| 124 | for param in params: |
||
| 125 | del pulsedmeasurementlogic.extraction_parameters[param] |
||
| 126 | # Update parameter dict and current method |
||
| 127 | self.extraction_settings = pulsedmeasurementlogic.extraction_parameters |
||
| 128 | return |
||
| 129 | |||
| 130 | @property |
||
| 131 | def extraction_settings(self): |
||
| 132 | """ |
||
| 133 | This property holds all parameters needed for the currently selected extraction_method as |
||
| 134 | well as the currently selected method name. |
||
| 135 | |||
| 136 | @return dict: dictionary with keys being the parameter name and values being the parameter |
||
| 137 | """ |
||
| 138 | # Get reference to the extraction method |
||
| 139 | if self.is_gated: |
||
| 140 | method = self._gated_extraction_methods.get(self._current_extraction_method) |
||
| 141 | else: |
||
| 142 | method = self._ungated_extraction_methods.get(self._current_extraction_method) |
||
| 143 | |||
| 144 | # Get keyword arguments for the currently selected method |
||
| 145 | settings_dict = self._get_extraction_method_kwargs(method) |
||
| 146 | |||
| 147 | # Attach current extraction method name |
||
| 148 | settings_dict['method'] = self._current_extraction_method |
||
| 149 | return settings_dict |
||
| 150 | |||
| 151 | View Code Duplication | @extraction_settings.setter |
|
| 152 | def extraction_settings(self, settings_dict): |
||
| 153 | """ |
||
| 154 | Update parameters contained in self._parameters by values in settings_dict. |
||
| 155 | Also sets the current extraction method by passing its name using key "method". |
||
| 156 | Parameters not included in self._parameters (except "method") will be ignored. |
||
| 157 | |||
| 158 | @param dict settings_dict: dictionary containing the parameters to set (name, value) |
||
| 159 | """ |
||
| 160 | if not isinstance(settings_dict, dict): |
||
| 161 | return |
||
| 162 | |||
| 163 | # go through all key-value pairs in settings_dict and update self._parameters and |
||
| 164 | # self._current_extraction_method accordingly. Ignore unknown parameters. |
||
| 165 | for parameter, value in settings_dict.items(): |
||
| 166 | if parameter == 'method': |
||
| 167 | if (value in self._gated_extraction_methods and self.is_gated) or ( |
||
| 168 | value in self._ungated_extraction_methods and not self.is_gated): |
||
| 169 | self._current_extraction_method = value |
||
| 170 | else: |
||
| 171 | self.log.error('Extraction method "{0}" could not be found in PulseExtractor.' |
||
| 172 | ''.format(value)) |
||
| 173 | elif parameter in self._parameters: |
||
| 174 | self._parameters[parameter] = value |
||
| 175 | else: |
||
| 176 | self.log.warning('No extraction parameter "{0}" found in PulseExtractor.\n' |
||
| 177 | 'Parameter will be ignored.'.format(parameter)) |
||
| 178 | return |
||
| 179 | |||
| 180 | @property |
||
| 181 | def extraction_methods(self): |
||
| 182 | """ |
||
| 183 | Return available extraction methods depending on if the fast counter is gated or not. |
||
| 184 | |||
| 185 | @return dict: Dictionary with keys being the method names and values being the methods. |
||
| 186 | """ |
||
| 187 | if self.is_gated: |
||
| 188 | return self._gated_extraction_methods |
||
| 189 | else: |
||
| 190 | return self._ungated_extraction_methods |
||
| 191 | |||
| 192 | @property |
||
| 193 | def full_settings_dict(self): |
||
| 194 | """ |
||
| 195 | Returns the full set of parameters for all methods as well as the currently selected method |
||
| 196 | in order to store them in a StatusVar in PulsedMeasurementLogic. |
||
| 197 | |||
| 198 | @return dict: full set of parameters and currently selected extraction method. |
||
| 199 | """ |
||
| 200 | settings_dict = self._parameters.copy() |
||
| 201 | settings_dict['method'] = self._current_extraction_method |
||
| 202 | return settings_dict |
||
| 203 | |||
| 204 | def extract_laser_pulses(self, count_data): |
||
| 205 | """ |
||
| 206 | Wrapper method to call the currently selected extraction method with count_data and the |
||
| 207 | appropriate keyword arguments. |
||
| 208 | |||
| 209 | @param numpy.ndarray count_data: 1D (ungated) or 2D (gated) numpy array (dtype='int64') |
||
| 210 | containing the timetrace to extract laser pulses from. |
||
| 211 | @return dict: result dictionary of the extraction method |
||
| 212 | """ |
||
| 213 | if count_data.ndim > 1 and not self.is_gated: |
||
| 214 | self.log.error('"is_gated" flag is set to False but the count data to extract laser ' |
||
| 215 | 'pulses from is in the format of a gated timetrace (2D numpy array).') |
||
| 216 | elif count_data.ndim == 1 and self.is_gated: |
||
| 217 | self.log.error('"is_gated" flag is set to True but the count data to extract laser ' |
||
| 218 | 'pulses from is in the format of an ungated timetrace (1D numpy array).') |
||
| 219 | |||
| 220 | if self.is_gated: |
||
| 221 | extraction_method = self._gated_extraction_methods[self._current_extraction_method] |
||
| 222 | else: |
||
| 223 | extraction_method = self._ungated_extraction_methods[self._current_extraction_method] |
||
| 224 | kwargs = self._get_extraction_method_kwargs(extraction_method) |
||
| 225 | return extraction_method(count_data=count_data, **kwargs) |
||
| 226 | |||
| 227 | View Code Duplication | def _get_extraction_method_kwargs(self, method): |
|
| 228 | """ |
||
| 229 | Get the proper values for keyword arguments other than "count_data" for <method>. |
||
| 230 | Try to take the values from self._parameters. If the keyword is missing in the dictionary, |
||
| 231 | take the default values from the method signature. |
||
| 232 | |||
| 233 | @param method: reference to a callable extraction method |
||
| 234 | @return dict: A dictionary containing the argument keywords for <method> and corresponding |
||
| 235 | values from self._parameters. |
||
| 236 | """ |
||
| 237 | kwargs_dict = dict() |
||
| 238 | method_signature = inspect.signature(method) |
||
| 239 | for name in method_signature.parameters.keys(): |
||
| 240 | if name == 'count_data': |
||
| 241 | continue |
||
| 242 | |||
| 243 | default = method_signature.parameters[name].default |
||
| 244 | recalled = self._parameters.get(name) |
||
| 245 | |||
| 246 | if recalled is not None and type(recalled) == type(default): |
||
| 247 | kwargs_dict[name] = recalled |
||
| 248 | else: |
||
| 249 | kwargs_dict[name] = default |
||
| 250 | return kwargs_dict |
||
| 251 | |||
| 252 | View Code Duplication | def __import_external_extractors(self, paths): |
|
| 253 | """ |
||
| 254 | Helper method to import all modules from directories contained in paths. |
||
| 255 | Find all classes in those modules that inherit exclusively from PulseExtractorBase class |
||
| 256 | and return a list of them. |
||
| 257 | |||
| 258 | @param iterable paths: iterable containing paths to import modules from |
||
| 259 | @return list: A list of imported valid extractor classes |
||
| 260 | """ |
||
| 261 | class_list = list() |
||
| 262 | for path in paths: |
||
| 263 | if not os.path.exists(path): |
||
| 264 | self.log.error('Unable to import extraction methods from "{0}".\n' |
||
| 265 | 'Path does not exist.'.format(path)) |
||
| 266 | continue |
||
| 267 | # Get all python modules to import from. |
||
| 268 | # The assumption is that in the directory pulse_extraction_methods, there are |
||
| 269 | # *.py files, which contain only extractor classes! |
||
| 270 | module_list = [name[:-3] for name in os.listdir(path) if |
||
| 271 | os.path.isfile(os.path.join(path, name)) and name.endswith('.py')] |
||
| 272 | |||
| 273 | # append import path to sys.path |
||
| 274 | sys.path.append(path) |
||
| 275 | |||
| 276 | # Go through all modules and create instances of each class found. |
||
| 277 | for module_name in module_list: |
||
| 278 | # import module |
||
| 279 | mod = importlib.import_module('{0}'.format(module_name)) |
||
| 280 | importlib.reload(mod) |
||
| 281 | # get all extractor class references defined in the module |
||
| 282 | tmp_list = [m[1] for m in inspect.getmembers(mod, self.is_extractor_class)] |
||
| 283 | # append to class_list |
||
| 284 | class_list.extend(tmp_list) |
||
| 285 | return class_list |
||
| 286 | |||
| 287 | def __populate_method_dicts(self, instance_list): |
||
| 288 | """ |
||
| 289 | Helper method to populate the dictionaries containing all references to callable extraction |
||
| 290 | methods contained in extractor instances passed to this method. |
||
| 291 | |||
| 292 | @param list instance_list: List containing instances of extractor classes |
||
| 293 | """ |
||
| 294 | self._ungated_extraction_methods = dict() |
||
| 295 | self._gated_extraction_methods = dict() |
||
| 296 | for instance in instance_list: |
||
| 297 | for method_name, method_ref in inspect.getmembers(instance, inspect.ismethod): |
||
| 298 | if method_name.startswith('gated_'): |
||
| 299 | self._gated_extraction_methods[method_name[6:]] = method_ref |
||
| 300 | elif method_name.startswith('ungated_'): |
||
| 301 | self._ungated_extraction_methods[method_name[8:]] = method_ref |
||
| 302 | return |
||
| 303 | |||
| 304 | def __populate_parameter_dict(self): |
||
| 305 | """ |
||
| 306 | Helper method to populate the dictionary containing all possible keyword arguments from all |
||
| 307 | extraction methods. |
||
| 308 | """ |
||
| 309 | self._parameters = dict() |
||
| 310 | for method in self._ungated_extraction_methods.values(): |
||
| 311 | self._parameters.update(self._get_extraction_method_kwargs(method=method)) |
||
| 312 | return |
||
| 313 | |||
| 314 | @staticmethod |
||
| 315 | def is_extractor_class(obj): |
||
| 316 | """ |
||
| 317 | Helper method to check if an object is a valid extractor class. |
||
| 318 | |||
| 319 | @param object obj: object to check |
||
| 320 | @return bool: True if obj is a valid extractor class, False otherwise |
||
| 321 | """ |
||
| 322 | if inspect.isclass(obj): |
||
| 323 | return PulseExtractorBase in obj.__bases__ and len(obj.__bases__) == 1 |
||
| 324 | return False |
||
| 325 |