class EstimatorQuadraticB(EstimatorCubic): """ A template estimator to be used as a reference implementation. For more information regarding how to build your own estimator, read more in the :ref:`User Guide <user_guide>`. Parameters ---------- demo_param : str, default='demo_param' A parameter used for demonstation of how to pass and store paramters. """ ## Cubic model: b44_quadratic_equation = sp.Eq( B_44, B_1 * phi_dot + B_2 * phi_dot * sp.Abs(phi_dot)) restoring_equation_quadratic = sp.Eq(C_44, C_1 * phi) subs = [(B_44, sp.solve(b44_quadratic_equation, B_44)[0]), (C_44, sp.solve(restoring_equation_quadratic, C_44)[0])] roll_decay_equation = equations.roll_decay_equation_general_himeno.subs( subs) # Normalizing with A_44: lhs = (roll_decay_equation.lhs / A_44).subs( equations.subs_normalize).simplify() roll_decay_equation_A = sp.Eq(lhs=lhs, rhs=0) acceleration = sp.solve(roll_decay_equation_A, phi_dot_dot)[0] functions = dict(EstimatorCubic.functions) functions['acceleration'] = lambdify(acceleration) @classmethod def load(cls, B_1A: float, B_2A: float, C_1A: float, X=None, **kwargs): """ Load data and parameters from an existing fitted estimator A_44 is total roll intertia [kg*m**2] (including added mass) Parameters ---------- B_1A B_1/A_44 : linear damping B_2A B_2/A_44 : quadratic damping C_1A C_1/A_44 : linear stiffness X : pd.DataFrame DataFrame containing the measurement that this estimator fits (optional). Returns ------- estimator Loaded with parameters from data and maybe also a loaded measurement X """ data = { 'B_1A': B_1A, 'B_2A': B_2A, 'C_1A': C_1A, } return super(cls, cls)._load(data=data, X=X)
class EstimatorQuadratic(EstimatorCubic): """ A template estimator to be used as a reference implementation. For more information regarding how to build your own estimator, read more in the :ref:`User Guide <user_guide>`. Parameters ---------- demo_param : str, default='demo_param' A parameter used for demonstation of how to pass and store paramters. """ ## Quadratic model with Cubic restoring force: b44_quadratic_equation = sp.Eq( B_44, B_1 * phi_dot + B_2 * phi_dot * sp.Abs(phi_dot)) restoring_equation_cubic = sp.Eq(C_44, C_1 * phi + C_3 * phi**3 + C_5 * phi**5) subs = [(B_44, sp.solve(b44_quadratic_equation, B_44)[0]), (C_44, sp.solve(restoring_equation_cubic, C_44)[0])] roll_decay_equation = equations.roll_decay_equation_general_himeno.subs( subs) # Normalizing with A_44: lhs = (roll_decay_equation.lhs / A_44).subs( equations.subs_normalize).simplify() roll_decay_equation_A = sp.Eq(lhs=lhs, rhs=0) acceleration = sp.solve(roll_decay_equation_A, phi_dot_dot)[0] functions = dict(EstimatorCubic.functions) functions['acceleration'] = lambdify(acceleration)
def load(cls, file_path: str): with open(file_path, 'rb') as file: polynom = dill.load(file) polynom.equation = polynom.get_equation() polynom.lamda = lambdify(polynom.equation.rhs) return polynom
class DirectLinearEstimator(DirectEstimator): """ A template estimator to be used as a reference implementation. For more information regarding how to build your own estimator, read more in the :ref:`User Guide <user_guide>`. Parameters ---------- demo_param : str, default='demo_param' A parameter used for demonstation of how to pass and store paramters. """ # Defining the diff equation for this estimator: rhs = -phi_dot_dot / (omega0**2) - 2 * zeta / omega0 * phi_dot roll_diff_equation = sp.Eq(lhs=phi, rhs=rhs) acceleration = sp.Eq(lhs=phi, rhs=sp.solve(roll_diff_equation, phi.diff().diff())[0]) functions = {'acceleration': lambdify(acceleration.rhs)} @classmethod def load(cls, omega0: float, zeta: float, X=None): """ Load data and parameters from an existing fitted estimator A_44 is total roll intertia [kg*m**2] (including added mass) Parameters ---------- omega0: roll natural frequency[rad/s] zeta: linear roll damping [-] X : pd.DataFrame DataFrame containing the measurement that this estimator fits (optional). Returns ------- estimator Loaded with parameters from data and maybe also a loaded measurement X """ data = { 'omega0': omega0, 'zeta': zeta, } return super(cls, cls)._load(data=data, X=X)
import numpy as np import pandas as pd from scipy.integrate import odeint from rolldecayestimators import DirectEstimator from rolldecayestimators.symbols import * from rolldecayestimators.substitute_dynamic_symbols import lambdify dGM = sp.symbols('dGM') lhs = phi_dot_dot + 2*zeta*omega0*phi_dot + omega0**2*phi+(dGM*phi*sp.Abs(phi)) + d*sp.Abs(phi_dot)*phi_dot roll_diff_equation = sp.Eq(lhs=lhs, rhs=0) acceleration = sp.Eq(lhs=phi, rhs=sp.solve(roll_diff_equation, phi.diff().diff())[0]) calculate_acceleration = lambdify(acceleration.rhs) # Defining the diff equation for this estimator: class DirectEstimatorImproved(DirectEstimator): """ A template estimator to be used as a reference implementation. For more information regarding how to build your own estimator, read more in the :ref:`User Guide <user_guide>`. Parameters ---------- demo_param : str, default='demo_param' A parameter used for demonstation of how to pass and store paramters. """ @staticmethod def estimator(df, omega0, zeta, dGM, d): phi = df['phi'] phi1d = df['phi1d']
class EstimatorCubic(DirectEstimator): """ A template estimator to be used as a reference implementation. For more information regarding how to build your own estimator, read more in the :ref:`User Guide <user_guide>`. Parameters ---------- demo_param : str, default='demo_param' A parameter used for demonstation of how to pass and store paramters. """ ## Cubic model: b44_cubic_equation = sp.Eq( B_44, B_1 * phi_dot + B_2 * phi_dot * sp.Abs(phi_dot) + B_3 * phi_dot**3) restoring_equation_cubic = sp.Eq(C_44, C_1 * phi + C_3 * phi**3 + C_5 * phi**5) subs = [(B_44, sp.solve(b44_cubic_equation, B_44)[0]), (C_44, sp.solve(restoring_equation_cubic, C_44)[0])] roll_decay_equation = equations.roll_decay_equation_general_himeno.subs( subs) # Normalizing with A_44: lhs = (roll_decay_equation.lhs / A_44).subs( equations.subs_normalize).simplify() roll_decay_equation_A = sp.Eq(lhs=lhs, rhs=0) acceleration = sp.solve(roll_decay_equation_A, phi_dot_dot)[0] functions = {'acceleration': lambdify(acceleration)} C_1_equation = equations.C_equation_linear.subs(symbols.C, symbols.C_1) # C_1 = GM*gm eqs = [C_1_equation, equations.normalize_equations[symbols.C_1]] A44_equation = sp.Eq( symbols.A_44, sp.solve(eqs, symbols.C_1, symbols.A_44)[symbols.A_44]) functions['A44'] = lambdify(sp.solve(A44_equation, symbols.A_44)[0]) eqs = [ equations.C_equation_linear, equations.omega0_equation, A44_equation, ] omgea0_equation = sp.Eq( symbols.omega0, sp.solve(eqs, symbols.A_44, symbols.C, symbols.omega0)[0][2]) functions['omega0'] = lambdify( sp.solve(omgea0_equation, symbols.omega0)[0]) def __init__(self, maxfev=1000, bounds={}, ftol=10**-15, p0={}, fit_method='integration'): new_bounds = { 'B_1A': (0, np.inf), # Assuming only positive coefficients # 'B_2A': (0, np.inf), # Assuming only positive coefficients # 'B_3A': (0, np.inf), # Assuming only positive coefficients } new_bounds.update(bounds) bounds = new_bounds super().__init__(maxfev=maxfev, bounds=bounds, ftol=ftol, p0=p0, fit_method=fit_method, omega_regression=True) @classmethod def load(cls, B_1A: float, B_2A: float, B_3A: float, C_1A: float, C_3A: float, C_5A: float, X=None, **kwargs): """ Load data and parameters from an existing fitted estimator A_44 is total roll intertia [kg*m**2] (including added mass) Parameters ---------- B_1A B_1/A_44 : linear damping B_2A B_2/A_44 : quadratic damping B_3A B_3/A_44 : cubic damping C_1A C_1/A_44 : linear stiffness C_3A C_3/A_44 : cubic stiffness C_5A C_5/A_44 : pentatonic stiffness X : pd.DataFrame DataFrame containing the measurement that this estimator fits (optional). Returns ------- estimator Loaded with parameters from data and maybe also a loaded measurement X """ data = { 'B_1A': B_1A, 'B_2A': B_2A, 'B_3A': B_3A, 'C_1A': C_1A, 'C_3A': C_3A, 'C_5A': C_5A, } return super(cls, cls)._load(data=data, X=X) def calculate_additional_parameters(self, A44): check_is_fitted(self, 'is_fitted_') parameters_additional = {} for key, value in self.parameters.items(): symbol_key = sp.Symbol(key) new_key = key[0:-1] symbol_new_key = ss.Symbol(new_key) if symbol_new_key in equations.normalize_equations: normalize_equation = equations.normalize_equations[ symbol_new_key] solution = sp.solve(normalize_equation, symbol_new_key)[0] new_value = solution.subs([ (symbol_key, value), (symbols.A_44, A44), ]) parameters_additional[new_key] = new_value return parameters_additional def result_for_database(self, meta_data={}): s = super().result_for_database(meta_data=meta_data) inputs = pd.Series(meta_data) inputs['m'] = inputs['Volume'] * inputs['rho'] parameters = pd.Series(self.parameters) inputs = parameters.combine_first(inputs) s['A_44'] = run(self.functions['A44'], inputs=inputs) parameters_additional = self.calculate_additional_parameters( A44=s['A_44']) s.update(parameters_additional) inputs['A_44'] = s['A_44'] s['omega0'] = run(function=self.functions['omega0'], inputs=inputs) self.results = s # Store it also return s
class RollDecay(BaseEstimator): # Defining the diff equation for this estimator: rhs = -phi_dot_dot / (omega0**2) - 2 * zeta / omega0 * phi_dot roll_diff_equation = sp.Eq(lhs=phi, rhs=rhs) acceleration = sp.Eq(lhs=phi, rhs=sp.solve(roll_diff_equation, phi.diff().diff())[0]) functions = {'acceleration': lambdify(acceleration.rhs)} def __init__(self, ftol=1e-09, maxfev=100000, bounds={}, p0={}, fit_method='derivation', omega_regression=True, omega0=None): self.is_fitted_ = False self.phi_key = 'phi' # Roll angle [rad] self.phi1d_key = 'phi1d' # Roll velocity [rad/s] self.phi2d_key = 'phi2d' # Roll acceleration [rad/s2] self.y_key = self.phi2d_key self.boundaries = bounds self.p0 = p0 self.maxfev = maxfev self.ftol = ftol self.set_fit_method(fit_method=fit_method) self.omega_regression = omega_regression self.assert_success = True self._omega0 = omega0 @classmethod def load(cls, data: {}, X=None): """ Load data and parameters from an existing fitted estimator Parameters ---------- data : dict Dict containing data for this estimator such as parameters X : pd.DataFrame DataFrame containing the measurement that this estimator fits (optional). Returns ------- estimator Loaded with parameters from data and maybe also a loaded measurement X """ return cls._load(data=data, X=X) @classmethod def _load(cls, data: {}, X=None): """ Load data and parameters from an existing fitted estimator Parameters ---------- data : dict Dict containing data for this estimator such as parameters X : pd.DataFrame DataFrame containing the measurement that this estimator fits (optional). Returns ------- estimator Loaded with parameters from data and maybe also a loaded measurement X """ estimator = cls() estimator.load_data(data=data) estimator.load_X(X=X) return estimator def load_data(self, data: {}): parameter_names = self.parameter_names missing = list(set(parameter_names) - set(data.keys())) if len(missing) > 0: raise ValueError( 'The following parameters are missing in data:%s' % missing) parameters = { key: value for key, value in data.items() if key in parameter_names } self.parameters = parameters self.is_fitted_ = True def load_X(self, X=None): if isinstance(X, pd.DataFrame): self.X = X def set_fit_method(self, fit_method): self.fit_method = fit_method if self.fit_method == 'derivation': self.y_key = self.phi2d_key elif self.fit_method == 'integration': self.y_key = self.phi_key else: raise ValueError('Unknown fit_mehod:%s' % self.fit_method) def __repr__(self): if self.is_fitted_: parameters = ''.join( '%s:%0.3f, ' % (key, value) for key, value in sorted(self.parameters.items()))[0:-1] return '%s(%s)' % (self.__class__.__name__, parameters) else: return '%s' % (self.__class__.__name__) @property def calculate_acceleration(self): return self.functions['acceleration'] @property def parameter_names(self): signature = inspect.signature(self.calculate_acceleration) remove = [self.phi_key, self.phi1d_key] if not self.omega_regression: remove.append('omega0') return list(set(signature.parameters.keys()) - set(remove)) @staticmethod def error(x, self, xs, ys): #return np.sum((ys - self.estimator(x, xs))**2) return ys - self.estimator(x, xs) def estimator(self, x, xs): self.parameters = {key: x for key, x in zip(self.parameter_names, x)} if not self.omega_regression: self.parameters['omega0'] = self.omega0 if self.fit_method == 'derivation': self.parameters['phi'] = xs[self.phi_key] self.parameters['phi1d'] = xs[self.phi1d_key] return self.estimator_acceleration(parameters=self.parameters) elif self.fit_method == 'integration': t = xs.index phi0 = xs.iloc[0][self.phi_key] try: phi1d0 = xs.iloc[0][self.phi1d_key] except: phi1d0 = 0 return self.estimator_integration(t=t, phi0=phi0, phi1d0=phi1d0) else: raise ValueError('Unknown fit_mehod:%s' % self.fit_method) def estimator_acceleration(self, parameters): acceleration = self.calculate_acceleration(**parameters) return acceleration def estimator_integration(self, t, phi0, phi1d0): try: df = self.simulate(t=t, phi0=phi0, phi1d0=phi1d0) except: df = pd.DataFrame(index=t) df['phi'] = np.inf df['phi1d'] = np.inf return df[self.y_key] def fit(self, X, y=None, **kwargs): self.X = X.copy() kwargs = {'self': self, 'xs': X, 'ys': X[self.y_key]} if self.fit_method == 'integration': self.result = least_squares(fun=self.error, x0=self.initial_guess, kwargs=kwargs, bounds=self.bounds, ftol=self.ftol, max_nfev=self.maxfev, loss='soft_l1', f_scale=0.1) else: self.result = least_squares(fun=self.error, x0=self.initial_guess, kwargs=kwargs, ftol=self.ftol, max_nfev=self.maxfev, method='lm') if self.assert_success: if not self.result['success']: raise FitError(self.result['message']) self.parameters = { key: x for key, x in zip(self.parameter_names, self.result.x) } if not self.omega_regression: self.parameters['omega0'] = self.omega0 self.is_fitted_ = True def simulate(self, t, phi0, phi1d0) -> pd.DataFrame: states0 = [phi0, phi1d0] #states = odeint(self.roll_decay_time_step, y0=states0, t=t, args=(self,parameters)) #df[self.phi_key] = states[:, 0] #df[self.phi1d_key] = states[:, 1] t_ = t - t[0] t_span = [t_[0], t_[-1]] self.simulation_result = solve_ivp( fun=self.roll_decay_time_step, t_span=t_span, y0=states0, t_eval=t_, ) if not self.simulation_result['success']: raise ValueError('Simulation failed') df = pd.DataFrame(index=t) df[self.phi_key] = self.simulation_result.y[0, :] df[self.phi1d_key] = self.simulation_result.y[1, :] p_old = df[self.phi1d_key] phi_old = df[self.phi_key] df[self.phi2d_key] = self.calculate_acceleration(phi1d=p_old, phi=phi_old, **self.parameters) return df def roll_decay_time_step(self, t, states): # states: # [phi,phi1d] phi_old = states[0] p_old = states[1] phi1d = p_old calculate_acceleration = self.calculate_acceleration phi2d = calculate_acceleration(phi1d=p_old, phi=phi_old, **self.parameters) d_states_dt = np.array([phi1d, phi2d]) return d_states_dt def predict(self, X) -> pd.DataFrame: check_is_fitted(self, 'is_fitted_') phi0 = X[self.phi_key].iloc[0] phi1d0 = X[self.phi1d_key].iloc[0] t = np.array(X.index) return self.simulate(t=t, phi0=phi0, phi1d0=phi1d0) def score(self, X=None, y=None, sample_weight=None): """ Return the coefficient of determination R_b^2 of the prediction. The coefficient R_b^2 is defined as (1 - u/v), where u is the residual sum of squares ((y_true - y_pred) ** 2).sum() and v is the total sum of squares ((y_true - y_true.mean()) ** 2).sum(). The best possible score is 1.0 and it can be negative (because the model can be arbitrarily worse). A constant model that always predicts the expected value of y, disregarding the input features, would get a R_b^2 score of 0.0. Parameters ---------- X : array-like of shape (n_samples, n_features) Test samples. For some estimators this may be a precomputed kernel matrix or a list of generic objects instead, shape = (n_samples, n_samples_fitted), where n_samples_fitted is the number of samples used in the fitting for the estimator. y : Dummy not used sample_weight : Dummy Returns ------- score : float R_b^2 of self.predict(X) wrt. y. """ y_true, y_pred = self.true_and_prediction(X=X) return r2_score(y_true=y_true, y_pred=y_pred) def true_and_prediction(self, X=None): if X is None: X = self.X y_true = X[self.phi_key] df_sim = self.predict(X) y_pred = df_sim[self.phi_key] return y_true, y_pred @property def bounds(self): minimums = [] maximums = [] for key in self.parameter_names: boundaries = self.boundaries.get(key, (-np.inf, np.inf)) assert len(boundaries) == 2 minimums.append(boundaries[0]) maximums.append(boundaries[1]) return [tuple(minimums), tuple(maximums)] @property def initial_guess(self): p0 = [] for key in self.parameter_names: p0.append(self.p0.get(key, 0.5)) return p0 @property def omega0(self): """ Mean natural frequency Returns ------- """ if not self._omega0 is None: return self._omega0 frequencies, dft = fft(self.X['phi']) omega0 = fft_omega0(frequencies=frequencies, dft=dft) return omega0 def result_for_database(self, meta_data={}, score=True): check_is_fitted(self, 'is_fitted_') s = {} s.update(self.parameters) if score: s['score'] = self.score(X=self.X) if not self.X is None: s['phi_start'] = self.X.iloc[0]['phi'] s['phi_stop'] = self.X.iloc[-1]['phi'] if hasattr(self, 'omega0'): s['omega0_fft'] = self.omega0 self.results = s # Store it also return s
import sympy as sp from rolldecayestimators.substitute_dynamic_symbols import lambdify from rolldecayestimators import equations from rolldecayestimators import symbols B44_lambda = lambdify( sp.solve(equations.B44_hat_equation_quadratic, symbols.B_44_hat)[0]) B_1_hat_lambda = lambdify( sp.solve(equations.B_1_hat_equation, symbols.B_1_hat)[0]) B_e_hat_lambda = lambdify( sp.solve(equations.B_e_hat_equation, symbols.B_e_hat)[0]) B_2_hat_lambda = lambdify( sp.solve(equations.B_2_hat_equation, symbols.B_2_hat)[0]) omega0_lambda = lambdify( sp.solve(equations.omega0_hat_equation, symbols.omega_hat)[0]) omega = omega_from_hat = lambdify( sp.solve(equations.omega0_hat_equation, symbols.omega0)[0]) omega_hat = omega_to_hat = lambdify( sp.solve(equations.omega0_hat_equation, symbols.omega_hat)[0]) B_e_lambda = lambdify(sp.solve(equations.B_e_equation, symbols.B_e)[0]) B_e_lambda_cubic = lambdify( sp.solve(equations.B_e_equation_cubic, symbols.B_e)[0]) B44_hat_equation = equations.B44_hat_equation.subs(symbols.B_44, symbols.B) B_hat_lambda = lambdify(sp.solve(B44_hat_equation, symbols.B_44_hat)[0]) B_to_hat_lambda = lambdify(sp.solve(B44_hat_equation, symbols.B_44_hat)[0]) B_from_hat_lambda = lambdify(sp.solve(B44_hat_equation, symbols.B)[0])
import numpy as np import sympy as sp import pandas as pd from rolldecayestimators import equations from rolldecayestimators import symbols from rolldecayestimators import lambdas from rolldecayestimators.substitute_dynamic_symbols import lambdify lambda_B_1_zeta = lambdify(sp.solve(equations.B_1_zeta_eq, symbols.B_1)[0]) eq_phi1d = sp.Eq( symbols.phi_dot_dot, sp.solve(equations.roll_decay_equation_cubic_A, symbols.phi_dot_dot)[0]) accelaration_lambda = lambdify(sp.solve(eq_phi1d, symbols.phi_dot_dot)[0]) def find_peaks(df_state_space): df_state_space['phi_deg'] = np.rad2deg(df_state_space['phi']) mask = (np.sign(df_state_space['phi1d']) != np.sign( np.roll(df_state_space['phi1d'], -1))) mask[0] = False mask[-1] = False df_max = df_state_space.loc[mask].copy() df_max['id'] = np.arange(len(df_max)) + 1 return df_max
class AnalyticalLinearEstimator(DirectEstimator): """ A template estimator to be used as a reference implementation. For more information regarding how to build your own estimator, read more in the :ref:`User Guide <user_guide>`. Parameters ---------- demo_param : str, default='demo_param' A parameter used for demonstation of how to pass and store paramters. """ # Defining the diff equation for this estimator: rhs = -phi_dot_dot / (omega0**2) - 2 * zeta / omega0 * phi_dot roll_diff_equation = sp.Eq(lhs=phi, rhs=rhs) acceleration = sp.Eq(lhs=phi, rhs=sp.solve(roll_diff_equation, phi.diff().diff())[0]) functions = { 'phi': lambdify(sp.solve(equations.analytical_solution, phi)[0]), 'velocity': lambdify(sp.solve(equations.analytical_phi1d, phi_dot)[0]), 'acceleration': lambdify(sp.solve(equations.analytical_phi2d, phi_dot_dot)[0]), } @property def parameter_names(self): signature = inspect.signature(self.calculate_acceleration) remove = ['phi_0', 'phi_01d', 't'] if not self.omega_regression: remove.append('omega0') return list(set(signature.parameters.keys()) - set(remove)) def estimator(self, x, xs): parameters = {key: x for key, x in zip(self.parameter_names, x)} if not self.omega_regression: parameters['omega0'] = self.omega0 t = xs.index phi_0 = xs.iloc[0][self.phi_key] phi_01d = xs.iloc[0][self.phi1d_key] return self.functions['phi'](t=t, phi_0=phi_0, phi_01d=phi_01d, **parameters) def predict(self, X) -> pd.DataFrame: check_is_fitted(self, 'is_fitted_') t = X.index phi_0 = X.iloc[0][self.phi_key] phi_01d = X.iloc[0][self.phi1d_key] df = pd.DataFrame(index=t) df['phi'] = self.functions['phi'](t=t, phi_0=phi_0, phi_01d=phi_01d, **self.parameters) df['phi1d'] = self.functions['velocity'](t=t, phi_0=phi_0, phi_01d=phi_01d, **self.parameters) df['phi2d'] = self.functions['acceleration'](t=t, phi_0=phi_0, phi_01d=phi_01d, **self.parameters) return df
class IkedaQuadraticEstimator(IkedaEstimator): functions_ikeda = IkedaEstimator.functions_ikeda functions_ikeda.append( lambdify(sp.solve(equations.B_e_equation, symbols.B_e)[0])) # 2 functions_ikeda.append( lambdify(sp.solve(equations.zeta_B1_equation, symbols.zeta)[0])) # 3 functions_ikeda.append( lambdify(sp.solve(equations.d_B2_equation, symbols.d)[0])) # 4 @property def B_e_lambda(self): return self.functions_ikeda[2] @property def zeta_B1_lambda(self): return self.functions_ikeda[3] @property def d_B2_lambda(self): return self.functions_ikeda[4] def fit(self, X=None, y=None, **kwargs): self.X = X if not self.X is None: self.phi_max = np.rad2deg(self.X[ self.phi_key].abs().max()) ## Initial roll angle in [deg] DRAFT = (self.TA + self.TF) / 2 omega0 = self.omega0 if (self.lpp * self.beam * DRAFT > 0): CB = self.Volume / (self.lpp * self.beam * DRAFT) else: raise IkedaEstimatorFitError('lpp, b or DRAFT is zero or nan!') self.ikeda_parameters = { 'LPP': self.lpp, 'Beam': self.beam, 'DRAFT': DRAFT, 'PHI': self.phi_max, 'BKL': self.BKL, 'BKB': self.BKB, 'OMEGA': omega0, 'OG': (-self.kg + DRAFT), 'CB': CB, 'V': self.V, 'CMID': self.A0, 'verify_input': self.verify_input, 'limit_inputs': self.limit_inputs, } #self.result=self.calculate(**kwargs) self.result = {} self.result_variation = self.calculate_phi_a_variation() if self.two_point_regression: B_1_, B_2_ = self.calculate_two_point_regression(**kwargs) self.result['B_1'] = B_1 = B_1_['B_44'] self.result['B_2'] = B_2 = B_2_['B_44'] B_1_, B_2_ = self.fit_Bs() # Not used... else: B_1, B_2 = self.fit_Bs() self.result['B_1'] = B_1 self.result['B_2'] = B_2 zeta, d = self.Bs_to_zeta_d(B_1=B_1, B_2=B_2) factor = 1.0 # Factor phi_a = np.abs(np.deg2rad(self.phi_max)) / factor # [Radians] self.result['B_e'] = self.B_e_lambda( B_1=B_1, B_2=B_2, omega0=self.ikeda_parameters['OMEGA'], phi_a=phi_a) self.parameters = { 'zeta': zeta, 'd': d, 'omega0': omega0, } self.is_fitted_ = True def calculate_two_point_regression(self, **kwargs): # Two point regression: data = { 'lpp': self.lpp, 'b': self.beam, 'DRAFT': (self.TA + self.TF) / 2, 'phi_max': self.phi_max, 'BKL': self.BKL, 'BKB': self.BKB, 'omega0': self.ikeda_parameters['OMEGA'], 'kg': self.kg, 'CB': self.ikeda_parameters['CB'], 'A0': self.A0, 'V': self.V, 'Volume': self.Volume, } row1 = pd.Series(data) row1.phi_max *= 0.5 row2 = pd.Series(data) s1_hat = calculate(row1, verify_input=self.verify_input, limit_inputs=self.limit_inputs, **kwargs) s2_hat = calculate(row2, verify_input=self.verify_input, limit_inputs=self.limit_inputs, **kwargs) s1_hat = self.B44_lambda(B_44_hat=s1_hat, Disp=row1.Volume, b=row1.b, g=self.g, rho=self.rho) s2_hat = self.B44_lambda(B_44_hat=s2_hat, Disp=row2.Volume, b=row2.b, g=self.g, rho=self.rho) s1 = pd.Series() s2 = pd.Series() for key, value in s1_hat.items(): new_key = key.replace('_hat', '') s1[new_key] = s1_hat[key] s2[new_key] = s2_hat[key] x = np.deg2rad([row1.phi_max, row2.phi_max ]) * 8 * row1.omega0 / (3 * np.pi) B_2 = (s2 - s1) / (x[1] - x[0]) B_1 = s1 - B_2 * x[0] # Save all of the component as one linear term: _1 and a quadratic term: _2 for key in s1.index: new_name_1 = '%s_1' % key self.result[new_name_1] = s1[key] new_name_2 = '%s_2' % key self.result[new_name_2] = s2[key] return B_1, B_2 def calculate_phi_a_variation(self): data = { 'lpp': self.lpp, 'b': self.beam, 'DRAFT': (self.TA + self.TF) / 2, 'phi_max': self.phi_max, 'BKL': self.BKL, 'BKB': self.BKB, 'omega0': self.ikeda_parameters['OMEGA'], 'kg': self.kg, 'CB': self.ikeda_parameters['CB'], 'A0': self.A0, 'V': self.V, 'Volume': self.Volume, } self.ship = ship = pd.Series(data) N = 40 changes = np.linspace(1, 0.0001, N) df_variation = variate_ship(ship=ship, key='phi_max', changes=changes) result = calculate_variation(df=df_variation, limit_inputs=self.limit_inputs, verify_input=self.verify_input) df_variation['g'] = 9.81 df_variation['rho'] = 1000 result = pd.concat((result, df_variation), axis=1) result['B_44'] = self.B44_lambda(B_44_hat=result.B_44_hat, Disp=ship.Volume, b=ship.b, g=result.g, rho=result.rho) result.dropna(inplace=True) return result def fit_Bs(self): def fit(df, B_1, B_2): omega0 = df['omega0'] phi_a = np.deg2rad( df['phi_max'] ) # Deg or rad (Radians gave better results actually)??? #phi_a = df['phi_max'] # Deg or rad??? return self.B_e_lambda(B_1, B_2, omega0, phi_a) coeffs, _ = curve_fit(f=fit, xdata=self.result_variation, ydata=self.result_variation['B_44']) B_1 = coeffs[0] B_2 = coeffs[1] self.result_variation['B_44_fit'] = fit(self.result_variation, *coeffs) return B_1, B_2 def Bs_to_zeta_d(self, B_1, B_2): m = self.Volume * self.rho zeta = self.zeta_B1_lambda(B_1=B_1, GM=self.gm, g=self.g, m=m, omega0=self.ikeda_parameters['OMEGA']) d = self.d_B2_lambda(B_2=B_2, GM=self.gm, g=self.g, m=m, omega0=self.ikeda_parameters['OMEGA']) return zeta, d def plot_variation(self, ax=None): if ax is None: fig, ax = plt.subplots() self.result_variation.plot(y=['B_44_hat'], ax=ax) def plot_B_fit(self, ax=None): if ax is None: fig, ax = plt.subplots() self.result_variation.plot(y='B_44', ax=ax) self.result_variation.plot(y='B_44_fit', ax=ax, style='--') @classmethod def load(cls, data: {}, X=None): """ Load data and parameters from an existing fitted estimator Parameters ---------- data : dict Dict containing data for this estimator such as parameters X : pd.DataFrame DataFrame containing the measurement that this estimator fits (optional). Returns ------- estimator Loaded with parameters from data and maybe also a loaded measurement X """ estimator = cls(**data) estimator.load_data(data=data) estimator.load_X(X=X) return estimator
class IkedaEstimator(DirectEstimator): eqs = [ equations.zeta_equation, # 0 equations.omega0_equation_linear ] # 1 functions_ikeda = [ lambdify(sp.solve(eqs, symbols.A_44, symbols.zeta)[0][1]), lambdify(sp.solve(equations.B44_equation, symbols.B_44)[0]), ] def __init__(self, lpp: float, TA, TF, beam, BKL, BKB, A0, kg, Volume, gm, V, rho=1000, g=9.81, phi_max=8, omega0=None, verify_input=True, limit_inputs=False, **kwargs): """ Estimate a roll decay test using the Simplified Ikeda Method to predict roll damping. NOTE! This method is currently only valid for zero speed! Parameters ---------- lpp Ship perpendicular length [m] TA Draught aft [m] TF Draught forward [m] beam Ship b [m] BKL Bilge keel length [m] BKB Bilge keel height [m] A0 Middship coefficient (A_m/(B*d) [-] kg Vertical centre of gravity [m] Volume Displacement of ship [m3] gm metacentric height [m] V ship speed [m/s] rho Density of water [kg/m3] g acceleration of gravity [m/s**2] phi_max max roll angle during test [deg] omega0 Natural frequency of motion [rad/s], if None it will be calculated with fft of signal For more info see: "rolldecaysestimators/simplified_ikeda.py" """ super().__init__(omega0=omega0) self.lpp = lpp self.TA = TA self.TF = TF self.beam = beam self.BKL = BKL self.BKB = BKB self.A0 = A0 self.kg = kg self.Volume = Volume self.V = V self.rho = rho self.g = g self.gm = gm self.phi_max = phi_max self.two_point_regression = True self.verify_input = verify_input self.limit_inputs = limit_inputs @property def zeta_lambda(self): return self.functions_ikeda[0] @property def B44_lambda(self): return self.functions_ikeda[1] #def simulate(self, t :np.ndarray, phi0 :float, phi1d0 :float,omega0:float, zeta:float)->pd.DataFrame: # """ # Simulate a roll decay test using the quadratic method. # :param t: time vector to be simulated [s] # :param phi0: initial roll angle [rad] # :param phi1d0: initial roll speed [rad/s] # :param omega0: roll natural frequency[rad/s] # :param zeta:linear roll damping [-] # :return: pandas data frame with time series of 'phi' and 'phi1d' # """ # parameters={ # 'omega0':omega0, # 'zeta':zeta, # } # return self._simulate(t=t, phi0=phi0, phi1d0=phi1d0, parameters=parameters) def fit(self, X, y=None, **kwargs): self.X = X self.phi_max = np.rad2deg( self.X[self.phi_key].abs().max()) ## Initial roll angle in [deg] DRAFT = (self.TA + self.TF) / 2 omega0 = self.omega0 if (self.lpp * self.beam * DRAFT > 0): CB = self.Volume / (self.lpp * self.beam * DRAFT) else: raise IkedaEstimatorFitError('lpp, b or DRAFT is zero or nan!') self.ikeda_parameters = { 'LPP': self.lpp, 'Beam': self.beam, 'DRAFT': DRAFT, 'PHI': self.phi_max, 'BKL': self.BKL, 'BKB': self.BKB, 'OMEGA': omega0, 'OG': (-self.kg + DRAFT), 'CB': CB, 'CMID': self.A0, 'V': self.V, 'verify_input': self.verify_input, 'limit_inputs': self.limit_inputs, } self.result = self.calculate() m = self.Volume * self.rho B_44 = self.B44_lambda(B_44_hat=self.result.B_44_HAT, Disp=self.Volume, b=self.beam, g=self.g, rho=self.rho) zeta = self.zeta_lambda(B_1=B_44, GM=self.gm, g=self.g, m=m, omega0=omega0) self.parameters = { 'zeta': zeta, 'omega0': omega0, 'd': 0, } self.is_fitted_ = True def calculate(self, **kwargs): B44HAT, BFHAT, BWHAT, BEHAT, BBKHAT, BLHAT = calculate_roll_damping( **self.ikeda_parameters, **kwargs) s = pd.Series() s['B_44_HAT'] = B44HAT s['B_F_HAT'] = BFHAT s['B_W_HAT'] = BWHAT s['B_E_HAT'] = BEHAT s['B_BK_HAT'] = BBKHAT s['B_L_HAT'] = BLHAT return s def result_for_database(self, score=True, **kwargs): s = super().result_for_database(score=score, **kwargs) s.update(self.result) return s
class DirectEstimator(RollDecay): """ A template estimator to be used as a reference implementation. For more information regarding how to build your own estimator, read more in the :ref:`User Guide <user_guide>`. Parameters ---------- bounds : dict, default=None Boundaries for the parameters expressed as dict: Ex: { 'zeta':(-np.inf, np.inf), 'd':(0,42), } fit_method : str, default='integration' The fitting method could be either 'integration' or 'derivation' 'integration' means that the diff equation is solved with ODE integration. The curve_fit runs an integration for each set of parameters to get the best fit. 'derivation' means that the diff equation os solved by first calculating the 1st and 2nd derivatives numerically. The derivatives are then inserted into the diff equation and the best fit is found. """ # Defining the diff equation for this estimator: rhs = -phi_dot_dot / (omega0 ** 2) - 2 * zeta / omega0 * phi_dot - d * sp.Abs(phi_dot) * phi_dot / (omega0 ** 2) roll_diff_equation = sp.Eq(lhs=phi, rhs=rhs) acceleration = sp.Eq(lhs=phi, rhs=sp.solve(roll_diff_equation, phi.diff().diff())[0]) functions = {'acceleration':lambdify(acceleration.rhs)} @classmethod def load(cls, omega0:float, d:float, zeta:float, X=None): """ Load data and parameters from an existing fitted estimator Parameters ---------- omega0 : roll frequency [rad/s] d : nondimensional linear damping zeta : nondimensional quadratic damping X : pd.DataFrame DataFrame containing the measurement that this estimator fits (optional). Returns ------- estimator Loaded with parameters from data and maybe also a loaded measurement X """ data={ 'd':d, 'zeta':zeta, 'omega0':omega0, } return super(cls, cls)._load(data=data, X=X) def calculate_amplitudes_and_damping(self): if hasattr(self,'X'): if not self.X is None: self.X_amplitudes=measure.calculate_amplitudes_and_damping(X=self.X) if self.is_fitted_: X_pred = self.predict(X=self.X) self.X_pred_amplitudes = measure.calculate_amplitudes_and_damping(X=X_pred) @staticmethod def calculate_damping(X_amplitudes): df_decrements = pd.DataFrame() for i in range(len(X_amplitudes) - 1): s1 = X_amplitudes.iloc[i] s2 = X_amplitudes.iloc[i + 1] decrement = s1 / s2 decrement.name = s1.name df_decrements = df_decrements.append(decrement) df_decrements['zeta_n'] = 1 / (2 * np.pi) * np.log(df_decrements['phi']) df_decrements['zeta_n'] *= 2 # !!! # Todo: Where did this one come from? X_amplitudes_new = X_amplitudes.copy() X_amplitudes_new = X_amplitudes_new.iloc[0:-1].copy() X_amplitudes_new['zeta_n'] = df_decrements['zeta_n'].copy() X_amplitudes_new['B_n'] = 2*X_amplitudes_new['zeta_n'] # [Nm*s] return X_amplitudes_new def measure_error(self, X): y_true, y_pred = self.true_and_prediction(X=X) return y_pred - y_true def score(self, X=None, y=None, sample_weight=None): """ Return the coefficient of determination R_b^2 of the prediction. The coefficient R_b^2 is defined as (1 - u/v), where u is the residual sum of squares ((y_true - y_pred) ** 2).sum() and v is the total sum of squares ((y_true - y_true.mean()) ** 2).sum(). The best possible score is 1.0 and it can be negative (because the model can be arbitrarily worse). A constant model that always predicts the expected value of y, disregarding the input features, would get a R_b^2 score of 0.0. Parameters ---------- X : array-like of shape (n_samples, n_features) Test samples. For some estimators this may be a precomputed kernel matrix or a list of generic objects instead, shape = (n_samples, n_samples_fitted), where n_samples_fitted is the number of samples used in the fitting for the estimator. y : Dummy not used sample_weight : Dummy Returns ------- score : float R_b^2 of self.predict(X) wrt. y. """ y_true, y_pred = self.true_and_prediction(X=X) #sample_weight = np.abs(y_true) #return r2_score(y_true=y_true, y_pred=y_pred,sample_weight=sample_weight) return r2_score(y_true=y_true, y_pred=y_pred) def calculate_average_linear_damping(self,phi_a=None): """ Calculate the average linear damping In line with Himeno this equivalent linear damping is calculated as Parameters ---------- phi_a : float, default = None linearize around this average angle [rad] phi_a is calculated based on data if None Returns ------- average_linear_damping """ check_is_fitted(self, 'is_fitted_') zeta = self.parameters['zeta'] d = self.parameters.get('d',0) if phi_a is None: phi_a = self.X[self.phi_key].abs().mean() return zeta + 4/(3*np.pi)*d*phi_a def plot_fit(self, ax=None, include_model_test=True, label=None, **kwargs): check_is_fitted(self, 'is_fitted_') if ax is None: fig,ax = plt.subplots() df = self.predict(X=self.X) df['phi_deg'] = np.rad2deg(df['phi']) if include_model_test: X = self.X.copy() X['phi_deg'] = np.rad2deg(X['phi']) X.plot(y=r'phi_deg', ax=ax, label='Model test') if not label: label = 'fit' df.plot(y=r'phi_deg', ax=ax, label=label, style='--',**kwargs) ax.legend() ax.set_xlabel(r'Time [s]') ax.set_ylabel(r'$\Phi$ [deg]') def plot_error(self,X=None, ax=None, **kwargs): check_is_fitted(self, 'is_fitted_') if ax is None: fig, ax = plt.subplots() error = self.measure_error(X=X) ax.plot(self.X.index, error, label=self.__repr__(), **kwargs) ax.legend() ax.set_xlabel('Time [s]') ax.set_ylabel('error: phi_pred - phi_true [rad]') def plot_peaks(self, ax=None, **kwargs): check_is_fitted(self, 'is_fitted_') if ax is None: fig,ax = plt.subplots() #self.X.plot(y='phi', ax=ax) self.X_amplitudes.plot(y='phi', ax=ax, **kwargs) #ax.plot([np.min(self.X.index),np.max(self.X.index)],[0,0],'m-') ax.set_title('Peaks') def plot_velocity(self, ax=None): check_is_fitted(self, 'is_fitted_') if ax is None: fig,ax = plt.subplots() self.X.plot(y='phi1d', ax=ax) self.X_amplitudes.plot(y='phi1d', ax=ax, style='r.') ax.plot([np.min(self.X.index), np.max(self.X.index)], [0, 0], 'm-') ax.set_title('Velocities') def plot_amplitude(self, ax=None, include_model_test=True): if ax is None: fig,ax = plt.subplots() if not hasattr(self,'X_amplitudes'): self.calculate_amplitudes_and_damping() if include_model_test: X_amplitudes=self.X_amplitudes.copy() X_amplitudes['phi_a']=np.rad2deg(X_amplitudes['phi_a']) X_amplitudes.plot(y='phi_a', style='o', label='Model test', ax=ax) if hasattr(self,'X_pred_amplitudes'): label = self.__repr__() X_pred_amplitudes = self.X_pred_amplitudes.copy() X_pred_amplitudes['phi_a'] = np.rad2deg(X_pred_amplitudes['phi_a']) X_pred_amplitudes.plot(y='phi_a', label=label, ax=ax) def plot_damping(self, ax=None, include_model_test=True,label=None, **kwargs): if ax is None: fig, ax = plt.subplots() plot = None if include_model_test: X_amplitudes = measure.calculate_amplitudes_and_damping(X=self.X) X_amplitudes['phi_a'] = np.rad2deg(X_amplitudes['phi_a']) plot = X_amplitudes.plot(x='phi_a', y='B_n', style='o', label='Model test', ax=ax, **kwargs) if self.is_fitted_: if not label: label = self.__repr__() X_pred = self.predict(X=self.X) X_pred_amplitudes = measure.calculate_amplitudes_and_damping(X=X_pred) X_pred_amplitudes['phi_a'] = np.rad2deg(X_pred_amplitudes['phi_a']) if plot is None: color=None else: line = plot.axes.get_lines()[-1] color = line.get_color() x = X_pred_amplitudes['phi_a'] y = X_pred_amplitudes['B_n'] ax.plot(x, y, color=color, label=label, **kwargs, lw=2) ax.set_xlabel(r'$\phi_a$ [deg]') ax.set_ylabel(r'$B$ [Nms]') ax.legend() def plot_omega0(self,ax=None, include_model_test=True, label=None, **kwargs): if ax is None: fig,ax = plt.subplots() if not hasattr(self, 'X_amplitudes'): self.calculate_amplitudes_and_damping() plot = None if include_model_test: X_amplitudes = self.X_amplitudes.copy() X_amplitudes['omega0_norm'] = X_amplitudes['omega0']/self.omega0 X_amplitudes['phi_a_deg'] = np.rad2deg(X_amplitudes['phi_a']) plot = X_amplitudes.plot(x='phi_a_deg', y='omega0_norm', style='o', label='Model test', ax=ax, **kwargs) if hasattr(self, 'X_pred_amplitudes'): if not label: label = self.__repr__() if plot is None: color = None else: line = plot.axes.get_lines()[-1] color = line.get_color() x = np.rad2deg(self.X_pred_amplitudes['phi_a']) y = self.X_pred_amplitudes['omega0']/self.omega0 ax.plot(x, y, color=color, label=label, **kwargs, lw=2) ax.set_xlabel(r'$\phi_a$ [deg]') ax.set_ylabel(r'$\frac{\omega_0^N}{\omega_0}$ [-]') ax.legend() def plot_fft(self, ax=None): check_is_fitted(self, 'is_fitted_') if ax is None: fig,ax = plt.subplots() frequencies, dft = self.fft(self.X['phi']) omega=2*np.pi*frequencies omega0 = self.fft_omega0(frequencies=frequencies, dft=dft) index = np.argmax(np.abs(dft)) ax.plot(omega, dft) ax.plot(omega0,dft[index],'ro')
def fit(self, X, y): self.X = X result = self.polynomial_regression.fit(X=self.good_X(X), y=y) self.equation = self.get_equation() self.lamda = lambdify(self.equation.rhs) return result