def _plot_SPP_if_valid(self, ax=None, **kwargs): """ Mark SPPs on the plot if they are valid. """ if ax is None: ax = self.plt if isinstance(self.positions, numbers.Number): if is_inside(self.positions, self.x): x_closest, idx = find_nearest(self.x, self.positions) try: ax.plot(x_closest, self.y_norm[idx], **kwargs) except (ValueError, TypeError): ax.plot(x_closest, self.y[idx], **kwargs) if isinstance(self.positions, np.ndarray) or isinstance( self.positions, Iterable): if np.array(self.positions).ndim == 0: self.positions = np.atleast_1d(self.positions) # iterate over 0-d array: need to cast np.atleast_1d for i, val in enumerate(self.positions): if is_inside(self.positions[i], self.x): x_closest, idx = find_nearest(self.x, self.positions[i]) try: ax.plot(x_closest, self.y_norm[idx], **kwargs) except (ValueError, TypeError): ax.plot(x_closest, self.y[idx], **kwargs)
def _split_on_SPP(a: np.ndarray, val: Union[List, np.ndarray]) -> List[np.ndarray]: """ Split up an array based on value(s). """ if isinstance(val, numbers.Number): if is_inside(val, a): v, _ = find_nearest(a, val) logger.info(f"split value was set to {v} instead of {val}.") else: logger.info(f"{val} is outside of array range, skipping.") return [a] idx = np.where(a != v)[0] return np.split(a[idx], np.where(np.diff(idx) != 1)[0] + 1) elif isinstance(val, (list, np.ndarray)): real_callbacks = [] for i, v in enumerate(val): if not np.any(a == v): if is_inside(v, a): value, _ = find_nearest(a, v) real_callbacks.append(value) logger.info(f"{v} was replaced with {value}.") else: logger.info(f"{v} was thrown away, not in range..") else: real_callbacks.append(v) idx = np.in1d(a, real_callbacks) split_at = a.searchsorted(real_callbacks) - np.arange( 0, np.count_nonzero(idx)) return np.split(a[~idx], split_at)
def cut_data(x, y, ref, sam, start=None, stop=None): x, y = _handle_input(x, y, ref, sam) if start is None: start = np.min(x) if stop is None: stop = np.max(x) if start < stop: low_item, _ = find_nearest(x, start) high_item, _ = find_nearest(x, stop) mask = np.where((x >= low_item) & (x <= high_item)) return x[mask], y[mask] elif stop < start: raise ValueError("Start must not exceed stop value.") else: return np.array([]), np.array([])
def flip_around(self, value, side="left"): """ Flip the phase's y values. Parameters ---------- value : float The x value where to perform flipping. side : str, optional The side where to flip the y values. Default is "left". """ if side == "left": idx = np.where(self.x <= value)[0] elif side == "right": idx = np.where(self.x >= value)[0] else: idx = np.array([]) x_to_flip, y_to_flip = self.x[idx], self.y[idx] _, cls_idx = find_nearest(x_to_flip, value) logger.info(f"Using {self.x[cls_idx]} instead {value} as flip center.") value_base = self.y[cls_idx] y_to_flip *= -1 y_to_flip += 2 * value_base self.y[idx] = y_to_flip
def _fit(self, reference_point, order): """ This is meant to be used privately, when the pprint_disp is handled by another function. The `fit` method is for public use. """ if self.is_coeff or self.is_dispersion_array: warnings.warn("No need to fit another curve.") return else: if self.GD_mode: order -= 1 self.fitorder = order _function = _fit_config[order] x, y = np.copy(self.x), np.copy(self.y) x -= reference_point if _has_lmfit: fitmodel = Model(_function) pars = fitmodel.make_params( **{f"b{i}": 1 for i in range(order + 1)}) result = fitmodel.fit(y, x=x, params=pars) else: popt, pcov = curve_fit(_function, x, y, maxfev=8000) if _has_lmfit: dispersion, dispersion_std = transform_lmfit_params_to_dispersion( *_unpack_lmfit(result.params.items()), drop_first=True, dof=1) fit_report = result.fit_report() self.fitted_curve = result.best_fit else: dispersion, dispersion_std = transform_cf_params_to_dispersion( popt, drop_first=True, dof=1) fit_report = ( "To display detailed results you must have `lmfit` installed." ) self.fitted_curve = _function(x, *popt) if self.GD_mode: _, idx = find_nearest(self.x, reference_point) dispersion = np.insert(dispersion, 0, self.fitted_curve[idx]) dispersion_std = np.insert(dispersion_std, 0, 0) # The below line must have 7 elements, so slice these redundant coeffs.. dispersion, dispersion_std = dispersion[: -1], dispersion_std[: -1] self.coef_array = self.coef_temp(*dispersion) self.coef_std_array = self.coef_std_temp(*dispersion_std) return dispersion, dispersion_std, fit_report
def set_initial_region(self, percent): """ Determines the initial region to fit""" self._init_set = True if percent < 0 or percent > 100: raise ValueError("percent must satisfy percent > 0 or percent < 100.") _, idx = find_nearest(self.x, 0) self._upper_bound = np.floor(idx + (percent / 2) * (len(self.x) + 1)) self._lower_bound = np.floor(idx - (percent / 2) * (len(self.x) + 1)) self._upper_bound = self._upper_bound.astype(int) self._lower_bound = self._lower_bound.astype(int) if self._lower_bound < 0: self._lower_bound = 0 if self._upper_bound > len(self.x): self._upper_bound = len(self.x) self._x_curr = self.x[self._lower_bound:self._upper_bound] self._y_curr = self._y_norm[self._lower_bound:self._upper_bound]
def _prepare_SPP_data(self): pos_x, pos_y = [], [] if self.positions is not None: position = np.array(self.positions, dtype=np.float64).flatten() for i, val in enumerate(position): if is_inside(position[i], self.x): x_closest, idx = find_nearest(self.x, position[i]) try: pos_x.append(x_closest) pos_y.append(self.y_norm[idx]) except (ValueError, TypeError): pos_x.append(x_closest) pos_y.append(self.y[idx]) pos_x = np.array(pos_x) pos_y = np.array(pos_y) return pos_x, pos_y
def test_find_nearest(self): x = np.array([1, 2, 4, 9]) el, idx = find_nearest(x, 10) assert el == 9 assert idx == 3
def min_max_method( x, y, ref, sam, ref_point, maxx=None, minx=None, SPP_callbacks=None, ): """ Build the phase from extremal positions and SPP_callbacks. """ x, y = _handle_input(x, y, ref, sam) if maxx is None: max_ind = argrelextrema(y, np.greater) maxx = x[max_ind] if minx is None: min_ind = argrelextrema(y, np.less) minx = x[min_ind] _, ref_index = find_nearest(x, ref_point) ref_point = x[ref_index] logger.info(f"refpoint set to {x[ref_index]} instead of {ref_point}.") # subtract the reference point from x axis at extremals max_freq = x[ref_index] - maxx min_freq = x[ref_index] - minx if SPP_callbacks is not None: if isinstance(SPP_callbacks, numbers.Number): SPP_callbacks -= ref_point elif isinstance(SPP_callbacks, (list, np.ndarray)): try: SPP_callbacks = np.asarray(SPP_callbacks) - ref_point except TypeError: pass else: raise TypeError("SPP_callbacks must be list-like, or number.") logger.info( f"SPP_callbacks are now {SPP_callbacks}, with ref_point {ref_point}." ) # find which extremal point is where (relative to reference_point) and order them # as they need to be multiplied together with the corresponding order `m` neg_freq = np.sort( np.append(max_freq[max_freq < 0], min_freq[min_freq < 0]))[::-1] pos_freq = np.sort( np.append(max_freq[max_freq >= 0], min_freq[min_freq >= 0])) pos_data_x, pos_data_y = _build_single_phase_data( -pos_freq, SPP_callbacks=SPP_callbacks) # if we fail, the whole negative half is empty try: if np.diff(pos_data_y)[-1] < 0: flip = True logger.info( "Positive side was flipped because the other side is decreasing." ) else: flip = False except IndexError: flip = False neq_data_x, neq_data_y = _build_single_phase_data( -neg_freq, SPP_callbacks=SPP_callbacks, flip=flip) x_s = np.insert(neq_data_x, np.searchsorted(neq_data_x, pos_data_x), pos_data_x) y_s = np.insert(neq_data_y, np.searchsorted(neq_data_x, pos_data_x), pos_data_y) return x_s + ref_point, -y_s + ref_point
def GD_lookup(self, reference_point=None, engine="cwt", silent=False, **kwargs): """ Quick GD lookup: it finds extremal points near the `reference_point` and returns an average value of 2*pi divided by distances between consecutive minimal or maximal values. Since it's relying on peak detection, the results may be irrelevant in some cases. If the parent class is `~pysprint.CosFitMethod`, then it will set the predicted value as initial parameter for fitting. Parameters ---------- reference_point : float The reference point for the algorithm. engine : str, optional The backend to use. Must be "cwt", "normal" or "fft". "cwt" will use `scipy.signal.find_peaks_cwt` function to detect peaks, "normal" will use `scipy.signal.find_peaks` to detect peaks. The "fft" engine uses Fourier-transform and looks for the outer peak to guess delay value. It's not reliable when working with low delay values. silent : bool, optional Whether to print the results immediately. Default in `False`. kwargs : dict, optional Additional keyword arguments to pass for peak detection algorithms. These are: pmin, pmax, threshold, width, floor_thres, etc.. Most of them are described in the `find_peaks` and `find_peaks_cwt` docs. """ precision = _get_config_value("precision") if engine not in ("cwt", "normal", "fft"): raise ValueError("Engine must be `cwt`, `fft` or `normal`.") if reference_point is None and engine != "fft": warnings.warn( f"Engine `{engine}` isn't available without reference point, falling back to FFT based prediction.", PySprintWarning) engine = "fft" if engine == "fft": pred, _ = find_center(*ifft_method(self.x, self.y)) if pred is None: if not silent: print("Prediction failed, skipping.") return print(f"The predicted GD is ± {pred:.{precision}f} fs.") if hasattr(self, "params"): self.params[3] = pred return if engine == "cwt": widths = kwargs.pop("widths", np.arange(1, 20)) floor_thres = kwargs.pop("floor_thres", 0.05) x_min, _, x_max, _ = self.detect_peak_cwt(widths=widths, floor_thres=floor_thres) # just validation _ = kwargs.pop("pmin", 0.1) _ = kwargs.pop("pmax", 0.1) _ = kwargs.pop("threshold", 0.35) else: pmin = kwargs.pop("pmin", 0.1) pmax = kwargs.pop("pmax", 0.1) threshold = kwargs.pop("threshold", 0.35) x_min, _, x_max, _ = self.detect_peak(pmin=pmin, pmax=pmax, threshold=threshold) # just validation _ = kwargs.pop("widths", np.arange(1, 10)) _ = kwargs.pop("floor_thres", 0.05) if kwargs: raise TypeError(f"Invalid argument:{kwargs}") try: closest_val, idx1 = find_nearest(x_min, reference_point) m_closest_val, m_idx1 = find_nearest(x_max, reference_point) except (ValueError, IndexError): if not silent: print("Prediction failed, skipping.. ") return try: truncated = np.delete(x_min, idx1) second_closest_val, _ = find_nearest(truncated, reference_point) except (IndexError, ValueError): if not silent: print("Prediction failed, skipping.. ") return try: m_truncated = np.delete(x_max, m_idx1) m_second_closest_val, _ = find_nearest(m_truncated, reference_point) except (IndexError, ValueError): if not silent: print("Prediction failed, skipping.. ") return lowguess = 2 * np.pi / np.abs(closest_val - second_closest_val) highguess = 2 * np.pi / np.abs(m_closest_val - m_second_closest_val) # estimate the GD with that if hasattr(self, "params"): self.params[3] = (lowguess + highguess) / 2 if not silent: print( f"The predicted GD is ± {((lowguess + highguess) / 2):.{precision}f} fs" f" based on reference point of {reference_point:.{precision}f}." )