def validate(self): """Validate parameters.""" super().validate() if "PMRA" not in self._parent.params or "PMDEC" not in self._parent.params: # Check ecliptic coordinates proper motion. if ("PMELONG" not in self._parent.params or "PMELAT" not in self._parent.params): raise MissingParameter( "DDK", "DDK model needs proper motion parameters.") if hasattr(self._parent, "PX"): if self._parent.PX.value <= 0.0 or self._parent.PX.value is None: raise TimingModelError("DDK model needs a valid `PX` value.") else: raise MissingParameter("Binary_DDK", "PX", "DDK model needs PX from" "Astrometry.")
def choose_model(self, param_inpar): """Choose the model components based on the parfile. Parameter --------- param_inpar: dict Dictionary of the unique parameters in .par file with the key is the parfile line. :func:`parse_parfile` returns this dictionary. Return ------ list List of selected components. dict Conflict components dictionary, where the key the component name, the value is a list of component names that are conflicted with the components in the key. list A list of parameters that are in the .parfile but not in the PINT defined parameters. Note ---- The selection algorithm: #. Look at the BINARY in the par file and catche the indicated binary model #. Translate para file parameters to the pint parameter name #. Go over the parameter-component map and pick up the components based on the parameters in parfile. #. Select the components that have its unique parameters in the parfile. In other words, select the components that have one parameter to on component mapping. #. Log the conflict components, one parameter to mulitple components mapping. """ selected_components = set() param_count = Counter() # 1. iteration read parfile with a no component timing_model to get # the overall control parameters. This will get us the binary model name # build the base fo the timing model # pint_param_dict, unknown_param = self._pintify_parfile(param_inpar) binary = param_inpar.get("BINARY", None) if binary is not None: binary = binary[0] binary_cp = self.all_components.search_binary_components(binary) selected_components.add(binary_cp.__class__.__name__) # 2. Get the component list from the parameters in the parfile. # 2.1 Check the aliases of input parameters. # This does not include the repeating parameters, but it should not # matter in the component selection. param_not_in_pint = [] # For parameters not initialized in PINT yet. param_components_inpar = {} for pp in param_inpar.keys(): try: p_name, first_init = self.all_components.alias_to_pint_param( pp) except UnknownParameter: param_not_in_pint.append(pp) continue param_count[p_name] += 1 # For the case that the indexed parameter maps to a component, but # the parameter with the provided index is not initialized yet. if p_name != first_init: param_not_in_pint.append(pp) p_cp = self.all_components.param_component_map.get( first_init, None) if p_cp: param_components_inpar[p_name] = p_cp # Back map the possible_components and the parameters in the parfile # This will remove the duplicate components. conflict_components = defaultdict(set) # graph for confilict for k, cps in param_components_inpar.items(): # If `timing_model` in param --> component mapping skip # Timing model is the base. if "timing_model" in cps: continue # Check if it is a binary component, if yes, skip. It is controlled # by the BINARY tag if self.all_components.components[ cps[0]].category == "pulsar_system": if binary is None: raise MissingBinaryError( f"Pulsar binary/pulsar system model is" f" decided by the parameter 'BINARY'. " f" Please indicate the binary model " f" before using parameter {k}, which is" f" a binary model parameter.") else: continue if len( cps ) == 1: # No conflict, parameter only shows in one component. selected_components.add(cps[0]) continue # Has conflict, same parameter shows in different components # Only record the conflict here and do nothing, if there is any # component unique parameter show in the parfile, the component will # be selected. if len(cps) > 1: # Add conflict to the conflict graph for cp in cps: temp_cf_cp = copy.deepcopy(cps) temp_cf_cp.remove(cp) conflict_components[cp].update(set(temp_cf_cp)) continue # Check if the selected component in the confilict graph. If it is # remove the selected componens with its conflict components. for ps_cp in selected_components: cf_cps = conflict_components.get(ps_cp, None) if cf_cps is not None: # Had conflict, but resolved. for cf_cp in cf_cps: del conflict_components[cf_cp] del conflict_components[ps_cp] # Check if there are components from the same category selected_cates = {} for cp in selected_components: cate = self.all_components.component_category_map[cp] if cate not in selected_cates.keys(): selected_cates[cate] = cp else: exisit_cp = selected_cates[cate] raise TimingModelError( f"Component '{cp}' and '{exisit_cp}' belong to the" f" same category '{cate}'. Only one component from" f" the same category can be used for a timing model." f" Please check your input (e.g., .par file).") return selected_components, conflict_components, param_not_in_pint
def _setup_model( self, timing_model, pint_param_dict, original_name=None, setup=True, validate=True, ): """Fill up a timing model with parameter values and then setup the model. This function fills up the timing model parameter values from the input pintified parameter dictionary. If the parameter has not initialized yet, it will add the parameter to the timing model. For the repeatable parameters, it will search matching key value pair first. If the input parameter line's key-value matches the existing parameter, the parameter value and uncertainty will copy to the existing parameter. If there is no match, it will find an empty existing parameter, whose `key` is `None`, and fill it up. If no empyt parameter left, it will add a new parameter to it. Parameters ---------- timing_model : pint.models.TimingModel Timing model to get setup. pint_param_dict: dict Pintified parfile dictionary which can be aquired by :meth:`ModelBuilder._pintify_parfile` origin_name : dict, optional A map from PINT name to the original input name. setup : bool, optional Whether to run the setup function in the timing model. validate : bool, optional Whether to run the validate funciotn in the timing model. """ if original_name is not None: use_alias = True else: use_alias = False for pp, v in pint_param_dict.items(): try: par = getattr(timing_model, pp) except AttributeError: # since the input is pintfied, it should be an uninitized indexed parameter # double check if the missing parameter an indexed parameter. pint_par, first_init = self.all_components.alias_to_pint_param( pp) try: prefix, _, index = split_prefixed_name(pint_par) except PrefixError: par_hosts = self.all_components.param_component_map[ pint_par] currnt_cp = timing_model.components.keys() raise TimingModelError( f"Parameter {pint_par} is recognized" f" by PINT, but not used in the current" f" timing model. It is used in {par_hosts}," f" but the current timing model uses {currnt_cp}.") # TODO need to create a beeter API for _loacte_param_host host_component = timing_model._locate_param_host(first_init) timing_model.add_param_from_top( getattr(timing_model, first_init).new_param(index), host_component[0][0], ) par = getattr(timing_model, pint_par) # Fill up the values param_line = len(v) if param_line < 2: if use_alias: # Use the input alias as input name = original_name[pp] else: name = pp par.from_parfile_line(" ".join([name] + v)) else: # For the repeatable parameters lines = copy.deepcopy(v) # Line queue. # Check how many repeatable parameters in the model. example_par = getattr(timing_model, pp) prefix, _, index = split_prefixed_name(pp) for li in lines: # Creat a temp parameter with the idx bigger than all the existing indices repeatable_map = timing_model.get_prefix_mapping(prefix) new_max_idx = max(repeatable_map.keys()) + 1 temp_par = example_par.new_param(new_max_idx) temp_par.from_parfile_line(" ".join( [prefix + str(new_max_idx), li])) if use_alias: # Use the input alias as input temp_par.use_alias = original_name[pp] # Check current repeatable's key and value # TODO need to change here when maskParameter name changes to name_key_value empty_repeat_param = [] for idx, rp in repeatable_map.items(): rp_par = getattr(timing_model, rp) if rp_par.compare_key_value(temp_par): # Key and key value match, copy the new line to it # and exit rp_par.from_parfile_line(" ".join([rp, li])) if use_alias: # Use the input alias as input rp_par.use_alias = original_name[pp] break if rp_par.key is None: # Empty space for new repeatable parameter empty_repeat_param.append(rp_par) # There is no current repeatable parameter matching the new line # First try to fill up an empty space. if empty_repeat_param != []: emt_par = empty_repeat_param.pop(0) emt_par.from_parfile_line(" ".join([emt_par.name, li])) if use_alias: # Use the input alias as input emt_par.use_alias = original_name[pp] else: # No empty space, add a new parameter to the timing model. host_component = timing_model._locate_param_host(pp) timing_model.add_param_from_top( temp_par, host_component[0][0]) if setup: timing_model.setup() if validate: timing_model.validate() return timing_model
def _pintify_parfile(self, parfile, allow_name_mixing=False): """Translate parfile parameter name to PINT style name. This function converts the parfile information to PINT understandable parameter name. It also returns the PINT unrecognized parameters and check if the parfile has illegal repeating lines. Parameters ---------- parfile : str, file-like object, or parfile dictionary Parfile name, parfile StringIO, or the parfile dictionary returned by :func:`parse_parfile`. allow_name_mixing : bool, optional Flag for allowing the input to have mixing aliases names for the same parameter. For example, if this flag is true, one can have T2EFAC and EFAC, both of them maps to PINT parameter EFAC, present in the parfile at the same time. Returns ------- pint_param_dict : dict Pintified parameter dictionary with the PINT name as key and list of parameter value-uncertainty lines as value. For the repeating parameters in the parfile, the value will contain mulitple lines. original_name_map : dict PINT name maps to the original .par file input names. PINT name is the key and the original name is in the value. unknown_param : dict The PINT unrecognized parameters in the format of a dictionary. The key is the unknown parameter name and the value is the parfile value lines. Raises ------ TimingModelError If the parfile has mulitple line with non-repeating parameters. """ pint_param_dict = defaultdict(list) original_name_map = defaultdict(list) unknown_param = defaultdict(list) repeating = Counter() if isinstance(parfile, (str, StringIO)): parfile_dict = parse_parfile(parfile) else: parfile_dict = parfile for k, v in parfile_dict.items(): try: pint_name, init0 = self.all_components.alias_to_pint_param(k) except UnknownParameter: if k in ignore_params: # Parameter is known but in the ingore list continue else: # Check ignored prefix try: pfx, idxs, idx = split_prefixed_name(k) if pfx in ignore_prefix: # It is an ignored prefix. continue else: unknown_param[k] += v except PrefixError: unknown_param[k] += v continue pint_param_dict[pint_name] += v original_name_map[pint_name].append(k) repeating[pint_name] += len(v) # Check if this parameter is allowed to be repeated by PINT if len(pint_param_dict[pint_name]) > 1: if pint_name not in self.all_components.repeatable_param: raise TimingModelError( f"Parameter {pint_name} is not a repeatable parameter. " f"However, mulitple line use it.") # Check if the name is mixed for p_n, o_n in original_name_map.items(): if len(o_n) > 1: if not allow_name_mixing: raise TimingModelError( f"Parameter {p_n} have mixed input names/alias " f"{o_n}. If you want to have mixing names, please use" f" 'allow_name_mixing=True', and the output .par file " f"will use '{original_name_map[pint_name][0]}'.") original_name_map[p_n] = o_n[0] return pint_param_dict, original_name_map, unknown_param