diff --git a/.gitignore b/.gitignore index 35075865..9e177699 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,8 @@ dmypy.json .pyre/ #cache /cache/*.tar + +# Thesis run outputs (regenerated by projects/thesis/run.py + aggregators) +projects/thesis/results/ +projects/thesis/thesis_summary.json +projects/thesis/thesis_ablation_summary.json diff --git a/epde/eq_mo_objectives.py b/epde/eq_mo_objectives.py index 4d459b4f..22a6c963 100644 --- a/epde/eq_mo_objectives.py +++ b/epde/eq_mo_objectives.py @@ -60,37 +60,8 @@ def equation_complexity_by_terms(system, equation_key): return np.count_nonzero(system.vals[equation_key].weights_internal) -def equation_complexity_by_factors(system, equation_key): - ''' - Evaluate the complexity of the system of PDEs, evaluating a number of factors in terms for each - equation. In the evaluation, we consider only terms with non-zero weights and target, while - the free coefficient is not included in the final metric. Also, the real-valued factors are - not considered in the result. - - Parameters: - ----------- - system - ``epde.structure.main_structures.SoEq`` object - The system, that is to be evaluated. - - Returns: - ---------- - discrepancy : list of integers. - The values of the error metric: list entry for each of the equations. - ''' - # eq_compl = 0 - - # for idx, term in enumerate(system.vals[equation_key].structure): - # if idx < system.vals[equation_key].target_idx: - # if not system.vals[equation_key].weights_final[idx] == 0: - # eq_compl += len(term.structure) - # elif idx > system.vals[equation_key].target_idx: - # if not system.vals[equation_key].weights_final[idx-1] == 0: - # eq_compl += len(term.structure) - # else: - # eq_compl += len(term.structure) - # return eq_compl +def _complexity_single_eq(system, equation_key): eq_compl = 0 - for idx, term in enumerate(system.vals[equation_key].structure): if idx < system.vals[equation_key].target_idx: if not system.vals[equation_key].weights_final[idx] == 0: @@ -103,6 +74,19 @@ def equation_complexity_by_factors(system, equation_key): return eq_compl +def equation_complexity_by_factors(system, equation_key=None): + ''' + Evaluate the complexity of the system of PDEs as a number of factors in + non-zero terms for each equation, excluding the free coefficient and + real-valued factors. When ``equation_key`` is None, returns a per-equation + tuple matching the ``system.vars_to_describe`` order; otherwise the scalar + complexity for the named equation. + ''' + if equation_key is None: + return tuple(_complexity_single_eq(system, k) for k in system.vars_to_describe) + return _complexity_single_eq(system, equation_key) + + def equation_terms_stability(system, equation_key = None): if equation_key: assert system.vals[equation_key].stability_calculated diff --git a/epde/integrate/__init__.py b/epde/integrate/__init__.py index 95f3cd5e..e6f104b6 100644 --- a/epde/integrate/__init__.py +++ b/epde/integrate/__init__.py @@ -2,4 +2,14 @@ from .bop import BOPElement, BoundaryConditions from .pinn_integration import SolverAdapter from .numeric_integration import OdeintAdapter -from .deepxde_integration import DeepXDEAdapter \ No newline at end of file + + +# ``deepxde_integration`` does ``import deepxde``, which prints a backend +# banner on first load. Defer that until the DeepXDE adapter is actually +# requested so plain ``import epde`` / ``from epde.integrate import +# SolverAdapter`` stays quiet. +def __getattr__(name): + if name == 'DeepXDEAdapter': + from .deepxde_integration import DeepXDEAdapter + return DeepXDEAdapter + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") \ No newline at end of file diff --git a/epde/integrate/deepxde_integration.py b/epde/integrate/deepxde_integration.py index c334fa6f..f600e85a 100644 --- a/epde/integrate/deepxde_integration.py +++ b/epde/integrate/deepxde_integration.py @@ -261,6 +261,7 @@ def __init__(self, pretrained_net=None, **config): self.num_boundary = int(self.config.get('num_boundary', 500)) self.num_initial = int(self.config.get('num_initial', 500)) self.epochs = int(self.config.get('epochs', 10000)) + # self.iterations = int(self.config.get('epochs', 5)) self.bc_type = self.config.get('bc_type', 'Dirichlet') self.fallback_bc_value = self.config.get('fallback_bc_value', 0.0) diff --git a/epde/interface/interface.py b/epde/interface/interface.py index 306e76e5..9291cd41 100644 --- a/epde/interface/interface.py +++ b/epde/interface/interface.py @@ -230,15 +230,16 @@ class EpdeSearch(object): optimizer_exec_params (`dict`): parameters for execution algorithm of optimization optimizer (`OptimizationPatternDirector`): the strategy of the evolutionary algorithm """ - def __init__(self, multiobjective_mode: bool = True, use_pic = True, use_default_strategy: bool = True, director=None, + def __init__(self, multiobjective_mode: bool = True, use_pic = True, use_default_strategy: bool = True, director=None, director_params: dict = {'variation_params': {}, 'mutation_params': {}, - 'pareto_combiner_params': {}, 'pareto_updater_params': {}}, + 'pareto_combiner_params': {}, 'pareto_updater_params': {}}, time_axis: int = 0, define_domain: bool = True, function_form=None, boundary: int = 0, - use_solver: bool = False, verbose_params: dict = {'show_iter_idx' : True}, + use_solver: bool = False, verbose_params: dict = {'show_iter_idx' : True}, coordinate_tensors=None, memory_for_cache=15, prune_domain: bool = False, - pivotal_tensor_label=None, pruner=None, threshold: float = 1e-2, - division_fractions=3, rectangular: bool = True, - params_filename: str = None, device: str = 'cpu'): + pivotal_tensor_label=None, pruner=None, threshold: float = 1e-2, + division_fractions=3, rectangular: bool = True, + params_filename: str = None, device: str = 'cpu', + fitness_cls=None, sparsity_cls=None): """ Args: multiobjective_mode (`bool`): optional, default True @@ -319,8 +320,9 @@ def __init__(self, multiobjective_mode: bool = True, use_pic = True, use_default self.director = BaselineDirector() builder = StrategyBuilder(EvolutionaryStrategy) self.director.builder = builder - self.director.use_baseline(use_solver=self._mode_info['solver_fitness'], - use_pic=self._use_pic, params=director_params) + self.director.use_baseline(use_solver=self._mode_info['solver_fitness'], + use_pic=self._use_pic, params=director_params, + fitness_cls=fitness_cls, sparsity_cls=sparsity_cls) else: raise NotImplementedError('Wrong arguments passed during the epde search initialization') @@ -360,8 +362,9 @@ def set_moeadd_params(self, population_size: int = 6, solution_params: dict = {} subregion_mating_limitation: float = .95, PBI_penalty: float = 1., training_epochs: int = 100, neighborhood_selector: Callable = simple_selector, - neighborhood_selector_params: tuple = (4,)): - """ + neighborhood_selector_params: tuple = (4,), + early_stopping_callback: Callable = None): + r""" Setting the parameters of the multiobjective evolutionary algorithm. declaration of the default values is held in the initialization of EpdeSearch object. @@ -416,9 +419,10 @@ def set_moeadd_params(self, population_size: int = 6, solution_params: dict = {} 'nds_method' : nds_method, 'ndl_update' : ndl_update_method} - self.optimizer_exec_params = {'epochs' : training_epochs} - - def set_singleobjective_params(self, population_size: int = 4, solution_params: dict = {}, + self.optimizer_exec_params = {'epochs' : training_epochs, + 'early_stopping_callback' : early_stopping_callback} + + def set_singleobjective_params(self, population_size: int = 4, solution_params: dict = {}, sorting_method: Callable = simple_sorting, training_epochs: int = 50): """ Setting parameters for singelobjective optimization. @@ -949,6 +953,18 @@ def cache(self): else: return None, global_var.tensor_cache + @property + def pareto_history(self): + """Per-epoch Pareto-level-0 snapshots, populated during ``fit``. + + Returns a list of length ``training_epochs``; each element is a + list of ``{'text_form': str, 'obj_fun': list}`` dicts -- one per + solution on the non-dominated front at the end of that epoch. + Empty list when the optimizer hasn't been run or doesn't track + epoch history (e.g. single-objective mode). + """ + return getattr(self.optimizer, '_pareto_history', []) + def get_equations_by_complexity(self, complexity : Union[float, list]): ''' Get equations with desired complexity. Works best with ``EpdeSearch.visualize_solutions(...)`` diff --git a/epde/interface/token_family.py b/epde/interface/token_family.py index a56c7867..17504185 100644 --- a/epde/interface/token_family.py +++ b/epde/interface/token_family.py @@ -557,15 +557,26 @@ def create_with_var(self, variable: str, token_status=None, **kwargs): assert variable is not None, 'Can not create token with a specific variable for ' families = [f for f in self.families if variable == f.variable] - while True: + max_iter = len(families) + 1 + family = None + for _ in range(max_iter): + if not families: + raise RuntimeError( + f"TFPool.create_with_var: no family can produce a token for variable={variable!r}" + ) try: probabilities = np.array([len(f.tokens) for f in families]) - family = np.random.choice(families, p = probabilities/probabilities.sum()) - return family.create(label=None, token_status=token_status, - all_vars = [family.variable for family in self.families_demand_equation], + family = np.random.choice(families, p=probabilities/probabilities.sum()) + return family.create(label=None, token_status=token_status, + all_vars=[fam.variable for fam in self.families_demand_equation], **kwargs) except ValueError: - families.remove(family) + if family is not None and family in families: + families.remove(family) + family = None + raise RuntimeError( + f"TFPool.create_with_var: exhausted {max_iter} attempts for variable={variable!r}" + ) def __add__(self, other): return TFPool(families=self.families + other.families) diff --git a/epde/operators/common/fitness.py b/epde/operators/common/fitness.py index 00c21fe7..bc331781 100644 --- a/epde/operators/common/fitness.py +++ b/epde/operators/common/fitness.py @@ -13,7 +13,10 @@ import matplotlib.pyplot as plt from matplotlib import cm -from epde.integrate import SolverAdapter, DeepXDEAdapter +from epde.integrate import SolverAdapter +# DeepXDEAdapter is imported lazily inside DeepXDEBasedFitness.apply() to +# avoid triggering deepxde's import-time backend banner when no DeepXDE +# solver is used (e.g. legacy L2/L2LR fitness paths). from epde.structure.main_structures import SoEq, Equation from epde.operators.utils.template import CompoundOperator import epde.globals as global_var @@ -69,17 +72,38 @@ def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool = if force_out_of_place: self.suboperators['sparsity'].apply(objective, subop_args['sparsity']) + # Reject degenerate candidates whose entire non-target library was + # zeroed by sparsity. Without this, ``EqRightPartSelector`` may + # commit a target_idx whose only surviving content is the + # intercept, yielding population members of the form + # ``~0 = u^2 * du/dx0`` (no real LHS) that cannot represent any + # PDE by construction. Mirrors the rejection in ``L2LRFitness`` + # so the LEGACY (L2Fitness) and NEW (L2LRFitness) RPS sweeps + # share the same admissibility criterion. + if all(objective.weights_internal == 0): + return None self.suboperators['coeff_calc'].apply(objective, subop_args['coeff_calc']) _, target, features = objective.evaluate(normalize = False, return_val = False) if features is None: discr_feats = 0 else: - discr_feats = np.dot(features, objective.weights_final[:-1][objective.weights_internal != 0]) + n_cols = features.shape[1] if features.ndim > 1 else 1 + mask = objective.weights_internal != 0 + if n_cols == len(mask): + discr_feats = np.dot(features, objective.weights_internal) + elif n_cols == int(mask.sum()): + discr_feats = np.dot(features, objective.weights_final[:-1]) + else: + discr_feats = np.zeros(features.shape[0]) discr = (discr_feats + np.full(target.shape, objective.weights_final[-1]) - target) - self.g_fun_vals = global_var.grid_cache.g_func_flat - discr = np.multiply(discr, self.g_fun_vals) + try: + self.g_fun_vals = global_var.grid_cache.g_func[global_var.grid_cache.g_func_mask].reshape(-1) + except AttributeError: + self.g_fun_vals = None + if self.g_fun_vals is not None and self.g_fun_vals.shape == discr.shape: + discr = np.multiply(discr, self.g_fun_vals) rl_error = np.linalg.norm(discr, ord = 2) if not (self.params['penalty_coeff'] > 0. and self.params['penalty_coeff'] < 1.): @@ -137,7 +161,21 @@ def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool = if features is None: discr = target - target.mean() else: - discr_feats = np.dot(features, objective.weights_final[:-1]) + # ``features`` width depends on the ``normalize`` flag passed to + # ``evaluate`` above: ``normalize=True`` returns all N-1 + # non-target columns; ``normalize=False`` filters to only the + # nonzero-weight columns. ``weights_final[:-1]`` matches the + # latter shape (nonzero count); ``weights_internal`` matches the + # former (full N-1, with zeros). Pick whichever lines up with + # the actual feature matrix -- same pattern as L2Fitness.apply. + n_cols = features.shape[1] if features.ndim > 1 else 1 + mask = objective.weights_internal != 0 + if n_cols == len(mask): + discr_feats = np.dot(features, objective.weights_internal) + elif n_cols == int(mask.sum()): + discr_feats = np.dot(features, objective.weights_final[:-1]) + else: + discr_feats = np.zeros(features.shape[0]) discr_feats = discr_feats + objective.weights_final[-1] discr = target - discr_feats @@ -155,19 +193,23 @@ def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool = objective.aic_calculated = True data_shape = global_var.grid_cache.inner_shape - if hasattr(objective, '_cached_sw_weights') and objective._cached_sw_weights is not None: - weights = objective._cached_sw_weights + if features is None: + # Degenerate candidate (all features pruned by sparsity). + # Nothing to fit sliding-window weights on -- skip the CV + # calculation and report unit stability so downstream callers + # still get a finite value. + total_lr = 1.0 else: - weights = calculate_weights(features, target, self.g_fun_vals, data_shape, objective.weights_final[-1] != 0) - weights_arr = np.array(weights) - std = weights_arr.std(axis=0, ddof=1) - mu = weights_arr.mean(axis=0) - - # Safe division - with np.errstate(divide='ignore', invalid='ignore'): - cv = (std ** 2) / (mu ** 2) - - total_lr = sum(cv) / len(data_shape) + if hasattr(objective, '_cached_sw_weights') and objective._cached_sw_weights is not None: + weights = objective._cached_sw_weights + else: + weights = calculate_weights(features, target, self.g_fun_vals, data_shape, objective.weights_final[-1] != 0) + weights_arr = np.array(weights) + std = weights_arr.std(axis=0, ddof=1) + mu = weights_arr.mean(axis=0) + with np.errstate(divide='ignore', invalid='ignore'): + cv = (std ** 2) / (mu ** 2) + total_lr = sum(cv) / len(data_shape) if force_out_of_place: return fitness_value * total_lr @@ -357,9 +399,8 @@ def apply(self, objective: SoEq, arguments: dict, force_out_of_place: bool = Fal # Safe division with np.errstate(divide='ignore', invalid='ignore'): cv = (std ** 2) / (mu ** 2) - cv[mu == 0] = 0.0 # Handle zero mean - total_lr = sum(cv[:-1]) / len(data_shape) + total_lr = sum(cv) / len(data_shape) eq.fitness_calculated = True eq.fitness_value = lp @@ -491,8 +532,8 @@ def _compute_stability_for_equation(self, eq: Equation): weights_arr = np.array(weights) std = weights_arr.std(axis=0, ddof=1) mu = weights_arr.mean(axis=0) - cv = np.where(mu != 0, (std / mu) ** 2, 0.0) - total_lr = np.sum(cv[:-1]) / len(data_shape) if len(cv) > 1 else 0.0 + cv = (std ** 2) / (mu ** 2) + total_lr = np.sum(cv) / len(data_shape) eq.coefficients_stability = total_lr eq.stability_calculated = True diff --git a/epde/operators/common/right_part_selection.py b/epde/operators/common/right_part_selection.py index fe3a9772..fe959f23 100644 --- a/epde/operators/common/right_part_selection.py +++ b/epde/operators/common/right_part_selection.py @@ -42,24 +42,49 @@ class EqRightPartSelector(CompoundOperator): ''' key = 'FitnessCheckingRightPartSelector' - @HistoryExtender('\n -> The equation structure was detected: ', 'a') + @HistoryExtender('\n -> The equation structure was detected: ', 'a') def apply(self, objective : Equation, arguments : dict): self_args, subop_args = self.parse_suboperator_args(arguments = arguments) - assert len(objective.structure) == len(objective.terms_labels) + # Duplicate-term detection: a frozenset of per-term factor signatures + # has the same length as ``structure`` iff every term is distinct. + # Comparing against ``terms_labels`` here would be dimensionally wrong + # (see the same family of bugs fixed in ``enforce_rps_uniqueness`` and + # ``simplify_equation``). + signatures = {term.factors_labels for term in objective.structure} + assert len(signatures) == len(objective.structure), \ + 'Equation has duplicate terms; randomize before right-part selection.' + outer_max_iter = 50 + inner_max_iter = 100 + outer_attempts = 0 while not (objective.simplified and objective.is_correct_right_part): + outer_attempts += 1 + if outer_attempts > outer_max_iter: + warnings.warn( + 'EqRightPartSelector.apply: outer loop did not converge ' + f'after {outer_max_iter} iterations; accepting current state.' + ) + break objective.reset_state(True) min_fitness = np.inf weights_internal = np.zeros(len(objective.structure) - 1) min_idx = 0 + inner_attempts = 0 while not any(term.contains_deriv(objective.main_var_to_explain) for term in objective.structure): - # while not any(term.contains_deriv() for term in objective.structure): + inner_attempts += 1 + if inner_attempts > inner_max_iter: + warnings.warn( + 'EqRightPartSelector.apply: restore_property failed to ' + f'introduce a deriv of {objective.main_var_to_explain!r} ' + f'after {inner_max_iter} attempts; randomizing equation.' + ) + objective.randomize() + break objective.restore_property(mandatory_family=False, deriv=True) - + for target_idx, target_term in enumerate(objective.structure): if not objective.structure[target_idx].contains_deriv(objective.main_var_to_explain): - # if not objective.structure[target_idx].contains_deriv(): continue objective.target_idx = target_idx fitness = self.suboperators['fitness_calculation'].apply(objective, arguments = subop_args['fitness_calculation'], force_out_of_place = True) @@ -87,11 +112,10 @@ def apply(self, objective : Equation, arguments : dict): if not self.simplify_equation(objective): objective.simplified = True if objective.structure[objective.target_idx].contains_deriv(objective.main_var_to_explain): - # if objective.structure[objective.target_idx].contains_deriv(): objective.is_correct_right_part = True - else: - objective.right_part_selected = True - objective.remove_zero_terms() + + objective.right_part_selected = True + objective.remove_zero_terms() def simplify_equation(self, objective: Equation): # Get nonzero terms @@ -99,52 +123,62 @@ def simplify_equation(self, objective: Equation): nonrs_terms = [term for i, term in enumerate(objective.structure) if i != objective.target_idx] nonzero_terms = [item for item, keep in zip(nonrs_terms, nonzero_terms_mask) if keep] nonzero_terms.append(objective.structure[objective.target_idx]) - equation_terms = [term.term_label_without_power for term in nonzero_terms] - - # If amount nonzero terms is more than one -- get their intersection - if len(equation_terms) > 1: - common_factors = list(frozenset.intersection(*equation_terms)) - if len(common_factors) > 0: - for common_factor in common_factors: - # Find if this intersection in the same dimension (i.e. trigonometry functions) + it's minimal order - min_order = np.inf - common_dim = [] - for term in nonzero_terms: - for factor in term.structure: - if len(factor.params) == 1: - factor_label = (factor.cache_label[0]) + equation_terms = [term.factors_labels_without_power for term in nonzero_terms] + + if len(equation_terms) <= 1: + return False + common_factors = list(frozenset.intersection(*equation_terms)) + if not common_factors: + return False + + for common_factor in common_factors: + # Min power across the matching factor in every nonzero term. + min_order = np.inf + for term in nonzero_terms: + for factor in term.structure: + if factor.structural_label_without_power == common_factor: + if factor.cache_label[1][0] < min_order: + min_order = factor.cache_label[1][0] + + # Reduce order of common factor in every term; drop zero-power factors. + max_iter = 100 + for term in nonzero_terms: + factors_simplified = [] + for factor in term.structure: + if factor.structural_label_without_power == common_factor: + for i, value in enumerate(factor.params_description): + if factor.params_description[i]["name"] == "power": + factor.params[i] -= min_order + if factor.params[i] == 0: + factors_simplified.append(factor) else: - factor_label = (factor.cache_label[0], (factor.cache_label[1][-1])) - if factor_label == common_factor: - if len(factor.params) > 1: - common_dim.append(factor.params[-1]) - if factor.cache_label[1][0] < min_order: - min_order = factor.cache_label[1][0] - if len(set(common_dim)) < 2: - # If dimension is the same -- reduce order of terms' factor - for term in nonzero_terms: - factors_simplified = [] - for factor in term.structure: - if len(factor.params) == 1: - factor_label = (factor.cache_label[0]) - else: - factor_label = (factor.cache_label[0], (factor.cache_label[1][-1])) - if factor_label == common_factor: - for i, value in enumerate(factor.params_description): - if factor.params_description[i]["name"] == "power": - factor.params[i] -= min_order - if factor.params[i] == 0: - factors_simplified.append(factor) - else: - continue - term.structure = [factor for factor in term.structure if factor not in factors_simplified] - term.reset_saved_state() - - # If term's order became zero -- replace term - while len(term.structure) == 0 or not term.contains_meaningful() or len(objective.terms_labels) != len(objective.structure): - term.randomize() - - return True + continue + term.structure = [factor for factor in term.structure if factor not in factors_simplified] + term.reset_saved_state() + + # If term's order became zero -- replace term. + # Cap retries so a constrained token pool can't + # deadlock the optimizer (same hazard fixed in + # ``enforce_rps_uniqueness``). + attempts = 0 + while attempts < max_iter: + empty = len(term.structure) == 0 + not_meaningful = not term.contains_meaningful() + signatures = {t.factors_labels for t in objective.structure} + duplicate = len(signatures) != len(objective.structure) + if not (empty or not_meaningful or duplicate): + break + term.randomize() + attempts += 1 + + # Structure changed: invalidate stale fitness / + # weights / AIC caches while leaving RPS to the + # caller's outer loop. + try: + objective.reset_state(reset_right_part=False) + except TypeError: + objective.reset_state() + return True return False def use_default_tags(self): @@ -185,17 +219,33 @@ def apply(self, objective : Equation, arguments : dict): if not objective.right_part_selected: term_selection = [term_idx for term_idx, term in enumerate(objective.structure) if term.contains_deriv(variable = objective.main_var_to_explain)] - + if len(term_selection) == 0: idx = np.random.choice([term_idx for term_idx, _ in enumerate(objective.structure)]) prev_term = objective.structure[idx] - while True: + # Bounded retry + dedup check: never spin against a finite + # token pool, never introduce a duplicate term (see + # feedback-structure-dedup memory). + max_iter = 100 + candidate_term = None + for _ in range(max_iter): candidate_term = Term(pool = prev_term.pool, mandatory_family = objective.main_var_to_explain, - max_factors_in_term = len(prev_term.structure), + max_factors_in_term = len(prev_term.structure), create_derivs = True) - if candidate_term.contains_deriv(variable = objective.main_var_to_explain): - break - + if not candidate_term.contains_deriv(variable = objective.main_var_to_explain): + continue + sig = candidate_term.factors_labels + if any(j != idx and t.factors_labels == sig + for j, t in enumerate(objective.structure)): + continue + break + else: + warnings.warn( + f'RandomRHPSelector: could not produce a unique deriv term ' + f'for {objective.main_var_to_explain!r} after {max_iter} ' + f'attempts; keeping last candidate (may duplicate).' + ) + objective.structure[idx] = candidate_term else: idx = np.random.choice(term_selection) @@ -208,3 +258,125 @@ def apply(self, objective : Equation, arguments : dict): def use_default_tags(self): self._tags = {'equation right part selection', 'gene level', 'contains suboperators', 'inplace'} + + +def _scrub_conflicting_terms(equation: Equation, fixed_rps, *, max_iter: int = 100, + skip_idx=None) -> bool: + """Replace any term in ``equation.structure`` whose factor signature is a + superset of one of the ``fixed_rps`` signatures (each a ``frozenset`` of + factor labels). When ``skip_idx`` is passed, the term at that index is left + alone -- used by the bidirectional pass below to preserve an equation's + own already-selected RPS. + + Returns True if at least one term was randomized; the equation's cached + fitness/weight state is reset on the way out. + """ + if not fixed_rps: + return False + + def _conflicts(t): + return any(rs.issubset(t.factors_labels) for rs in fixed_rps) + + changed = False + for idx, term in enumerate(equation.structure): + if idx == skip_idx: + continue + if not _conflicts(term): + continue + for _ in range(max_iter): + term.randomize() + term.reset_saved_state() + signatures = {t.factors_labels for t in equation.structure} + duplicate = len(signatures) != len(equation.structure) + if not _conflicts(term) and not duplicate: + break + changed = True + + if changed: + try: + equation.reset_state(reset_right_part=False) + except TypeError: + equation.reset_state() + return changed + + +class SoEqRightPartSelector(CompoundOperator): + """Chromosome-level RPS that enforces bidirectional cross-equation + uniqueness. + + Forward sequential pass (pre-scrub each equation against + already-selected RPS, then run the per-equation sweep) handles the + case where equation_k > equation_j re-uses equation_j's RPS as a + non-target term. A second bidirectional convergence pass closes the + other direction: equation_j's structure is also scrubbed of any term + whose factor set is a superset of equation_k's (k > j) RPS. Without + the second pass the FIRST equation in ``vars_to_describe`` could keep + a later equation's target as a non-RPS term (e.g. LV's eq for u + keeping ``dv/dx0``), since at the time it was processed the later + RPS was not yet known. + + The bidirectional pass is bounded by ``max_bidirectional_passes`` and + exits as soon as a full sweep produces no scrubbing changes + (fixed-point). Each pass also re-runs the per-equation selector when + its structure changed, since the prior target_idx may no longer be + optimal under the new structure. + """ + key = 'SoEqRightPartSelector' + + def apply(self, objective, arguments: dict): + self_args, subop_args = self.parse_suboperator_args(arguments=arguments) + eq_selector = self.suboperators['eq_right_part_selector'] + eq_args = subop_args.get('eq_right_part_selector', arguments) + + equations = list(objective) + rps_signatures = [None] * len(equations) + + # Forward sequential pass: pre-scrub each equation against + # already-fixed RPS signatures, then run the per-equation selector. + for eq_idx, equation in enumerate(equations): + other_rps = [rs for rs in rps_signatures[:eq_idx] if rs is not None] + if other_rps: + _scrub_conflicting_terms(equation, other_rps, max_iter=100) + eq_selector.apply(objective=equation, arguments=eq_args) + try: + rps_signatures[eq_idx] = equation.structure[ + equation.target_idx].factors_labels + except (AttributeError, IndexError, TypeError): + rps_signatures[eq_idx] = None + + # Bidirectional convergence: each equation now knows the others' + # RPS, so re-scrub against the full set (skipping own target) and + # re-select when scrubbing changes the structure. Iterates until + # a full pass yields no changes. + max_passes = 5 + for _ in range(max_passes): + any_changes = False + for eq_idx, equation in enumerate(equations): + other_rps = [rs for i, rs in enumerate(rps_signatures) + if i != eq_idx and rs is not None] + if not other_rps: + continue + target_idx = getattr(equation, 'target_idx', None) + changed = _scrub_conflicting_terms( + equation, other_rps, max_iter=100, skip_idx=target_idx, + ) + if not changed: + continue + # Scrubbing mutated non-target terms: force re-selection so + # the post-scrub structure is evaluated for the best RPS. + equation.right_part_selected = False + equation.simplified = False + equation.is_correct_right_part = False + eq_selector.apply(objective=equation, arguments=eq_args) + try: + rps_signatures[eq_idx] = equation.structure[ + equation.target_idx].factors_labels + except (AttributeError, IndexError, TypeError): + pass + any_changes = True + if not any_changes: + break + + def use_default_tags(self): + self._tags = {'right part selection', 'chromosome level', + 'contains suboperators', 'inplace'} diff --git a/epde/operators/common/sparsity.py b/epde/operators/common/sparsity.py index 08539305..a510dc59 100644 --- a/epde/operators/common/sparsity.py +++ b/epde/operators/common/sparsity.py @@ -7,6 +7,8 @@ """ import numpy as np +from sklearn.linear_model import Lasso + import epde.globals as global_var from epde.operators.utils.template import CompoundOperator from epde.structure.main_structures import Equation @@ -14,10 +16,7 @@ from sklearn.base import BaseEstimator, RegressorMixin # import seaborn as sns import matplotlib.pyplot as plt -from epde.supplementary import calculate_weights - -import numpy as np -from sklearn.base import BaseEstimator, RegressorMixin +from epde.supplementary import calculate_weights, GramSetup # class PhysicsInformedLasso(BaseEstimator, RegressorMixin): @@ -226,7 +225,8 @@ def get_cv(self, weights): mu = weights_arr.mean(axis=0) with np.errstate(divide='ignore', invalid='ignore'): - cv = (std ** 2) / (mu ** 2 + std ** 2) + cv = std ** 2 / mu ** 2 + # cv = std ** 2 return np.nan_to_num(cv) @@ -254,6 +254,13 @@ def fit(self, X, y, sample_weights=None): norm_sq_features = np.sum(X_aug ** 2, axis=0) X_T_y = X_aug.T @ y # Cached once; slice by active_mask each outer iter. + # Pre-build the full sliding-window Gram matrix ONCE. The outer + # RFE loop below will slice it by ``active_mask`` per iteration + # instead of re-running the expensive ``X^T diag(w) X`` matmul on + # the surviving columns. The math is exact: a sub-block of the + # full Gram equals the Gram of the corresponding sub-columns. + gram_setup = GramSetup(X, y, sample_weights, self.grid_shape) + outer_iteration = 0 max_outer_iters = total_features # Max possible eliminations @@ -266,14 +273,9 @@ def fit(self, X, y, sample_weights=None): surviving_features_mask = active_mask[:-1] intercept_is_active = active_mask[-1] - # 2. Calculate physical priors ONLY for the active library - weights = calculate_weights( - X[:, surviving_features_mask], - y, - sample_weights=sample_weights, - grid_shape=self.grid_shape, - fit_intercept=intercept_is_active - ) + # 2. Calculate physical priors ONLY for the active library -- + # slice the precomputed full Gram by the current active mask. + weights = gram_setup.solve(active_mask) # Slice data for the CD run X_active = X_aug[:, active_mask] @@ -289,6 +291,7 @@ def fit(self, X, y, sample_weights=None): # terms get shrunk to zero before they pollute the residual. cv_order = np.argsort(active_cv)[::-1] active_thresholds = active_cv * max_corr + # active_thresholds = active_cv * norm_sq_active # Initialize coefficients active_coef = weights.mean(axis=0) @@ -423,6 +426,58 @@ def apply(self, objective : Equation, arguments : dict): # print(f'Metaparameter: {objective.metaparameters}, objective.metaparameters[("sparsity", objective.main_var_to_explain)]') self_args, subop_args = self.parse_suboperator_args(arguments = arguments) + estimator = Lasso(alpha=objective.metaparameters[('sparsity', objective.main_var_to_explain)]['value'], + copy_X=True, fit_intercept=True, max_iter=1000, + positive=False, precompute=False, random_state=None, + selection='random', tol=0.0001, warm_start=False) + + _, target, features = objective.evaluate(normalize = True, return_val = False) + + self.g_fun_vals = global_var.grid_cache.g_func[global_var.grid_cache.g_func_mask] + + n_features = features.shape[1] if (features is not None and hasattr(features, 'ndim') and features.ndim > 1) else 0 + if features is None or not np.all(np.isfinite(features)) or not np.all(np.isfinite(target)): + # Degenerate features (e.g. constant column triggering divide-by-zero + # in objective.evaluate's min-max normalisation). Fall back to a + # zero-weight assignment so the candidate is treated as "empty" + # rather than aborting the whole optimisation run. + coef = np.zeros(n_features) + intercept = 0.0 + else: + estimator.fit(features, target, self.g_fun_vals) + coef = estimator.coef_ + intercept = estimator.intercept_ + objective.weights_internal = coef + objective.weights_internal_evald = True + objective.weights_final = np.append([weight for weight in coef if weight != 0], intercept) + objective.weights_final_evald = True + # objective._cached_sw_weights = estimator.cached_weights_ + # Note: _eval_cache is intentionally NOT wiped here. The cache stores + # (value, target, features) tuples keyed on (normalize, return_val, + # grids is None); none of those depend on the weights this operator + # just updated. Structural mutations call ``Equation.reset_state`` + # which performs the wipe at the right moment. + + + def use_default_tags(self): + self._tags = {'sparsity', 'gene level', 'no suboperators', 'inplace'} + + +class VWSRSparsity(CompoundOperator): + """ + Variance-Weighted Sparse Regression operator. + + Mirrors :class:`LASSOSparsity` but swaps the sklearn ``Lasso`` estimator + for :class:`PhysicsInformedLasso`, which derives feature-specific L1 + penalties from the squared coefficient of variation of sliding-window + fits. Used as the regression step of the "new" pipeline in the EPDE + within-platform comparison (thesis Section 4.5). + """ + key = 'VWSRBasedSparsity' + + def apply(self, objective : Equation, arguments : dict): + self_args, subop_args = self.parse_suboperator_args(arguments = arguments) + estimator = PhysicsInformedLasso(grid_shape=global_var.grid_cache.inner_shape) _, target, features = objective.evaluate(normalize = True, return_val = False) @@ -435,10 +490,10 @@ def apply(self, objective : Equation, arguments : dict): objective.weights_final = np.append([weight for weight in estimator.coef_ if weight != 0], estimator.intercept_) objective.weights_final_evald = True objective._cached_sw_weights = estimator.cached_weights_ - objective._eval_cache = {} - + # See LASSOSparsity.apply: _eval_cache survives a weights update; + # only structural resets via ``Equation.reset_state`` should wipe it. def use_default_tags(self): self._tags = {'sparsity', 'gene level', 'no suboperators', 'inplace'} - + diff --git a/epde/operators/multiobjective/moeadd_specific.py b/epde/operators/multiobjective/moeadd_specific.py index 7ae77295..6ca224bb 100644 --- a/epde/operators/multiobjective/moeadd_specific.py +++ b/epde/operators/multiobjective/moeadd_specific.py @@ -29,8 +29,19 @@ def penalty_based_intersection(sol_obj, weight, ideal_obj, ''' solution_objective = sol_obj.obj_fun if obj_normalizer is None else obj_normalizer(sol_obj.obj_fun) - weight_full = np.array([item for item in weight for _ in sol_obj.vals]) - ideal_obj_full = np.array([item for item in ideal_obj for _ in sol_obj.vals]) + weight_arr = np.asarray(weight) + ideal_obj_arr = np.asarray(ideal_obj) + n_eqs = len(sol_obj.vals) + n_obj = solution_objective.shape[0] + if weight_arr.size * n_eqs == n_obj: + # MOEA/D weight is per objective TYPE -- expand to per-equation space. + weight_full = np.repeat(weight_arr, n_eqs) + ideal_obj_full = np.repeat(ideal_obj_arr, n_eqs) + else: + # Weight already lives in the full objective space (legacy + # objective list of per-equation partials). + weight_full = weight_arr + ideal_obj_full = ideal_obj_arr weight_norm = np.linalg.norm(weight_full) @@ -106,7 +117,7 @@ def locate_pareto_worst(levels, weights: np.ndarray, best_obj: np.ndarray, penal # NOTE: If your solution objects have a `.rank` or `.ndl` attribute, # replace this inner loop entirely with: `domain_solution_NDL_idxs[solution_idx] = solution.rank` for level_idx, level in enumerate(levels.levels): - if any(solution.terms_labels == level_solution.terms_labels for level_solution in level): + if any(solution.equations_labels == level_solution.equations_labels for level_solution in level): domain_solution_NDL_idxs[solution_idx] = level_idx break @@ -360,18 +371,13 @@ def apply(self, objective: ParetoLevels, arguments: dict): temp_offspring = self.suboperators['chromosome_mutation'].apply(objective=temp_offspring, arguments=subop_args['chromosome_mutation']) temp_offspring.reset_state(True) + # SoEqRightPartSelector enforces cross-equation RPS + # uniqueness inline (sequential pre-scrub), so no post-hoc + # ``enforce_rps_uniqueness`` retry loop is needed here. self.suboperators['right_part_selector'].apply(objective=temp_offspring, arguments=subop_args['right_part_selector']) - if len(temp_offspring.vars_to_describe) > 1: - term_replaced = is_rps_in_other_equation(temp_offspring) - while any(term_replaced): - temp_offspring.reset_state(True) - self.suboperators['right_part_selector'].apply(objective=temp_offspring, - arguments=subop_args['right_part_selector']) - term_replaced = is_rps_in_other_equation(temp_offspring) - - system = temp_offspring.terms_labels + system = temp_offspring.equations_labels if system not in objective.history: self.suboperators['chromosome_fitness'].apply(objective=temp_offspring, arguments=subop_args['chromosome_fitness']) @@ -437,32 +443,18 @@ def apply(self, objective : ParetoLevels, arguments : dict): if len(objective.population) == 0: for idx, candidate in enumerate(objective.unplaced_candidates): candidate.reset_state(True) + # SoEqRightPartSelector handles cross-equation RPS + # uniqueness inline; no post-hoc retry needed. self.suboperators['right_part_selector'].apply(objective = candidate, arguments = subop_args['right_part_selector']) - if len(candidate.vars_to_describe) > 1: - replaced = is_rps_in_other_equation(candidate) - while any(replaced): - candidate.reset_state(True) - self.suboperators['right_part_selector'].apply(objective=candidate, - arguments=subop_args['right_part_selector']) - replaced = is_rps_in_other_equation(candidate) - - system = candidate.terms_labels + + system = candidate.equations_labels while system in objective.history: candidate.create() candidate.reset_state(True) self.suboperators['right_part_selector'].apply(objective=candidate, arguments=subop_args['right_part_selector']) - - if len(candidate.vars_to_describe) > 1: - replaced = is_rps_in_other_equation(candidate) - while any(replaced): - candidate.reset_state(True) - self.suboperators['right_part_selector'].apply(objective=candidate, - arguments=subop_args['right_part_selector']) - replaced = is_rps_in_other_equation(candidate) - - system = candidate.terms_labels + system = candidate.equations_labels self.suboperators['chromosome_fitness'].apply(objective=candidate, arguments=subop_args['chromosome_fitness']) objective.history.add(system) @@ -503,20 +495,61 @@ def has_subset_pair(collection_of_sets): # No subset relationship found among any pairs return False, None, None -def is_rps_in_other_equation(objective): - rsterms = [None for _ in objective.vals] - replaced = [False for _ in objective.vals] - for equation_idx, equation in enumerate(objective.vals): - rsterms[equation_idx] = equation.structure[equation.target_idx].term_label +def _debug_assert_rps_unique(objective) -> list: + """Debug helper: scan an SoEq's equations and return a per-equation + list of bools indicating which equations contain at least one + non-target term whose factor set is a superset of another equation's + target term factor set. + + The post-hoc enforcement loop that used to call this and rewrite + conflicting terms is gone -- ``SoEqRightPartSelector`` now propagates + the uniqueness constraint forward across equations during the RPS + sweep itself, so a correctly-implemented pipeline must produce an + all-False result here. Use this in tests or temporary asserts to + catch regressions; do NOT wire it back into the operator graph as a + repair step. + """ + equations = list(objective.vals) + rsterms = [eq.structure[eq.target_idx].factors_labels for eq in equations] + flagged = [False] * len(equations) - for equation_idx, equation in enumerate(objective.vals): - rs = rsterms[:equation_idx] + rsterms[equation_idx + 1:] + for eq_idx, equation in enumerate(equations): + other_rs = rsterms[:eq_idx] + rsterms[eq_idx + 1:] for term_idx, term in enumerate(equation.structure): - if any(rsterm.issubset(term.term_label) for rsterm in rs): - replaced[equation_idx] = True - term.randomize() - term.reset_saved_state() - while any(rsterm.issubset(term.term_label) for rsterm in rs) or len(equation.terms_labels) != len(equation.structure): - term.randomize() - term.reset_saved_state() - return replaced \ No newline at end of file + if term_idx == equation.target_idx: + continue + if any(rs.issubset(term.factors_labels) for rs in other_rs): + flagged[eq_idx] = True + break + return flagged + + +def is_rps_in_other_equation(objective): + """Deprecated alias. The post-hoc uniqueness repair has been replaced + by ``SoEqRightPartSelector`` (sequential pre-scrub), so this is now a + pure assertion helper that returns a per-equation flag list without + mutating anything. External callers should migrate to using the new + operator and remove their ``while any(is_rps_in_other_equation(...))`` + retry loops; the new operator guarantees the result is all-False on a + well-formed SoEq. + """ + warnings.warn( + 'is_rps_in_other_equation is now a pure debug check; ' + 'SoEqRightPartSelector enforces uniqueness during RPS dispatch. ' + 'Drop your retry loop.', + DeprecationWarning, stacklevel=2, + ) + return _debug_assert_rps_unique(objective) + + +def enforce_rps_uniqueness(objective, *, max_iter: int = 100) -> list: + """Deprecated. Retained as an assertion-only shim for code that + imports the old name; mutates nothing. Wraps + :func:`_debug_assert_rps_unique`. + """ + warnings.warn( + 'enforce_rps_uniqueness is now a pure debug check; ' + 'SoEqRightPartSelector enforces uniqueness during RPS dispatch.', + DeprecationWarning, stacklevel=2, + ) + return _debug_assert_rps_unique(objective) \ No newline at end of file diff --git a/epde/operators/multiobjective/mutations.py b/epde/operators/multiobjective/mutations.py index 7c01ed16..a2b077fe 100644 --- a/epde/operators/multiobjective/mutations.py +++ b/epde/operators/multiobjective/mutations.py @@ -44,11 +44,11 @@ def apply(self, objective : SoEq, arguments : dict): # TODO: add setter for best altered_objective.vals.replace_gene(gene_key = eq_key, value = altered_eq) - # for param_key in params_keys: - # altered_param = self.suboperators['param_mutation'].apply(altered_objective.vals[param_key], - # subop_args['param_mutation']) - # altered_objective.vals.replace_gene(gene_key = param_key, value = altered_param) - # altered_objective.vals.pass_parametric_gene(key = param_key, value = altered_param) + for param_key in params_keys: + altered_param = self.suboperators['param_mutation'].apply(altered_objective.vals[param_key], + subop_args['param_mutation']) + altered_objective.vals.replace_gene(gene_key = param_key, value = altered_param) + altered_objective.vals.pass_parametric_gene(key = param_key, value = altered_param) return altered_objective @@ -68,7 +68,11 @@ def apply(self, objective : Equation, arguments : dict): # objective.structure[term_idx].reset_saved_state() equation = deepcopy(objective) for _ in range(10): - equation.add_random_term() + if not equation.add_random_term(): + # Either ``terms_number`` reached or the pool ran out of + # uniques. Either way, further attempts would no-op or + # risk introducing a duplicate downstream -- stop here. + break assert len(equation.terms_labels) == len(equation.structure) @@ -120,15 +124,27 @@ def apply(self, objective : tuple, arguments : dict): #term_idx, equation): """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) - temp = deepcopy(objective[1].structure[objective[0]]) - objective[1].structure[objective[0]].randomize() - objective[1].structure[objective[0]].reset_saved_state() - while (len(objective[1].terms_labels) != len(objective[1].structure) - or objective[1].structure[objective[0]].terms_labels == temp.terms_labels): - objective[1].structure[objective[0]].randomize() - objective[1].structure[objective[0]].reset_saved_state() - # print(f'CREATED DURING MUTATION: {new_term.name}, while contatining {objective[1].structure[objective[0]].descr_variable_marker}') - return objective[1].structure[objective[0]] + term_idx, equation = objective + temp = deepcopy(equation.structure[term_idx]) + equation.structure[term_idx].randomize() + equation.structure[term_idx].reset_saved_state() + equation._invalidate_label_cache() + + # Re-randomize while the mutation produced a duplicate term within + # the equation OR no actual change vs the previous term. Cap the + # retries so a tight token pool can't deadlock the optimizer (same + # hazard fixed in ``enforce_rps_uniqueness`` / ``simplify_equation``). + max_iter = 100 + for _ in range(max_iter): + signatures = {t.factors_labels for t in equation.structure} + duplicate = len(signatures) != len(equation.structure) + unchanged = equation.structure[term_idx].factors_labels == temp.factors_labels + if not (duplicate or unchanged): + break + equation.structure[term_idx].randomize() + equation.structure[term_idx].reset_saved_state() + equation._invalidate_label_cache() + return equation.structure[term_idx] def use_default_tags(self): self._tags = {'mutation', 'term level', 'exploration', 'no suboperators'} @@ -161,24 +177,18 @@ def apply(self, objective : tuple, arguments : dict): # term_idx, objective self_args, subop_args = self.parse_suboperator_args(arguments = arguments) unmutable_params = {'dim', 'power'} - # objective[1] = deepcopy(objective[1]) - while True: - # Костыль! - print('ENTERING LOOP') - try: - objective[1].target_idx - except AttributeError: - objective[1].target_idx = 0 - # - term = objective[1].structure[objective[0]] + term_idx, equation = objective + if not hasattr(equation, 'target_idx'): + equation.target_idx = 0 + + # Cap the retry loop so a constrained token pool can't deadlock + # the optimizer (same hazard fixed in ``enforce_rps_uniqueness``). + max_iter = 100 + for _ in range(max_iter): + term = equation.structure[term_idx] for factor in term.structure: - if objective[0] == objective[1].target_idx: + if term_idx == equation.target_idx: continue - # if objective[0] < altered_objective.target_idx: - # corresponding_weight = altered_objective.weights_internal[objective[0]] - # else: - # corresponding_weight = altered_objective.weights_internal[objective[0] - 1] - # if corresponding_weight == 0: parameter_selection = deepcopy(factor.params) for param_idx, param_properties in factor.params_description.items(): if np.random.random() < self.params['r_param_mutation'] and param_properties['name'] not in unmutable_params: @@ -187,21 +197,20 @@ def apply(self, objective : tuple, arguments : dict): # term_idx, objective shift = 0 continue if isinstance(interval[0], int): - shift = np.rint(np.random.normal(loc= 0, scale = self.params['multiplier']*(interval[1] - interval[0]))).astype(int) # + shift = np.rint(np.random.normal(loc=0, scale=self.params['multiplier']*(interval[1] - interval[0]))).astype(int) elif isinstance(interval[0], float): - shift = np.random.normal(loc= 0, scale = self.params['multiplier']*(interval[1] - interval[0])) + shift = np.random.normal(loc=0, scale=self.params['multiplier']*(interval[1] - interval[0])) else: - raise ValueError('In current version of framework only integer and real values for parameters are supported') + raise ValueError('In current version of framework only integer and real values for parameters are supported') if self.params['strict_restrictions']: parameter_selection[param_idx] = np.min((np.max((parameter_selection[param_idx] + shift, interval[0])), interval[1])) else: parameter_selection[param_idx] = parameter_selection[param_idx] + shift factor.params = parameter_selection term.structure = filter_powers(term.structure) - print(f'checking presence of {term.name} as {objective[0]}-th element in {objective[1].text_form}') - # if check_uniqueness(term, objective[1].structure[:objective[0]] + - # objective[1].structure[objective[0]+1:]): - if len(objective[1].terms_labels) == len(objective[1].structure): + equation._invalidate_label_cache() + signatures = {t.factors_labels for t in equation.structure} + if len(signatures) == len(equation.structure): break term.reset_saved_state() return term diff --git a/epde/operators/multiobjective/variation.py b/epde/operators/multiobjective/variation.py index 3ccbf3dd..f8918111 100644 --- a/epde/operators/multiobjective/variation.py +++ b/epde/operators/multiobjective/variation.py @@ -92,11 +92,11 @@ def apply(self, objective : ParetoLevels, arguments : dict): assert len(crossover_pool[pair_idx, 0].vals[eq_key].terms_labels) == len(crossover_pool[pair_idx, 0].vals[eq_key].structure) assert len(crossover_pool[pair_idx, 1].vals[eq_key].terms_labels) == len(crossover_pool[pair_idx, 1].vals[eq_key].structure) - # if len(new_system_1.vars_to_describe) > 1 and np.random.random() < 0.2: - # key = np.random.choice(new_system_1.vars_to_describe) - # temp = deepcopy(new_system_1.vals.chromosome[key]) - # new_system_1.vals.chromosome[key] = new_system_2.vals.chromosome[key] - # new_system_2.vals.chromosome[key] = temp + if len(new_system_1.vars_to_describe) > 1 and np.random.random() < 0.2: + key = np.random.choice(new_system_1.vars_to_describe) + temp = deepcopy(new_system_1.vals.chromosome[key]) + new_system_1.vals.chromosome[key] = new_system_2.vals.chromosome[key] + new_system_2.vals.chromosome[key] = temp offsprings.extend([new_system_1, new_system_2]) @@ -185,24 +185,36 @@ def apply(self, objective : tuple, arguments : dict): equation1.structure = flatten(equation1_terms); equation2.structure = flatten(equation2_terms) - for term in equation1.structure: - if term.term_label not in equation1.terms_labels: + # Inject parent2's "similar but not identical" terms into equation1 + # (and vice versa) when they don't already appear there. The previous + # version iterated ``equation1.structure`` here, but every term in + # that list is already in ``equation1.terms_labels`` by construction, + # so the loop was a no-op. The intent is to take partner-only similar + # terms from the OTHER parent's similar bucket. + eq1_signatures = {t.factors_labels for t in equation1.structure} + for term in equation2_terms[1]: + if term.factors_labels not in eq1_signatures: equation1.structure.append(term) + eq1_signatures.add(term.factors_labels) + eq2_signatures = {t.factors_labels for t in equation2.structure} for term in equation1_terms[1]: - if term.term_label not in equation2.terms_labels: + if term.factors_labels not in eq2_signatures: equation2.structure.append(term) + eq2_signatures.add(term.factors_labels) for i in range(len(equation1.structure)): - if equation1.structure[i].term_label == equation1_target_term.term_label: + if equation1.structure[i].factors_labels == equation1_target_term.factors_labels: equation1.target_idx = i break for i in range(len(equation2.structure)): - if equation2.structure[i].term_label == equation2_target_term.term_label: + if equation2.structure[i].factors_labels == equation2_target_term.factors_labels: equation2.target_idx = i break + equation1._invalidate_label_cache() + equation2._invalidate_label_cache() return equation1, equation2 def use_default_tags(self): @@ -210,11 +222,11 @@ def use_default_tags(self): class EquationExchangeCrossover(CompoundOperator): key = 'EquationExchangeCrossover' - + @HistoryExtender(f'\n -> performing equation exchange crossover', 'ba') def apply(self, objective : tuple, arguments : dict): self_args, subop_args = self.parse_suboperator_args(arguments = arguments) - + # objective[0].structure, objective[1].structure = objective[1].structure, objective[0].structure return objective[0], objective[1] diff --git a/epde/operators/singleobjective/mutations.py b/epde/operators/singleobjective/mutations.py index e8ac9ac1..2bd9d7d3 100644 --- a/epde/operators/singleobjective/mutations.py +++ b/epde/operators/singleobjective/mutations.py @@ -7,6 +7,7 @@ """ import numpy as np +import warnings from copy import deepcopy from functools import partial from typing import Union @@ -60,11 +61,15 @@ class EquationMutation(CompoundOperator): @HistoryExtender(f'\n -> mutating equation', 'ba') def apply(self, objective : Equation, arguments : dict): - self_args, subop_args = self.parse_suboperator_args(arguments = arguments) + self_args, subop_args = self.parse_suboperator_args(arguments = arguments) + mutated = False for term_idx in range(objective.n_immutable, len(objective.structure)): if np.random.uniform(0, 1) <= self.params['r_mutation']: objective.structure[term_idx] = self.suboperators['mutation'].apply(objective = (term_idx, objective), arguments = subop_args['mutation']) + mutated = True + if mutated: + objective._invalidate_label_cache() return objective def use_default_tags(self): @@ -160,23 +165,17 @@ def apply(self, objective : tuple, arguments : dict): # term_idx, objective unmutable_params = {'dim', 'power'} # objective[1] = deepcopy(objective[1]) - while True: - # Костыль! - print('ENTERING LOOP') - try: - objective[1].target_idx - except AttributeError: - objective[1].target_idx = 0 - # - term = objective[1].structure[objective[0]] + try: + objective[1].target_idx + except AttributeError: + objective[1].target_idx = 0 + + max_iter = 100 + for _ in range(max_iter): + term = objective[1].structure[objective[0]] for factor in term.structure: if objective[0] == objective[1].target_idx: continue - # if objective[0] < altered_objective.target_idx: - # corresponding_weight = altered_objective.weights_internal[objective[0]] - # else: - # corresponding_weight = altered_objective.weights_internal[objective[0] - 1] - # if corresponding_weight == 0: parameter_selection = deepcopy(factor.params) for param_idx, param_properties in factor.params_description.items(): if np.random.random() < self.params['r_param_mutation'] and param_properties['name'] not in unmutable_params: @@ -189,17 +188,22 @@ def apply(self, objective : tuple, arguments : dict): # term_idx, objective elif isinstance(interval[0], float): shift = np.random.normal(loc= 0, scale = self.params['multiplier']*(interval[1] - interval[0])) else: - raise ValueError('In current version of framework only integer and real values for parameters are supported') + raise ValueError('In current version of framework only integer and real values for parameters are supported') if self.params['strict_restrictions']: parameter_selection[param_idx] = np.min((np.max((parameter_selection[param_idx] + shift, interval[0])), interval[1])) else: parameter_selection[param_idx] = parameter_selection[param_idx] + shift factor.params = parameter_selection term.structure = filter_powers(term.structure) - print(f'checking presence of {term.name} as {objective[0]}-th element in {objective[1].text_form}') - if check_uniqueness(term, objective[1].structure[:objective[0]] + + objective[1]._invalidate_label_cache() + if check_uniqueness(term, objective[1].structure[:objective[0]] + objective[1].structure[objective[0]+1:]): break + else: + warnings.warn( + f"TermParameterMutation: no unique mutation found in {max_iter} attempts; " + "leaving last candidate (may duplicate an existing term)." + ) term.reset_saved_state() return term diff --git a/epde/operators/singleobjective/variation.py b/epde/operators/singleobjective/variation.py index 1ee5b61f..eb387c86 100644 --- a/epde/operators/singleobjective/variation.py +++ b/epde/operators/singleobjective/variation.py @@ -175,10 +175,12 @@ def apply(self, objective : tuple, arguments : dict): for i in range(same_num + similar_num, len(objective[0].structure)): if check_uniqueness(objective[0].structure[i], objective[1].structure) and check_uniqueness(objective[1].structure[i], objective[0].structure): - objective[0].structure[i], objective[1].structure[i] = self.suboperators['term_crossover'].apply(objective = (objective[0].structure[i], + objective[0].structure[i], objective[1].structure[i] = self.suboperators['term_crossover'].apply(objective = (objective[0].structure[i], objective[1].structure[i]), arguments = subop_args['term_crossover']) - + + objective[0]._invalidate_label_cache() + objective[1]._invalidate_label_cache() return objective[0], objective[1] def use_default_tags(self): @@ -186,12 +188,14 @@ def use_default_tags(self): class EquationExchangeCrossover(CompoundOperator): key = 'EquationExchangeCrossover' - + @HistoryExtender(f'\n -> performing equation exchange crossover', 'ba') def apply(self, objective : tuple, arguments : dict): self_args, subop_args = self.parse_suboperator_args(arguments = arguments) - + objective[0].structure, objective[1].structure = objective[1].structure, objective[0].structure + objective[0]._invalidate_label_cache() + objective[1]._invalidate_label_cache() return objective[0], objective[1] def use_default_tags(self): diff --git a/epde/optimizers/moeadd/moeadd.py b/epde/optimizers/moeadd/moeadd.py index bfbf33af..d0c01ad2 100644 --- a/epde/optimizers/moeadd/moeadd.py +++ b/epde/optimizers/moeadd/moeadd.py @@ -106,13 +106,15 @@ def marriageSolutionAssignment(weights: np.ndarray, solutions: List[MOEADDSoluti w_preferences[i] = np.roll(w_preferences[i], shift = -1, axis = 0) w_preferences[i, -1] = -1 - print('acute_angles\n', acute_angles) - print('matches\n', matches) + if global_var.verbose.show_iter_idx: + print('acute_angles\n', acute_angles) + print('matches\n', matches) checkWeightAssignmentUniqueness(matches) for sol_idx, solution in enumerate(solutions): weight_idx = np.where(matches[:, sol_idx] == 1)[0][0] - print(f'Assigned weight {weight_idx} for {sol_idx}') + if global_var.verbose.show_iter_idx: + print(f'Assigned weight {weight_idx} for {sol_idx}') solution.set_domain(weight_idx) @@ -251,11 +253,11 @@ def delete_point(self, point): """ new_levels = [] population_cleared = [] - point_system = point.terms_labels + point_system = point.equations_labels for level in self.levels: temp = [] for element in level: - if element.terms_labels != point_system: + if element.equations_labels != point_system: temp.append(element) population_cleared.append(element) if not len(temp) == 0: @@ -301,7 +303,10 @@ def get_by_complexity(self, complexity): def set_weights(self, weights): if weights is None: - print(f'Setting ParetoLevels attribule weights with None: this should be a placeholder, expect futher logs.') + warnings.warn( + "Setting ParetoLevels.weights to None: this should be a placeholder; " + "expect further logs." + ) #if neccessary, implement additional logic into setter self._weights = weights @@ -466,6 +471,10 @@ def __init__(self, population_instruct, pop_size, solution_params, self.best_obj = best_sol_vals self._hist = [] + # Per-epoch Pareto-level-0 snapshots populated during ``optimize``. + # Each entry is a list of ``{'text_form', 'obj_fun'}`` dicts -- one + # per solution on the non-dominated front at the end of that epoch. + self._pareto_history = [] def abbreviated_search(self, population, sorting_method, update_method): """ @@ -574,7 +583,8 @@ def pass_best_objectives(self, *args) -> None: None """ if len(self.pareto_levels.population) != 0: - print('comparing lengths', len(args), len(self.pareto_levels.population[0].obj_funs)) + if global_var.verbose.show_iter_idx: + print('comparing lengths', len(args), len(self.pareto_levels.population[0].obj_funs)) assert len(args) == len(self.pareto_levels.population[0].obj_funs) self.best_obj = np.empty(len(self.pareto_levels.population[0].obj_funs)) elif len(self.pareto_levels.unplaced_candidates) != 0: @@ -600,19 +610,25 @@ def set_strategy(self, strategy_director): builder.assemble(True) self.set_sector_processer(builder.processer) - def optimize(self, epochs): + def optimize(self, epochs, early_stopping_callback=None): """ - Method for the main unconstrained evolutionary optimization. Can be applied repeatedly to - the population, if the previous results are insufficient. The output of the - optimization shall be accessed with the ``optimizer.pareto_level`` object and + Method for the main unconstrained evolutionary optimization. Can be applied repeatedly to + the population, if the previous results are insufficient. The output of the + optimization shall be accessed with the ``optimizer.pareto_level`` object and its attributes ``.levels`` or ``.population``. - - Args: + + Args: epochs (`int`): Maximum number of iterations, during that the optimization will be held. - + early_stopping_callback (`callable`, optional): hook invoked at the end of every + epoch with ``(snapshot, epoch_idx)`` where ``snapshot`` is the list of + ``{'text_form': ..., 'obj_fun': ...}`` dicts for the current Pareto level 0. + Returning a truthy value terminates the optimization. Use this to plug in + domain-specific stop conditions (e.g. thesis runs that already match a known + ground-truth structure). Default ``None`` runs the full ``epochs`` budget. + Note: that if the algorithm converges to a single Pareto frontier, the optimization is stopped. - + """ if not self.abbreviated_search_executed: self.hist = [] @@ -622,18 +638,42 @@ def optimize(self, epochs): print(f'Multiobjective optimization : {epoch_idx}-th epoch.') for weight_idx in np.arange(len(self.weights)): if global_var.verbose.show_iter_idx: - print(f'During MO : processing {weight_idx}-th weight.') + print(f'During MO : processing {weight_idx}-th weight.') sp_kwargs = self.form_processer_args(weight_idx) - self.sector_processer.run(population_subset = self.pareto_levels, + self.sector_processer.run(population_subset = self.pareto_levels, EA_kwargs = sp_kwargs) stats = self.pareto_levels.get_stats() self._hist.append(stats) + # Snapshot the current Pareto-0 structures so consumers can + # ask "in which epoch was equation X first discovered?". + snapshot = [] + for sol in self.pareto_levels.levels[0]: + try: + obj = sol.obj_fun.tolist() if hasattr(sol.obj_fun, 'tolist') else list(sol.obj_fun) + except Exception: + obj = None + snapshot.append({'text_form': sol.text_form, 'obj_fun': obj}) + self._pareto_history.append(snapshot) if global_var.verbose.iter_fitness: print(f'\n--- Dominating Pareto front (epoch {epoch_idx}) ---') for sol_idx, solution in enumerate(self.pareto_levels.levels[0]): print(f' [{sol_idx}] obj_fun = {solution.obj_fun}') print(f' {solution.text_form}') - print('---') + print('---') + + if early_stopping_callback is not None: + try: + should_stop = bool(early_stopping_callback(snapshot, int(epoch_idx))) + except Exception as exc: + # A misbehaving callback must not abort the run; log + # and keep going so the user still gets a result. + print(f'[early_stopping_callback] raised {exc!r}; ignoring.') + should_stop = False + if should_stop: + if global_var.verbose.show_iter_idx: + print(f'Early stopping at epoch {int(epoch_idx) + 1}/' + f'{int(epochs)} (callback returned True).') + break def form_processer_args(self, cur_weight : int): # TODO: inspect the most convenient input format """ diff --git a/epde/optimizers/moeadd/population_constr.py b/epde/optimizers/moeadd/population_constr.py index 0c98f7b0..950d6339 100644 --- a/epde/optimizers/moeadd/population_constr.py +++ b/epde/optimizers/moeadd/population_constr.py @@ -39,9 +39,9 @@ def applyToPassed(self, passed_solution: SoEq, **kwargs): passed_solution.use_default_multiobjective_function(self.use_pic) def create(self, **kwargs): - # sparsity = kwargs.get('sparsity', 10 ** (np.random.uniform(low = np.log10(self.sparsity_interval[0]), - # high = np.log10(self.sparsity_interval[1]), - # size = len(self.vars_demand_equation)))) + sparsity = kwargs.get('sparsity', 10 ** (np.random.uniform(low = np.log10(self.sparsity_interval[0]), + high = np.log10(self.sparsity_interval[1]), + size = len(self.vars_demand_equation)))) # # nonzero_terms = kwargs.get('nonzero_terms', np.random.randint(low=1, # high=self.terms_number, # size=len(self.vars_demand_equation))) @@ -57,8 +57,8 @@ def create(self, **kwargs): # print(f'Creating new equation, sparsity value {sparsity}') metaparameters = {'terms_number' : {'optimizable' : False, 'value' : terms_number}, 'max_factors_in_term' : {'optimizable' : False, 'value' : max_factors_in_term}} - # for idx, variable in enumerate(self.vars_demand_equation): - # metaparameters[('sparsity', variable)] = {'optimizable' : True, 'value' : sparsity[idx]} + for idx, variable in enumerate(self.vars_demand_equation): + metaparameters[('sparsity', variable)] = {'optimizable' : True, 'value' : sparsity[idx]} # metaparameters[('nonzero_terms', variable)] = {'optimizable': True, 'value': nonzero_terms[idx]} # metaparameters[('threshold', variable)] = {'optimizable': True, 'value': threshold[idx]} # metaparameters[('nu', variable)] = {'optimizable': True, 'value': nu[idx]} diff --git a/epde/optimizers/moeadd/strategy.py b/epde/optimizers/moeadd/strategy.py index 8fd0281d..5774f981 100644 --- a/epde/optimizers/moeadd/strategy.py +++ b/epde/optimizers/moeadd/strategy.py @@ -15,10 +15,10 @@ from epde.operators.multiobjective.selections import MOEADDSelection from epde.operators.multiobjective.variation import get_basic_variation from epde.operators.common.fitness import L2Fitness, L2LRFitness, SolverBasedFitness, PIC, DeepXDEBasedFitness -from epde.operators.common.right_part_selection import RandomRHPSelector, EqRightPartSelector +from epde.operators.common.right_part_selection import RandomRHPSelector, EqRightPartSelector, SoEqRightPartSelector from epde.operators.multiobjective.moeadd_specific import get_pareto_levels_updater, SimpleNeighborSelector, get_initial_sorter -from epde.operators.common.sparsity import LASSOSparsity +from epde.operators.common.sparsity import LASSOSparsity, VWSRSparsity from epde.operators.common.coeff_calculation import LinRegBasedCoeffsEquation from epde.optimizers.builder import add_sequential_operators, OptimizationPatternDirector, StrategyBuilder @@ -28,7 +28,9 @@ class MOEADDDirector(OptimizationPatternDirector): """ Class for creating strategy builder of multicriterian optimization """ - def use_baseline(self, use_solver: bool = False, use_pic: bool = True, variation_params : dict = {}, mutation_params : dict = {}, + def use_baseline(self, use_solver: bool = False, use_pic: bool = True, + fitness_cls=None, sparsity_cls=None, + variation_params : dict = {}, mutation_params : dict = {}, sorter_params : dict = {}, pareto_combiner_params : dict = {}, pareto_updater_params : dict = {}, **kwargs): add_kwarg_to_operator = partial(add_base_param_to_operator, target_dict = kwargs) @@ -44,8 +46,8 @@ def use_baseline(self, use_solver: bool = False, use_pic: bool = True, variation # right_part_selector = RandomRHPSelector() right_part_selector = EqRightPartSelector() - - sparsity = LASSOSparsity() + + sparsity = (sparsity_cls if sparsity_cls is not None else VWSRSparsity)() coeff_calc = LinRegBasedCoeffsEquation() if use_solver: @@ -56,9 +58,9 @@ def use_baseline(self, use_solver: bool = False, use_pic: bool = True, variation sparsity_c = map_operator_between_levels(sparsity, 'gene level', 'chromosome level') coeff_calc_c = map_operator_between_levels(coeff_calc, 'gene level', 'chromosome level') else: - sparsity_c = sparsity; coeff_calc_c = coeff_calc + sparsity_c = sparsity; coeff_calc_c = coeff_calc - fitness = L2LRFitness(['penalty_coeff']) + fitness = (fitness_cls if fitness_cls is not None else L2LRFitness)(['penalty_coeff']) add_kwarg_to_operator(operator = fitness) fitness.set_suboperators({'sparsity' : sparsity_c, 'coeff_calc' : coeff_calc_c}) @@ -77,9 +79,15 @@ def use_baseline(self, use_solver: bool = False, use_pic: bool = True, variation sparsity_c = map_operator_between_levels(sparsity, 'gene level', 'chromosome level') + # Chromosome-level RPS that threads "already-fixed RPS signatures" + # across the equations of a SoEq, pre-scrubbing each later + # equation's structure so the post-hoc enforce_rps_uniqueness check + # becomes unnecessary (the entire SoEq comes out uniqueness-clean + # by construction). + sys_rps_inner = SoEqRightPartSelector() + sys_rps_inner.set_suboperators({'eq_right_part_selector': right_part_selector}) rps_cond = lambda x: any([not elem_eq.right_part_selected for elem_eq in x.vals]) - sys_rps = map_operator_between_levels(right_part_selector, 'gene level', 'chromosome level', - objective_condition=rps_cond) + sys_rps = OperatorCondition(sys_rps_inner, rps_cond) # Separate mutation from population updater for better customization. initial_sorter = get_initial_sorter(right_part_selector = sys_rps, chromosome_fitness = fitness, diff --git a/epde/optimizers/moeadd/supplementary.py b/epde/optimizers/moeadd/supplementary.py index d0c31681..8fa9f47d 100644 --- a/epde/optimizers/moeadd/supplementary.py +++ b/epde/optimizers/moeadd/supplementary.py @@ -84,31 +84,48 @@ def ndl_update(new_solution, levels) -> list: # efficient_ndl_update """ moving_set = {new_solution} - new_levels = deepcopy(levels) # levels# CAUSES ERRORS DUE TO DEEPCOPY + # Shallow per-level copy: ndl_update only mutates the outer list (slice + # assignment, append, extend) and the inner level lists (append, replace); + # the MOEADDSolution objects themselves are never mutated, so cloning them + # via deepcopy is pure overhead (per-call on every individual added to the + # non-dominated levels). Aliasing ``levels`` directly DOES corrupt the input + # because of the in-place slice assignment below -- the comprehension below + # gives us a fresh outer list and fresh inner lists while preserving the + # original solution-object identities. + new_levels = [list(lvl) for lvl in levels] for level_idx in np.arange(len(levels)): moving_set_new = set() for ms_idx, moving_set_elem in enumerate(moving_set): - if np.any([check_dominance(solution, moving_set_elem) for solution in new_levels[level_idx]]): + level_new = new_levels[level_idx] + # Compute each direction of dominance against the (possibly already + # mutated) new level exactly once instead of re-running the same + # list-comp inside up to three branches. + dom_over_me = [check_dominance(s, moving_set_elem) for s in level_new] + dom_by_me = [check_dominance(moving_set_elem, s) for s in level_new] + if any(dom_over_me): moving_set_new.add(moving_set_elem) - elif (not np.any([check_dominance(solution, moving_set_elem) for solution in new_levels[level_idx]]) and - not np.any([check_dominance(moving_set_elem, solution) for solution in new_levels[level_idx]])): - new_levels[level_idx].append(moving_set_elem) - elif np.all([check_dominance(moving_set_elem, solution) for solution in levels[level_idx]]): + elif not any(dom_by_me): + # Falls through from branch 1, so `not any(dom_over_me)` already holds: + # incomparable with every existing element, append to this level. + level_new.append(moving_set_elem) + elif all(check_dominance(moving_set_elem, s) for s in levels[level_idx]): + # NOTE: this branch deliberately checks the ORIGINAL ``levels`` + # snapshot, not the mutated ``new_levels``, to detect the case + # where this element dominates the entire pre-update Pareto + # layer and therefore deserves a new layer above it. temp_levels = new_levels[level_idx:] new_levels[level_idx:] = [] new_levels.append([moving_set_elem,]) - new_levels.extend(temp_levels) # ; completed_levels = True + new_levels.extend(temp_levels) else: - dominated_level_elems = [level_elem for level_elem in new_levels[level_idx] if check_dominance( - moving_set_elem, level_elem)] - non_dominated_level_elems = [ - level_elem for level_elem in new_levels[level_idx] if not check_dominance(moving_set_elem, level_elem)] - non_dominated_level_elems.append(moving_set_elem) - new_levels[level_idx] = non_dominated_level_elems - - for element in dominated_level_elems: - moving_set_new.add(element) + # Partial domination: keep non-dominated elements + me at this + # level; bump dominated elements down via moving_set_new. + new_levels[level_idx] = [le for le, dom in zip(level_new, dom_by_me) if not dom] + new_levels[level_idx].append(moving_set_elem) + for le, dom in zip(level_new, dom_by_me): + if dom: + moving_set_new.add(le) moving_set = moving_set_new if not len(moving_set): break diff --git a/epde/optimizers/single_criterion/strategy.py b/epde/optimizers/single_criterion/strategy.py index 58bc578e..c6c73846 100644 --- a/epde/optimizers/single_criterion/strategy.py +++ b/epde/optimizers/single_criterion/strategy.py @@ -7,7 +7,7 @@ from epde.operators.common.right_part_selection import RandomRHPSelector from epde.operators.common.fitness import L2Fitness -from epde.operators.common.sparsity import LASSOSparsity +from epde.operators.common.sparsity import LASSOSparsity, VWSRSparsity from epde.operators.common.coeff_calculation import LinRegBasedCoeffsEquation from epde.operators.singleobjective.mutations import get_singleobjective_mutation from epde.operators.singleobjective.variation import get_singleobjective_variation @@ -38,7 +38,7 @@ def use_baseline(self, params: dict, **kwargs): selection = RouletteWheelSelection(['parents_fraction']) add_kwarg_to_operator(operator = selection) - sparsity = LASSOSparsity() + sparsity = VWSRSparsity() coeff_calc = LinRegBasedCoeffsEquation() eq_fitness = L2Fitness(['penalty_coeff']) add_kwarg_to_operator(operator = eq_fitness) diff --git a/epde/structure/factor.py b/epde/structure/factor.py index 1b90232a..bcff0430 100644 --- a/epde/structure/factor.py +++ b/epde/structure/factor.py @@ -273,6 +273,48 @@ def cache_label(self): cache_label = factor_params_to_str(self) return cache_label + def _quantized_params(self, drop_power: bool = False) -> tuple: + """Return params with continuous-tolerance ones quantized into bucket + indices and exact-equality ones passed through. Continuous params + (those with ``equality_ranges[name] > 0``, e.g. trig ``freq``) get + ``int((v - bounds[0]) / equality_ranges[name])``; exact-equality + params (``power``, ``dim``) stay numeric. When ``drop_power=True`` + the param named ``'power'`` is omitted from the result tuple. + """ + parts = [] + for i in range(len(self.params)): + name = self.params_description[i]['name'] + if drop_power and name == 'power': + continue + v = self.params[i] + tol = self.equality_ranges.get(name, 0) + if tol > 0: + origin = self.params_description[i]['bounds'][0] + parts.append(int((v - origin) / tol)) + else: + parts.append(v) + return tuple(parts) + + @property + def structural_label(self): + """Hashable canonical identity for structural dedup. + + Sits next to ``cache_label`` (which keys the tensor cache and + must stay exact). Continuous params are quantized into bucket + indices so set-based dedup and ``Factor.__eq__``'s tolerance + comparison agree. + """ + return (self.cache_label[0], self._quantized_params(drop_power=False)) + + @property + def structural_label_without_power(self): + """``structural_label`` with the ``power`` param dropped. + + Used by ``simplify_equation`` to find shared factors across + terms regardless of their individual powers. + """ + return (self.cache_label[0], self._quantized_params(drop_power=True)) + @property def name(self): form = self.label + '{' diff --git a/epde/structure/main_structures.py b/epde/structure/main_structures.py index b530bfa3..7e31b570 100644 --- a/epde/structure/main_structures.py +++ b/epde/structure/main_structures.py @@ -37,6 +37,42 @@ from epde.supplementary import filter_powers, normalize_ts, population_sort, flatten, rts, exp_form, minmax_normalize +_DEFAULT_EQUATION_METAPARAMETERS = { + 'sparsity': {'optimizable': True, 'value': 1.}, + 'terms_number': {'optimizable': False, 'value': 5.}, + 'max_factors_in_term': {'optimizable': False, 'value': 1.}, +} + + +def _deepcopy_slots(src, memo, attrs_to_avoid_copy=()): + """Slot-aware deep copy used by Term/Equation/SoEq. + + Replicates the loop that previously lived in each class's __deepcopy__: + iterate __slots__, skip attrs in attrs_to_avoid_copy (sets them to None + instead), tolerate slots that are not yet set (AttributeError -> skip), + deepcopy lists element-by-element so subclassed list types survive. + + A free function (not a mixin) because __slots__ classes cannot gain a new + attribute via mixin without redeclaring slots; a helper sidesteps that. + """ + clss = src.__class__ + new_struct = clss.__new__(clss) + memo[id(src)] = new_struct + for k in src.__slots__: + try: + if k in attrs_to_avoid_copy: + setattr(new_struct, k, None) + else: + value = getattr(src, k) + if isinstance(value, list): + setattr(new_struct, k, [copy.deepcopy(elem, memo) for elem in value]) + else: + setattr(new_struct, k, copy.deepcopy(value, memo)) + except AttributeError: + pass + return new_struct + + class Term(ComplexStructure): """ Class for describing the term of differential equation @@ -56,8 +92,19 @@ class Term(ComplexStructure): 'pool', 'max_factors_in_term', 'cache_linked', 'occupied_tokens_labels', '_descr_variable_marker'] - def __init__(self, pool, passed_term=None, mandatory_family=None, max_factors_in_term=1, - create_derivs: bool = False, interelement_operator=np.multiply, collapse_powers = True): + def __init__(self, pool: 'TFPool', passed_term=None, mandatory_family: str = None, + max_factors_in_term: Union[int, dict] = 1, + create_derivs: bool = False, interelement_operator: Callable = np.multiply, + collapse_powers: bool = True): + """ + Construct a single Term (a product of Factor objects). + + If ``passed_term`` is None, the term is randomized from ``pool`` honoring + ``max_factors_in_term`` and any ``mandatory_family`` constraint. If + ``passed_term`` is a list/str, the term is built from the supplied factors + and ``collapse_powers`` controls whether identical factors are collapsed + into a single factor with summed power. + """ super().__init__(interelement_operator) self.pool = pool self.max_factors_in_term = max_factors_in_term @@ -207,8 +254,8 @@ def descr_variable_marker(self, marker: False): def evaluate(self, structural, grids=None): assert global_var.tensor_cache is not None, 'Currently working only with connected cache' normalize = structural - if self.saved[structural] or (self.term_label, normalize) in global_var.tensor_cache: - value = global_var.tensor_cache.get(self.term_label, normalized=normalize, + if self.saved[structural] or (self.factors_labels, normalize) in global_var.tensor_cache: + value = global_var.tensor_cache.get(self.factors_labels, normalized=normalize, saved_as=self.saved_as[normalize]) value = value.reshape(-1) return value @@ -228,13 +275,14 @@ def evaluate(self, structural, grids=None): # value *= factor_value_normalized if np.all([len(factor.params) == 1 for factor in self.structure]) and grids is None: # Место возможных проблем: сохранение/загрузка нормализованных данных - self.saved[normalize] = global_var.tensor_cache.add(self.term_label, value, normalized=normalize) + self.saved[normalize] = global_var.tensor_cache.add(self.factors_labels, value, normalized=normalize) if self.saved[normalize]: - self.saved_as[normalize] = self.term_label + self.saved_as[normalize] = self.factors_labels value = value.reshape(-1) return value - def filter_tokens_by_right_part(self, reference_target, equation, equation_position): + def filter_tokens_by_right_part(self, reference_target, equation, equation_position, + max_retries: int = 100): warnings.warn(message='Tokens can no longer be set as right-part-unique', category=DeprecationWarning) taken_tokens = [factor.label for factor in reference_target.structure @@ -242,9 +290,8 @@ def filter_tokens_by_right_part(self, reference_target, equation, equation_posit meaningful_taken = any([factor.status['meaningful'] for factor in reference_target.structure if factor.status['unique_for_right_part']]) - accept_term_try = 0 - while True: - accept_term_try += 1 + new_term = None + for accept_term_try in range(1, max_retries + 1): new_term = copy.deepcopy(self) for factor_idx, factor in enumerate(new_term.structure): if factor.label in taken_tokens: @@ -255,14 +302,17 @@ def filter_tokens_by_right_part(self, reference_target, equation, equation_posit self.structure = new_term.structure self.structure = filter_powers(self.structure) self.reset_saved_state() - break + return if accept_term_try == 10 and global_var.verbose.show_warnings: warnings.warn('Can not create unique term, while filtering equation tokens in regards to the right part.') if accept_term_try >= 10: self.randomize(forbidden_factors=new_term.occupied_tokens_labels + taken_tokens) - if accept_term_try == 100: - print('Something wrong with the random generation of term while running "filter_tokens_by_right_part"') - print('proposed', new_term.name, 'for ', equation.text_form, 'with respect to', reference_target.name) + + last_attempt_name = new_term.name if new_term is not None else '' + raise RuntimeError( + f'filter_tokens_by_right_part: failed to create unique term after ' + f'{max_retries} retries. Last attempted: {last_attempt_name} for ' + f'{equation.text_form} with respect to {reference_target.name}') def reset_occupied_tokens(self): occupied_tokens_new = [] @@ -286,6 +336,20 @@ def available_tokens(self): available_tokens.append(token_new) return available_tokens + def iter_available_tokens(self): + """Generator equivalent of `available_tokens`; yields one filtered family at a time. + + Allows consumers that only need to iterate (rather than realize the full + list) to avoid the per-call list materialization. Each yielded family is + still deepcopied — that's the unavoidable per-element cost. + """ + for token in self.pool.families: + if not all([label in self.occupied_tokens_labels for label in token.tokens]): + token_new = copy.deepcopy(token) + token_new.tokens = [ + label for label in token.tokens if label not in self.occupied_tokens_labels] + yield token_new + @property def total_params(self): return max(sum([len(element.params) - 1 for element in self.structure]), 1) @@ -331,52 +395,39 @@ def __eq__(self, other): @HistoryExtender('\n -> was copied by deepcopy(self)', 'n') def __deepcopy__(self, memo=None): - clss = self.__class__ - new_struct = clss.__new__(clss) - memo[id(self)] = new_struct - - attrs_to_avoid_copy = [] - for k in self.__slots__: - try: - if k not in attrs_to_avoid_copy: - if not isinstance(k, list): - setattr(new_struct, k, copy.deepcopy( - getattr(self, k), memo)) - else: - temp = [] - for elem in getattr(self, k): - temp.append(copy.deepcopy(elem, memo)) - setattr(new_struct, k, temp) - else: - setattr(new_struct, k, None) - except AttributeError: - pass + return _deepcopy_slots(self, memo) - return new_struct + @property + def factors_labels_without_power(self) -> frozenset: + """Return a frozenset of structural labels with the ``power`` param dropped. + + Identity is delegated to ``Factor.structural_label_without_power``, + which quantizes continuous-tolerance params (e.g. trig ``freq``) + into bucket indices so structural dedup stays consistent with + ``Factor.__eq__``. + """ + return frozenset(factor.structural_label_without_power for factor in self.structure) + + @property + def factors_labels(self) -> frozenset: + """Return a frozenset of structural labels for each factor in the term. + + Identity is delegated to ``Factor.structural_label``, which + bucketises continuous-tolerance params (e.g. trig ``freq``) so + within-bucket differences don't fracture structural identity. + Used as a hashable identity for set/membership checks. + """ + return frozenset(factor.structural_label for factor in self.structure) @property def term_label_without_power(self): - described = set() - for factor in self.structure: - if len(factor.params) == 1: - factor_label = (factor.cache_label[0]) - else: - factor_label = (factor.cache_label[0], (factor.cache_label[1][-1])) - described.add(factor_label) - described = frozenset(described) - return described + # TODO(deprecate): use factors_labels_without_power + return self.factors_labels_without_power @property def term_label(self): - described = set() - for factor in self.structure: - if factor.ftype == 'trigonometric': - label = (factor.cache_label[0], tuple(factor.cache_label[1][i] for i, param in factor.params_description.items() if param['name'] != 'freq')) - described.add(label) - else: - described.add(factor.cache_label) - described = frozenset(described) - return described + # TODO(deprecate): use factors_labels + return self.factors_labels class Equation(ComplexStructure): @@ -385,13 +436,12 @@ class Equation(ComplexStructure): 'target_idx', 'right_part_selected', '_weights_final', 'weights_final_evald', 'simplified', 'is_correct_right_part', '_weights_internal', 'weights_internal_evald', 'fitness_calculated', 'stability_calculated', 'aic_calculated', 'solver_form_defined', '_fitness_value', '_coefficients_stability', '_aic', 'metaparameters', 'main_var_to_explain', - '_eval_cache', '_cached_sw_weights'] # , '_solver_form' + '_eval_cache', '_cached_sw_weights', + '_terms_labels_cache', '_terms_labels_without_power_cache'] # , '_solver_form' def __init__(self, pool: TFPool, basic_structure: Union[list, tuple, set], var_to_explain: str = None, - metaparameters: dict = {'sparsity': {'optimizable': True, 'value': 1.}, - 'terms_number': {'optimizable': False, 'value': 5.}, - 'max_factors_in_term': {'optimizable': False, 'value': 1.}}, + metaparameters: dict = None, interelement_operator: Callable = np.add): """ @@ -430,6 +480,9 @@ def __init__(self, pool: TFPool, basic_structure: Union[list, tuple, set], var_t super().__init__(interelement_operator) self.reset_state() + if metaparameters is None: + metaparameters = copy.deepcopy(_DEFAULT_EQUATION_METAPARAMETERS) + self.n_immutable = len(basic_structure) self.pool = pool self.structure = [] @@ -448,21 +501,27 @@ def __init__(self, pool: TFPool, basic_structure: Union[list, tuple, set], var_t self.main_var_to_explain = var_to_explain force_var_to_explain = True # False + max_iter = 100 for i in range(len(basic_structure), int(self.metaparameters['terms_number']['value'])): new_term = Term(self.pool, max_factors_in_term=self.metaparameters['max_factors_in_term']['value'], mandatory_family=None, passed_term=None) - while new_term.term_label in self.terms_labels: + for _ in range(max_iter): + if new_term.factors_labels not in self.terms_labels: + break new_term.randomize() new_term.reset_saved_state() - # check_test += 1 - # - - - # if new_term.described_variables_extra not in self.described_variables_full: - # force_var_to_explain = False - # break - + else: + # Pool can't yield a unique term against the current + # structure -- stop, don't try further slots. Subsequent + # ``new_term`` draws would face the same exhausted pool, + # so the only honest outcome is a shorter equation. + warnings.warn( + f"Equation.__init__: no unique term in {max_iter} attempts at slot {i}; " + "pool may be exhausted -- stopping with a shorter equation." + ) + break self.structure.append(new_term) + self._invalidate_label_cache() for idx, _ in enumerate(self.structure): self.structure[idx].use_cache() @@ -487,6 +546,7 @@ def manual_reconst(self, attribute:str, value, except_attrs:dict): attrs_from_dict(term, term_elem, except_attrs) self.structure.append(term) + self._invalidate_label_cache() def reset_explaining_term(self, term_idx=0): for idx, term in enumerate(self.structure): @@ -509,8 +569,17 @@ def remove_zero_terms(self): if self.weights_internal[idx] == 0: target_bias += 1 if i < self.target_idx else 0 zero_terms.append(i) - self.structure = [term for term_idx, term in enumerate(self.structure) if term_idx not in zero_terms] - self.target_idx -= target_bias + if zero_terms: + self.structure = [term for term_idx, term in enumerate(self.structure) if term_idx not in zero_terms] + self.target_idx -= target_bias + # ``_invalidate_label_cache`` also wipes _eval_cache, which + # is essential here: the right-part-selector's per-target + # sweep populates the cache keyed on target_idx, and the + # adjusted target_idx above can collide with a swept value. + # ``_cached_sw_weights`` was computed for the surviving + # features and still aligns with the new structure, so it + # is preserved. + self._invalidate_label_cache() def __eq__(self, other): @@ -548,27 +617,58 @@ def restore_property(self, deriv: bool = False, mandatory_family: bool = False, # TODO: non-urgent, rewrite for an arbitrary equation property check if not (deriv or mandatory_family): raise ValueError('No property passed for restoration.') - while True: - # print( - # f'Restoring containment of {mandatory_family} in {self.text_form}.') + # Bound both the outer and the inner sampling loops, and reject any + # candidate whose factor signature would collide with another + # existing term -- see feedback-structure-dedup memory. + max_outer = 200 + max_inner = 100 + + def _would_duplicate(idx, candidate): + sig = candidate.factors_labels + return any(j != idx and other.factors_labels == sig + for j, other in enumerate(self.structure)) + + mf_marker = self.main_var_to_explain if mandatory_family else None + max_factors = self.metaparameters['max_factors_in_term']['value'] + for _ in range(max_outer): replacement_idx = np.random.randint(low=0, high=len(self.structure)) - mf_marker = self.main_var_to_explain if mandatory_family else None - temp = Term(self.pool, mandatory_family=mf_marker, - max_factors_in_term=self.metaparameters['max_factors_in_term']['value']) + temp = Term(self.pool, mandatory_family=mf_marker, max_factors_in_term=max_factors) if t_derivative: - while not temp.contains_t_derivative(): - temp = Term(self.pool, mandatory_family=mf_marker, - max_factors_in_term=self.metaparameters['max_factors_in_term']['value']) - break + inner = 0 + while not temp.contains_t_derivative() and inner < max_inner: + temp = Term(self.pool, mandatory_family=mf_marker, max_factors_in_term=max_factors) + inner += 1 + if not temp.contains_t_derivative(): + continue + if _would_duplicate(replacement_idx, temp): + continue + self.structure[replacement_idx] = temp + self._invalidate_label_cache() + return if deriv and mandatory_family and temp.contains_deriv() and temp.contains_variable(self.main_var_to_explain): + if _would_duplicate(replacement_idx, temp): + continue self.structure[replacement_idx] = temp - break + self._invalidate_label_cache() + return elif deriv and temp.contains_deriv(self.main_var_to_explain) and not mandatory_family: + if _would_duplicate(replacement_idx, temp): + continue self.structure[replacement_idx] = temp - break + self._invalidate_label_cache() + return elif mandatory_family and temp.contains_variable(self.main_var_to_explain) and not deriv: + if _would_duplicate(replacement_idx, temp): + continue self.structure[replacement_idx] = temp - break + self._invalidate_label_cache() + return + warnings.warn( + f'Equation.restore_property: could not satisfy ' + f'deriv={deriv}, mandatory_family={mandatory_family}, ' + f't_derivative={t_derivative} without duplication after ' + f'{max_outer} attempts; leaving structure unchanged.' + ) def reconstruct_by_right_part(self, right_part_idx): warnings.warn(message='Tokens can no longer be set as right-part-unique', @@ -585,9 +685,29 @@ def reconstruct_by_right_part(self, right_part_idx): new_eq.reset_saved_state() return new_eq - def evaluate(self, normalize=True, return_val=False, grids=None): - cache_key = (normalize, return_val, grids is None) - if grids is None and hasattr(self, '_eval_cache') and cache_key in self._eval_cache: + def evaluate(self, normalize: bool = True, return_val: bool = False, + grids: list = None) -> Tuple: + """Evaluate the equation and return (value, target, features). + + ``target`` is the LHS term values; ``features`` is a 2-D matrix of the + non-target term evaluations (``None`` if every other term is zero-weight + and ``normalize=False``); ``value`` is the residual when + ``return_val=True`` else ``None``. + + Caching policy: results are cached per + (normalize, return_val, grids-is-None, target_idx) when + ``grids is None`` AND ``normalize`` is True. The ``normalize=False`` + branch additionally filters ``feature_indexes`` by the current + ``weights_internal`` (lines below); since callers update weights + between successive ``evaluate(normalize=False)`` calls, caching that + branch would risk returning stale (target, features) tuples with + out-of-date feature masks. ``normalize=True`` is weight-independent + and is the path benefitting from cache hits (sparsity then L2LRFitness + both call ``evaluate(normalize=True)`` in one fitness invocation). + """ + cacheable = (grids is None) and normalize + cache_key = (normalize, return_val, grids is None, self.target_idx) + if cacheable and hasattr(self, '_eval_cache') and cache_key in self._eval_cache: return self._eval_cache[cache_key] target = self.structure[self.target_idx].evaluate(False, grids=grids) @@ -629,18 +749,24 @@ def evaluate(self, normalize=True, return_val=False, grids=None): else: features_val = np.zeros_like(target) value = np.add(elem1, - features_val) - # print(value.shape) result = (value, target, features) else: result = (None, target, features) - if grids is None: + if cacheable: if not hasattr(self, '_eval_cache'): self._eval_cache = {} self._eval_cache[cache_key] = result return result - def reset_state(self, reset_right_part: bool = True): + def reset_state(self, reset_right_part: bool = True) -> None: + """Drop all cached evaluation/fitness state on this Equation. + + Call after any structural mutation (or to discard a stale fitness/AIC + evaluation). Set ``reset_right_part=False`` to keep target_idx and + weight assignments — useful when only the LHS-derived caches need + clearing. + """ if reset_right_part: self.right_part_selected = False self.is_correct_right_part = False @@ -660,32 +786,33 @@ def reset_state(self, reset_right_part: bool = True): self.aic_calculated = False self.solver_form_defined = False self._eval_cache = {} + # consumed by epde.operators.common.fitness.L2LRFitness; resets here. self._cached_sw_weights = None + self._terms_labels_cache = None + self._terms_labels_without_power_cache = None + + def _invalidate_label_cache(self): + """Drop memoized caches keyed on the current structure; call after + ``self.structure`` (or ``self.target_idx``) mutates. + + Covers both the terms-labels caches and the per-evaluation + ``_eval_cache`` populated by :meth:`evaluate`. The eval cache key + includes ``self.target_idx`` and the cached value depends on which + terms occupy ``self.structure``, so any structural mutation must + drop it -- otherwise callers like the right-part-selector's + per-target sweep can leave stale entries that survive into the + post-RPS fitness call (e.g. after ``remove_zero_terms`` adjusts + ``target_idx`` onto a value the sweep already cached). + """ + self._terms_labels_cache = None + self._terms_labels_without_power_cache = None + if hasattr(self, '_eval_cache'): + self._eval_cache = {} @HistoryExtender('\n -> was copied by deepcopy(self)', 'n') def __deepcopy__(self, memo=None): - clss = self.__class__ - new_struct = clss.__new__(clss) - memo[id(self)] = new_struct - - attrs_to_avoid_copy = [] - for k in self.__slots__: - try: - if k not in attrs_to_avoid_copy: - if not isinstance(k, list): - setattr(new_struct, k, copy.deepcopy(getattr(self, k), memo)) - else: - temp = [] - for elem in getattr(self, k): - temp.append(copy.deepcopy(elem, memo)) - setattr(new_struct, k, temp) - else: - setattr(new_struct, k, None) - except AttributeError: - pass - - return new_struct + return _deepcopy_slots(self, memo) def copy_properties_to(self, new_equation): new_equation.weights_internal_evald = self.weights_internal_evald @@ -714,20 +841,34 @@ def copy_properties_to(self, new_equation): pass def add_history(self, add): - # print(add) self._history += add - def add_random_term(self): + def add_random_term(self) -> bool: + """Try to append one fresh, non-duplicate term to ``self.structure``. + + Returns ``True`` if a term was appended, ``False`` if either the + ``terms_number`` cap was already reached or the token pool could + not produce a non-duplicate within ``max_iter`` retries. Callers + that invoke this in a loop (e.g. ``EquationMutation.apply``, + ``Equation.__init__``) MUST stop on the first ``False`` -- once + the pool stops yielding uniques, further calls will not yield any + either, and continuing past the failure pushes downstream + operators (``_scrub_conflicting_terms``, ``EqRightPartSelector``) + into states that violate the duplicate-term invariant. + """ + cap = int(self.metaparameters['terms_number']['value']) + if len(self.structure) >= cap: + return False + max_iter = 10 new_term = Term(self.pool, max_factors_in_term=self.metaparameters['max_factors_in_term']['value'], mandatory_family=None, passed_term=None) - - attempt = 0 - while new_term.term_label in self.terms_labels or attempt < 10: + for _ in range(max_iter): + if new_term.factors_labels not in self.terms_labels: + self.structure.append(deepcopy(new_term)) + self._invalidate_label_cache() + return True new_term.randomize() - attempt += 1 - - if attempt < 10: - self.structure.append(deepcopy(new_term)) + return False @property def history(self): @@ -803,7 +944,7 @@ def text_form(self): form += 'k_' + str(term_idx) + ' ' + \ self.structure[term_idx].name + ' + ' form += 'k_' + str(len(self.structure)) + ' = 0' - except: + except (AttributeError, IndexError, TypeError): form = '' return form @@ -831,7 +972,19 @@ def state(self): return self.text_form @property - def terms_labels_without_power(self): + def terms_labels_without_power(self) -> frozenset: + """Frozenset of per-term factor-label sets, with the power parameter dropped. + + Skips terms whose internal weight is exactly zero (target term always + contributes). Memoized in ``_terms_labels_without_power_cache``; the + 15 call sites of :meth:`_invalidate_label_cache` cover every + Equation-driven structure mutation. Term-level mutations from + external operators that bypass the Equation must invalidate the + cache themselves. + """ + cached = getattr(self, '_terms_labels_without_power_cache', None) + if cached is not None: + return cached described = set() for term_idx, term in enumerate(self.structure): cache_label = set() @@ -852,13 +1005,23 @@ def terms_labels_without_power(self): factor_label = (factor.cache_label[0], (factor.cache_label[1][-1])) cache_label.add(factor_label) if len(cache_label) > 0: - cache_label = frozenset(cache_label) - described.add(cache_label) - described = frozenset(described) - return described + described.add(frozenset(cache_label)) + result = frozenset(described) + self._terms_labels_without_power_cache = result + return result @property - def terms_labels(self): + def terms_labels(self) -> frozenset: + """Frozenset of per-term factor-label sets identifying this equation's structure. + + Each inner element is the ``Term.factors_labels`` of one term. Used as + a hashable structural fingerprint for membership tests against + ``objective.history``. Memoized in ``_terms_labels_cache``; see + ``terms_labels_without_power`` for invalidation contract. + """ + cached = getattr(self, '_terms_labels_cache', None) + if cached is not None: + return cached described = set() for term_idx, term in enumerate(self.structure): cache_label = set() @@ -868,10 +1031,20 @@ def terms_labels(self): cache_label.add(label) else: cache_label.add(factor.cache_label) - cache_label = frozenset(cache_label) - described.add(cache_label) - described = frozenset(described) - return described + described.add(frozenset(cache_label)) + result = frozenset(described) + self._terms_labels_cache = result + return result + + @property + def factors_labels(self) -> frozenset: + """Alias of ``terms_labels`` — naming mirror used by some operators.""" + return self.terms_labels + + @property + def factors_labels_without_power(self) -> frozenset: + """Alias of ``terms_labels_without_power``.""" + return self.terms_labels_without_power def max_deriv_orders(self): solver_form = self.solver_form() @@ -994,8 +1167,9 @@ def check_metaparameters(metaparameters: dict): class SoEq(moeadd.MOEADDSolution): - def __init__(self, pool: TFPool, metaparameters: dict): + def __init__(self, pool: TFPool, metaparameters: dict) -> None: ''' + Top-level solution gene: a system of one Equation per variable. Parameters ---------- @@ -1045,13 +1219,12 @@ def use_default_multiobjective_function(self, use_pic: bool = False): self.use_legacy_multiobjective_function() def use_legacy_multiobjective_function(self): - from epde.eq_mo_objectives import generate_partial, equation_fitness, equation_complexity_by_factors - complexity_objectives = [generate_partial(equation_complexity_by_factors, eq_key) - for eq_key in self.vars_to_describe] - quality_objectives = [generate_partial( - equation_fitness, eq_key) for eq_key in self.vars_to_describe] - self.set_objective_functions( - quality_objectives + complexity_objectives) + from epde.eq_mo_objectives import equation_fitness, equation_complexity_by_factors + # Both functions return per-equation tuples when called without an + # equation_key, so the overall obj_fun layout matches the NEW path + # (one weight per objective TYPE, expanded across equations by + # MOEA/D). See penalty_based_intersection for the expansion logic. + self.set_objective_functions([equation_fitness, equation_complexity_by_factors]) def use_pic_multiobjective_function(self): from epde.eq_mo_objectives import generate_partial, equation_fitness, equation_complexity_by_factors, equation_terms_stability, equation_aic @@ -1192,27 +1365,15 @@ def __hash__(self): return hash(self.vals.hash_descr) def __deepcopy__(self, memo=None): - clss = self.__class__ - new_struct = clss.__new__(clss) - memo[id(self)] = new_struct - + # SoEq has no own __slots__; the helper iterates the inherited + # (likely empty) ABC slots harmlessly. Then carry the __dict__ over. + new_struct = _deepcopy_slots(self, memo) for k, v in self.__dict__.items(): setattr(new_struct, k, copy.deepcopy(v, memo)) - - for k in self.__slots__: - try: - if not isinstance(k, list): - setattr(new_struct, k, copy.deepcopy(getattr(self, k), memo)) - else: - temp = [] - for elem in getattr(self, k): - temp.append(copy.deepcopy(elem, memo)) - setattr(new_struct, k, temp) - except AttributeError: - pass return new_struct - def reset_state(self, reset_right_part: bool = True): + def reset_state(self, reset_right_part: bool = True) -> None: + """Forward reset_state to every Equation in this system.""" for equation in self.vals: equation.reset_state(reset_right_part) @@ -1220,9 +1381,13 @@ def copy_properties_to(self, objective): for eq_label in self.vals.equation_keys: # Not the best code possible here self.vals[eq_label].copy_properties_to(objective.vals[eq_label]) - def solver_params(self, full_domain, grids=None): + def solver_params(self, full_domain: bool, grids: list = None) -> Tuple: ''' - Returns solver form, grid and boundary conditions + Return solver form, grid and boundary conditions for every equation. + + Pass ``full_domain=True`` to read from the initial-data cache (the + complete sampled domain) instead of the active grid cache. ``grids`` + overrides the implicit grid used to evaluate solver forms. ''' equation_forms = [] bconds = [] @@ -1242,19 +1407,39 @@ def fitness_calculated(self): return all([equation.fitness_calculated for equation in self.vals]) @property - def terms_labels_without_power(self): + def equations_labels_without_power(self) -> Tuple[frozenset, ...]: + """Tuple of ``Equation.terms_labels_without_power`` for each equation. + + Order matches ``self.vars_to_describe``. Useful for structural identity + checks on the system as a whole (e.g., dedup against history). + """ equations_caches = [] for equation in self.vals: equations_caches.append(equation.terms_labels_without_power) return tuple(equations_caches) @property - def terms_labels(self): + def equations_labels(self) -> Tuple[frozenset, ...]: + """Tuple of ``Equation.terms_labels`` for each equation in the system. + + Element order matches ``self.vars_to_describe``. The hashable per-equation + frozensets enable ``system in objective.history`` membership checks. + """ equations_caches = [] for equation in self.vals: equations_caches.append(equation.terms_labels) return tuple(equations_caches) + @property + def terms_labels_without_power(self): + # TODO(deprecate): use equations_labels_without_power + return self.equations_labels_without_power + + @property + def terms_labels(self): + # TODO(deprecate): use equations_labels + return self.equations_labels + class SoEqIterator(object): def __init__(self, system: SoEq): diff --git a/epde/supplementary.py b/epde/supplementary.py index cf8da907..69cd6ac7 100644 --- a/epde/supplementary.py +++ b/epde/supplementary.py @@ -281,17 +281,17 @@ def detect_similar_terms(base_equation_1, base_equation_2): different_terms = all_first_equation_terms.symmetric_difference(all_second_equation_terms) for term in base_equation_1.structure: - if term.term_label in common_terms: + if term.factors_labels in common_terms: same_terms_from_eq1.append(term) - elif term.term_label in (all_first_equation_terms - all_second_equation_terms): + elif term.factors_labels in (all_first_equation_terms - all_second_equation_terms): similar_terms_from_eq1.append(term) else: different_terms_from_eq1.append(term) for term in base_equation_2.structure: - if term.term_label in common_terms: + if term.factors_labels in common_terms: same_terms_from_eq2.append(term) - elif term.term_label in (all_second_equation_terms - all_first_equation_terms): + elif term.factors_labels in (all_second_equation_terms - all_first_equation_terms): similar_terms_from_eq2.append(term) else: different_terms_from_eq2.append(term) @@ -393,76 +393,221 @@ def minmax_normalize(matrix): return matrix -def calculate_weights(X, y, sample_weights, grid_shape, fit_intercept=True): +def _cholesky_solve_batched(A, b): + """Solve ``A @ x = b`` batched over the leading axis using Cholesky. + + ``A`` is assumed symmetric positive-definite (shape ``(batch, n, n)``); + ``b`` is the RHS ``(batch, n, 1)``. Returns ``(x, L)`` where ``x`` is + the solution and ``L`` is the lower-triangular factor (so the caller + can reuse it for iterative refinement). If Cholesky fails on any batch + entry, returns ``(None, None)`` to signal "use the lstsq fallback". + + numpy doesn't ship a batched triangular solver, so the two triangular + solves go through ``np.linalg.solve`` -- still SPD-stable and ~1.5x + faster than feeding the full ``A`` to ``np.linalg.solve``. """ - Vectorized calculation of weights across sliding windows. - Dynamically handles whether the intercept should be fit. + try: + L = np.linalg.cholesky(A) + except np.linalg.LinAlgError: + return None, None + try: + z = np.linalg.solve(L, b) + x = np.linalg.solve(L.transpose(0, 2, 1), z) + except np.linalg.LinAlgError: + return None, L + return x, L + + +def _per_batch_lstsq(A, b): + """Per-batch SVD-based least-squares solve. Used as the safety net + when Cholesky reports the equilibrated batch is non-SPD. Returns + weights of shape ``(batch, n, 1)`` matching the input RHS layout so + the caller can compose with subsequent matrix products without + reshaping. + """ + batch_size = A.shape[0] + n = A.shape[1] + out = np.empty((batch_size, n, 1)) + for i in range(batch_size): + sol, *_ = np.linalg.lstsq(A[i], b[i, :, 0], rcond=None) + out[i, :, 0] = sol + return out + + +class GramSetup: + """Precomputed batched normal-equation matrices for fast active-mask + solves. Splits :func:`calculate_weights` into a setup phase (compute + ``X^T diag(w) X`` and ``X^T diag(w) y`` per window-batch per dimension, + using the FULL augmented feature matrix) and a solve phase (slice each + full Gram matrix by an active-feature mask and solve). The setup is + mask-independent; only the solve depends on which columns are active. + + Used by :class:`PhysicsInformedLasso.fit`, whose outer RFE loop calls + ``calculate_weights`` per shrinking column subset. With this split the + expensive ``X^T diag(w) X`` matmul runs ONCE per fit and each outer + iter only pays the cost of an (active × active) solve. The math is + exact: a sub-block of a Gram matrix equals the Gram of the + corresponding sub-columns. """ - n_samples, n_features = X.shape - # 1. Augment X with intercept ONLY if it is currently active - if fit_intercept: + def __init__(self, X, y, sample_weights, grid_shape): + n_samples = X.shape[0] + # Always augment X with the intercept column so callers can toggle + # ``fit_intercept`` via the active mask's last bit rather than + # re-running setup. X_aug = np.hstack([X, np.ones((n_samples, 1))]) - else: - X_aug = X # Use raw X directly - - n_features_aug = X_aug.shape[1] - - # 2. Reshape to spatial grid - X_grid = X_aug.reshape(*grid_shape, n_features_aug) - y_grid = y.reshape(*grid_shape) - sample_weights_grid = sample_weights.reshape(*grid_shape) - - all_weights = [] - - # 3. Iterate over dimensions - for dim in range(len(grid_shape)): - window_size = grid_shape[dim] // 2 - num_horizons = window_size + 1 - step_size = max(1, num_horizons // 30) - - # --- Create Sliding Windows (Zero Copy) --- - X_windows = sliding_window_view(X_grid, window_shape=window_size, axis=dim) - y_windows = sliding_window_view(y_grid, window_shape=window_size, axis=dim) - w_windows = sliding_window_view(sample_weights_grid, window_shape=window_size, axis=dim) - - # Apply step size stride - X_windows = X_windows.take(indices=range(0, num_horizons, step_size), axis=dim) - y_windows = y_windows.take(indices=range(0, num_horizons, step_size), axis=dim) - w_windows = w_windows.take(indices=range(0, num_horizons, step_size), axis=dim) - - # --- Reshape for Batch Regression --- - X_windows = np.moveaxis(X_windows, dim, 0) - y_windows = np.moveaxis(y_windows, dim, 0) - w_windows = np.moveaxis(w_windows, dim, 0) - - X_windows = np.moveaxis(X_windows, -2, -1) - - # Flatten spatial dimensions - batch_size = X_windows.shape[0] - X_batch = X_windows.reshape(batch_size, -1, n_features_aug) - y_batch = y_windows.reshape(batch_size, -1) - weights_batch = w_windows.reshape(batch_size, -1, 1) - - # --- Solve Normal Equations (Batch Mode) --- - XTW = X_batch.transpose(0, 2, 1) * weights_batch.transpose(0, 2, 1) - XTWX = XTW @ X_batch - XTWy = XTW @ y_batch[..., None] - - # Dynamic ridge penalty based on current active features - ridge = 1e-6 * np.eye(n_features_aug) - XTWX += ridge - - # 2. Solve (Fast CPU Vectorized Solver) - try: - w_batch = np.linalg.solve(XTWX, XTWy) - all_weights.append(w_batch.squeeze(-1)) - except np.linalg.LinAlgError: - w_batch = np.linalg.lstsq(XTWX, XTWy, rcond=None)[0] - # lstsq returns 2D array if targets are 1D, so check shape - if w_batch.ndim == 3: - all_weights.append(w_batch.squeeze(-1)) + n_features_aug = X_aug.shape[1] + + X_grid = X_aug.reshape(*grid_shape, n_features_aug) + y_grid = y.reshape(*grid_shape) + sample_weights_grid = sample_weights.reshape(*grid_shape) + + self.n_features_aug = n_features_aug + self.grid_shape = grid_shape + self._per_dim = [] + + for dim in range(len(grid_shape)): + window_size = grid_shape[dim] // 2 + num_horizons = window_size + 1 + step_size = max(1, num_horizons // 30) + + X_windows = sliding_window_view(X_grid, window_shape=window_size, axis=dim) + y_windows = sliding_window_view(y_grid, window_shape=window_size, axis=dim) + w_windows = sliding_window_view(sample_weights_grid, window_shape=window_size, axis=dim) + + X_windows = X_windows.take(indices=range(0, num_horizons, step_size), axis=dim) + y_windows = y_windows.take(indices=range(0, num_horizons, step_size), axis=dim) + w_windows = w_windows.take(indices=range(0, num_horizons, step_size), axis=dim) + + X_windows = np.moveaxis(X_windows, dim, 0) + y_windows = np.moveaxis(y_windows, dim, 0) + w_windows = np.moveaxis(w_windows, dim, 0) + X_windows = np.moveaxis(X_windows, -2, -1) + + batch_size = X_windows.shape[0] + X_batch = X_windows.reshape(batch_size, -1, n_features_aug) + y_batch = y_windows.reshape(batch_size, -1) + weights_batch = w_windows.reshape(batch_size, -1, 1) + + XTW = X_batch.transpose(0, 2, 1) * weights_batch.transpose(0, 2, 1) + XTWX_full = XTW @ X_batch + XTWy_full = XTW @ y_batch[..., None] + + # Per-batch column scales for equilibration in :meth:`solve`. + # ``diag`` is the per-feature L2 norm squared (weighted) of the + # underlying X columns; ``sqrt`` brings it back to a column- + # norm scale. The ``1e-30`` floor is a degenerate-column guard + # (well below any meaningful data scale) so ``1/scale`` stays + # finite for near-zero columns. + diag = np.diagonal(XTWX_full, axis1=1, axis2=2) + scales = np.sqrt(np.maximum(np.abs(diag), 1e-30)) + + self._per_dim.append((XTWX_full, XTWy_full, scales)) + + def solve(self, active_mask=None, ridge_rel=None, ridge_floor=None): + """Solve the normal equations for the active-feature subset across + every window-batch in every spatial dimension. ``active_mask`` is a + length-``n_features_aug`` boolean array; pass ``None`` for the full + set (equivalent to the legacy ``fit_intercept=True`` path). Returns + weights of shape ``(total_windows_across_dims, active_count)``. + + Stability strategy (preserves the Gram-sub-block precompute trick): + + 1. **Column equilibration**: rescale columns by + ``1/sqrt(diag(XTWX))`` so the equilibrated Gram has unit + diagonals and a much smaller effective condition number than + the raw ``XTWX`` (which carries the squared condition number + of the underlying ``sqrt(W) X``). + 2. **Cholesky on the equilibrated SPD batch** (with batched LU + fallback if scipy's batched triangular solve isn't available + on this numpy). Cholesky has tighter backward error than LU + and is ~2x faster on SPD inputs. + 3. **One step of iterative refinement** on the original (un- + equilibrated) system, recovering 6-8 decimal digits that + normal-equation conditioning costs. + 4. **Per-batch lstsq safety net** for any window-batch where + Cholesky fails (non-SPD after equilibration -- rare). + + ``ridge_rel`` / ``ridge_floor`` are kept as no-op kwargs for + backward compatibility with callers from the previous adaptive- + ridge era; the equilibrated solve does not need a per-feature + ridge, only a tiny flat ``1e-10`` on the unit-diagonal matrix. + """ + if active_mask is None: + active_mask = np.ones(self.n_features_aug, dtype=bool) + active_size = int(active_mask.sum()) + + all_weights = [] + for XTWX_full, XTWy_full, scales_full in self._per_dim: + # Two-step boolean slice. Boolean indexing copies, so the + # result is a fresh array we can modify in place without + # corrupting the cached full Gram. + XTWX_a = XTWX_full[:, active_mask, :][:, :, active_mask] + XTWy_a = XTWy_full[:, active_mask, :] + s_a = scales_full[:, active_mask] # (batch, k) + inv_s = 1.0 / s_a # (batch, k) + + # Equilibrate: A = D^-1 XTWX D^-1, b = D^-1 XTWy. After this + # the diagonal of A is 1 by construction; the off-diagonals + # are the correlation coefficients between the underlying + # columns of sqrt(W) X. + A = XTWX_a * inv_s[:, :, None] * inv_s[:, None, :] + b = XTWy_a * inv_s[:, :, None] + + # Tiny flat ridge on the equilibrated diagonal (now ~1 by + # construction) to keep Cholesky well-defined when columns + # are exactly collinear. + idx = np.arange(active_size) + A[:, idx, idx] += 1e-10 + + batch_size = A.shape[0] + w_norm, L = _cholesky_solve_batched(A, b) + if w_norm is None: + # Cholesky failed somewhere in the batch; per-entry + # lstsq safety net on the equilibrated system. + w_norm = _per_batch_lstsq(A, b) + + # Iterative refinement on the ORIGINAL system to claw back + # digits lost to normal-equation condition squaring. + # w0 = D^-1 w_norm is the candidate solution in original + # coordinates; the residual r = XTWy - XTWX @ w0 measures + # how much it misses the original equation; the correction + # dw_norm solves the same equilibrated system on D^-1 r and + # is unscaled back to dw. + w0 = w_norm * inv_s[:, :, None] + r = XTWy_a - XTWX_a @ w0 + r_norm = r * inv_s[:, :, None] + if L is not None: + try: + z = np.linalg.solve(L, r_norm) + dw_norm = np.linalg.solve(L.transpose(0, 2, 1), z) + except np.linalg.LinAlgError: + dw_norm = _per_batch_lstsq(A, r_norm) else: - all_weights.append(w_batch) + dw_norm = _per_batch_lstsq(A, r_norm) + w = w0 + dw_norm * inv_s[:, :, None] + + all_weights.append(w.squeeze(-1)) + return np.vstack(all_weights) - return np.vstack(all_weights) + +def calculate_weights(X, y, sample_weights, grid_shape, fit_intercept=True): + """ + Vectorized calculation of weights across sliding windows. + Dynamically handles whether the intercept should be fit. + + Single-shot wrapper over :class:`GramSetup`: builds the precomputed + Gram once and immediately solves with the requested intercept policy. + Callers that solve the same Gram against many active masks (e.g. + :class:`PhysicsInformedLasso.fit`) should instantiate ``GramSetup`` + directly and call ``.solve(active_mask)`` per iteration to avoid + re-running the expensive ``X^T diag(w) X`` matmul. + """ + setup = GramSetup(X, y, sample_weights, grid_shape) + active_mask = np.ones(setup.n_features_aug, dtype=bool) + if not fit_intercept: + # GramSetup always augments with the intercept column; drop it + # from the active set to mimic the legacy ``fit_intercept=False`` + # branch (which never augmented in the first place). + active_mask[-1] = False + return setup.solve(active_mask) diff --git a/projects/pic/data/ac/ac.py b/projects/pic/data/ac/ac.py index 72e2d85c..b51f1b86 100644 --- a/projects/pic/data/ac/ac.py +++ b/projects/pic/data/ac/ac.py @@ -116,9 +116,9 @@ def AC_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): print('Shapes:', data.shape, grid[0].shape) dimensionality = 1 - epde_search_obj = EpdeSearch(use_solver=False, use_pic=True, boundary=(5, 12), + epde_search_obj = EpdeSearch(use_solver=True, use_pic=True, boundary=(5, 12), coordinate_tensors=((grid[0], grid[1])), verbose_params={'show_iter_idx': True}, - device='cpu') + device='cuda') epde_search_obj.set_preprocessor(default_preprocessor_type='FD', preprocessor_kwargs={}) diff --git a/projects/pic/data/aizawa/aizawa.npz b/projects/pic/data/aizawa/aizawa.npz new file mode 100644 index 00000000..e1c460a4 Binary files /dev/null and b/projects/pic/data/aizawa/aizawa.npz differ diff --git a/projects/pic/data/aizawa/aizawa.py b/projects/pic/data/aizawa/aizawa.py new file mode 100644 index 00000000..805afad1 --- /dev/null +++ b/projects/pic/data/aizawa/aizawa.py @@ -0,0 +1,136 @@ +import sys +import os + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname( __file__ ), '../..'))) + +import pickle +from typing import Tuple, List +import numpy as np + +from epde.interface.prepared_tokens import CustomTokens, PhasedSine1DTokens, ConstantToken, CustomEvaluator +from epde.interface.equation_translator import translate_equation +from epde.interface.interface import EpdeSearch + +from epde.operators.common.coeff_calculation import LinRegBasedCoeffsEquation +from epde.operators.common.sparsity import LASSOSparsity + +from epde.operators.utils.operator_mappers import map_operator_between_levels +import epde.operators.common.fitness as fitness +from epde.operators.utils.template import CompoundOperator + +from epde import TrigonometricTokens, GridTokens, CacheStoredTokens +import epde.globals as global_var + +import scipy.io as scio + +def load_pretrained_PINN(ann_filename): + try: + with open(ann_filename, 'rb') as data_input_file: + data_nn = pickle.load(data_input_file) + except FileNotFoundError: + print('No model located, proceeding with ann approx. retraining.') + data_nn = None + return data_nn + + +def noise_data(data, noise_level): + # add noise level to the input data + return noise_level * 0.01 * np.std(data) * np.random.normal(size=data.shape) + data + + +def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, + search_obj: EpdeSearch, all_vars: List[str] = ['u', ]) -> bool: + metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} + + correct_eq = translate_equation(correct_symbolic, search_obj.pool, all_vars=all_vars) + for var in all_vars: + correct_eq.vals[var].main_var_to_explain = var + correct_eq.vals[var].metaparameters = metaparams + print(correct_eq.text_form) + + incorrect_eq = translate_equation(eq_incorrect_symbolic, search_obj.pool, + all_vars=all_vars) # , all_vars = ['u', 'v']) + for var in all_vars: + incorrect_eq.vals[var].main_var_to_explain = var + incorrect_eq.vals[var].metaparameters = metaparams + print(incorrect_eq.text_form) + + fit_operator.apply(correct_eq, {}) + fit_operator.apply(incorrect_eq, {}) + print([[correct_eq.vals[var].fitness_value, incorrect_eq.vals[var].fitness_value] for var in all_vars]) + print([[correct_eq.vals[var].coefficients_stability, incorrect_eq.vals[var].coefficients_stability] for var in + all_vars]) + print([[correct_eq.vals[var].aic, incorrect_eq.vals[var].aic] for var in all_vars]) + + # print([correct_eq.vals[var].coefficients_stability < incorrect_eq.vals[var].coefficients_stability for var in all_vars]) + return all([correct_eq.vals[var].coefficients_stability < incorrect_eq.vals[var].coefficients_stability for var in + all_vars]) + + +def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: + sparsity = LASSOSparsity() + coeff_calc = LinRegBasedCoeffsEquation() + + # sparsity = map_operator_between_levels(sparsity, 'gene level', 'chromosome level') + # coeff_calc = map_operator_between_levels(coeff_calc, 'gene level', 'chromosome level') + + fitness_operator.set_suboperators({'sparsity': sparsity, + 'coeff_calc': coeff_calc}) + fitness_cond = lambda x: not getattr(x, 'fitness_calculated') + fitness_operator.params = operator_params + fitness_operator = map_operator_between_levels(fitness_operator, 'gene level', 'chromosome level', + objective_condition=fitness_cond) + return fitness_operator + + +def aizawa_discovery(noise_level): + data_file = os.path.join(os.path.dirname(__file__), 'aizawa.npz') + data = np.load(data_file) + t = data['t'] + u = data['u'] + + x = u[..., 0] + y = u[..., 1] + z = u[..., 2] + dimensionality = x.ndim - 1 + + trig_tokens = TrigonometricTokens(freq=(2 - 1e-8, 2 + 1e-8), + dimensionality=dimensionality) + grid_tokens = GridTokens(['x_0', ], dimensionality=dimensionality, max_power=2) + + epde_search_obj = EpdeSearch(use_solver=False, multiobjective_mode=True, use_pic=True, boundary=15, + coordinate_tensors=(t,), verbose_params={'show_iter_idx': True}, + device='cuda') + + epde_search_obj.set_preprocessor(default_preprocessor_type='FD', + preprocessor_kwargs={}) + + popsize = 16 + epde_search_obj.set_moeadd_params(population_size=popsize, training_epochs=50) + + factors_max_number = {'factors_num': [1, 2], 'probas' : [0.8, 0.2]} + + epde_search_obj.fit(data=[x, y, z], variable_names=['x', 'y', 'z'], max_deriv_order=(1,), + equation_terms_max_number=7, data_fun_pow=3, additional_tokens=[], + equation_factors_max_number=factors_max_number, + eq_sparsity_interval=(1e-8, 1e-0)) # + + epde_search_obj.equations(only_print=True, num=1) + epde_search_obj.visualize_solutions() + + return epde_search_obj + + +if __name__ == "__main__": + import torch + from epde.operators.utils.default_parameter_loader import EvolutionaryParams + print(torch.cuda.is_available()) + # Operator = fitness.SolverBasedFitness # Replace by the developed PIC-based operator. + # Operator = fitness.PIC + Operator = fitness.L2LRFitness + params = EvolutionaryParams() + operator_params = params.get_default_params_for_operator('DiscrepancyBasedFitnessWithCV') #{"penalty_coeff": 0.2, "pinn_loss_mult": 1e4} + print('operator_params ', operator_params) + fit_operator = prepare_suboperators(Operator(list(operator_params.keys())), operator_params) + + aizawa_discovery(0) diff --git a/projects/pic/data/apoptosis/apoptosis.npz b/projects/pic/data/apoptosis/apoptosis.npz new file mode 100644 index 00000000..f0214dd3 Binary files /dev/null and b/projects/pic/data/apoptosis/apoptosis.npz differ diff --git a/projects/pic/data/apoptosis/apoptosis.py b/projects/pic/data/apoptosis/apoptosis.py new file mode 100644 index 00000000..dee9709b --- /dev/null +++ b/projects/pic/data/apoptosis/apoptosis.py @@ -0,0 +1,136 @@ +import sys +import os + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname( __file__ ), '../..'))) + +import pickle +from typing import Tuple, List +import numpy as np + +from epde.interface.prepared_tokens import CustomTokens, PhasedSine1DTokens, ConstantToken, CustomEvaluator +from epde.interface.equation_translator import translate_equation +from epde.interface.interface import EpdeSearch + +from epde.operators.common.coeff_calculation import LinRegBasedCoeffsEquation +from epde.operators.common.sparsity import LASSOSparsity + +from epde.operators.utils.operator_mappers import map_operator_between_levels +import epde.operators.common.fitness as fitness +from epde.operators.utils.template import CompoundOperator + +from epde import TrigonometricTokens, GridTokens, CacheStoredTokens +import epde.globals as global_var + +import scipy.io as scio + +def load_pretrained_PINN(ann_filename): + try: + with open(ann_filename, 'rb') as data_input_file: + data_nn = pickle.load(data_input_file) + except FileNotFoundError: + print('No model located, proceeding with ann approx. retraining.') + data_nn = None + return data_nn + + +def noise_data(data, noise_level): + # add noise level to the input data + return noise_level * 0.01 * np.std(data) * np.random.normal(size=data.shape) + data + + +def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, + search_obj: EpdeSearch, all_vars: List[str] = ['u', ]) -> bool: + metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} + + correct_eq = translate_equation(correct_symbolic, search_obj.pool, all_vars=all_vars) + for var in all_vars: + correct_eq.vals[var].main_var_to_explain = var + correct_eq.vals[var].metaparameters = metaparams + print(correct_eq.text_form) + + incorrect_eq = translate_equation(eq_incorrect_symbolic, search_obj.pool, + all_vars=all_vars) # , all_vars = ['u', 'v']) + for var in all_vars: + incorrect_eq.vals[var].main_var_to_explain = var + incorrect_eq.vals[var].metaparameters = metaparams + print(incorrect_eq.text_form) + + fit_operator.apply(correct_eq, {}) + fit_operator.apply(incorrect_eq, {}) + print([[correct_eq.vals[var].fitness_value, incorrect_eq.vals[var].fitness_value] for var in all_vars]) + print([[correct_eq.vals[var].coefficients_stability, incorrect_eq.vals[var].coefficients_stability] for var in + all_vars]) + print([[correct_eq.vals[var].aic, incorrect_eq.vals[var].aic] for var in all_vars]) + + # print([correct_eq.vals[var].coefficients_stability < incorrect_eq.vals[var].coefficients_stability for var in all_vars]) + return all([correct_eq.vals[var].coefficients_stability < incorrect_eq.vals[var].coefficients_stability for var in + all_vars]) + + +def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: + sparsity = LASSOSparsity() + coeff_calc = LinRegBasedCoeffsEquation() + + # sparsity = map_operator_between_levels(sparsity, 'gene level', 'chromosome level') + # coeff_calc = map_operator_between_levels(coeff_calc, 'gene level', 'chromosome level') + + fitness_operator.set_suboperators({'sparsity': sparsity, + 'coeff_calc': coeff_calc}) + fitness_cond = lambda x: not getattr(x, 'fitness_calculated') + fitness_operator.params = operator_params + fitness_operator = map_operator_between_levels(fitness_operator, 'gene level', 'chromosome level', + objective_condition=fitness_cond) + return fitness_operator + + +def apoptosis_discovery(noise_level): + data_file = os.path.join(os.path.dirname(__file__), 'apoptosis.npz') + data = np.load(data_file) + t = data['t'] + u = data['u'] + + x = u[..., 0] + y = u[..., 1] + z = u[..., 2] + dimensionality = x.ndim - 1 + + trig_tokens = TrigonometricTokens(freq=(2 - 1e-8, 2 + 1e-8), + dimensionality=dimensionality) + grid_tokens = GridTokens(['x_0', ], dimensionality=dimensionality, max_power=2) + + epde_search_obj = EpdeSearch(use_solver=False, multiobjective_mode=True, use_pic=True, boundary=15, + coordinate_tensors=(t,), verbose_params={'show_iter_idx': True}, + device='cuda') + + epde_search_obj.set_preprocessor(default_preprocessor_type='FD', + preprocessor_kwargs={}) + + popsize = 16 + epde_search_obj.set_moeadd_params(population_size=popsize, training_epochs=50) + + factors_max_number = {'factors_num': [1, 2], 'probas' : [0.8, 0.2]} + + epde_search_obj.fit(data=[x, y, z], variable_names=['x', 'y', 'z'], max_deriv_order=(1,), + equation_terms_max_number=7, data_fun_pow=1, additional_tokens=[], + equation_factors_max_number=factors_max_number, + eq_sparsity_interval=(1e-8, 1e-0)) # + + epde_search_obj.equations(only_print=True, num=1) + epde_search_obj.visualize_solutions() + + return epde_search_obj + + +if __name__ == "__main__": + import torch + from epde.operators.utils.default_parameter_loader import EvolutionaryParams + print(torch.cuda.is_available()) + # Operator = fitness.SolverBasedFitness # Replace by the developed PIC-based operator. + # Operator = fitness.PIC + Operator = fitness.L2LRFitness + params = EvolutionaryParams() + operator_params = params.get_default_params_for_operator('DiscrepancyBasedFitnessWithCV') #{"penalty_coeff": 0.2, "pinn_loss_mult": 1e4} + print('operator_params ', operator_params) + fit_operator = prepare_suboperators(Operator(list(operator_params.keys())), operator_params) + + apoptosis_discovery(0) diff --git a/projects/pic/data/autocatalysis/autocatalysis.npz b/projects/pic/data/autocatalysis/autocatalysis.npz new file mode 100644 index 00000000..bdcefb74 Binary files /dev/null and b/projects/pic/data/autocatalysis/autocatalysis.npz differ diff --git a/projects/pic/data/autocatalysis/autocatalysis.py b/projects/pic/data/autocatalysis/autocatalysis.py new file mode 100644 index 00000000..bd6f8397 --- /dev/null +++ b/projects/pic/data/autocatalysis/autocatalysis.py @@ -0,0 +1,170 @@ +import sys +import os + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname( __file__ ), '../..'))) + +import pickle +from typing import Tuple, List +import numpy as np + +from epde.interface.prepared_tokens import CustomTokens, PhasedSine1DTokens, ConstantToken, CustomEvaluator +from epde.interface.equation_translator import translate_equation +from epde.interface.interface import EpdeSearch + +from epde.operators.common.coeff_calculation import LinRegBasedCoeffsEquation +from epde.operators.common.sparsity import LASSOSparsity + +from epde.operators.utils.operator_mappers import map_operator_between_levels +import epde.operators.common.fitness as fitness +from epde.operators.utils.template import CompoundOperator + +from epde import TrigonometricTokens, GridTokens, CacheStoredTokens +import epde.globals as global_var + +import scipy.io as scio + +def load_pretrained_PINN(ann_filename): + try: + with open(ann_filename, 'rb') as data_input_file: + data_nn = pickle.load(data_input_file) + except FileNotFoundError: + print('No model located, proceeding with ann approx. retraining.') + data_nn = None + return data_nn + + +def noise_data(data, noise_level): + # add noise level to the input data + return noise_level * 0.01 * np.std(data) * np.random.normal(size=data.shape) + data + + +def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, + search_obj: EpdeSearch, all_vars: List[str] = ['u', ]) -> bool: + metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} + + correct_eq = translate_equation(correct_symbolic, search_obj.pool, all_vars=all_vars) + for var in all_vars: + correct_eq.vals[var].main_var_to_explain = var + correct_eq.vals[var].metaparameters = metaparams + print(correct_eq.text_form) + + incorrect_eq = translate_equation(eq_incorrect_symbolic, search_obj.pool, + all_vars=all_vars) # , all_vars = ['u', 'v']) + for var in all_vars: + incorrect_eq.vals[var].main_var_to_explain = var + incorrect_eq.vals[var].metaparameters = metaparams + print(incorrect_eq.text_form) + + fit_operator.apply(correct_eq, {}) + fit_operator.apply(incorrect_eq, {}) + print([[correct_eq.vals[var].fitness_value, incorrect_eq.vals[var].fitness_value] for var in all_vars]) + print([[correct_eq.vals[var].coefficients_stability, incorrect_eq.vals[var].coefficients_stability] for var in + all_vars]) + print([[correct_eq.vals[var].aic, incorrect_eq.vals[var].aic] for var in all_vars]) + + # print([correct_eq.vals[var].coefficients_stability < incorrect_eq.vals[var].coefficients_stability for var in all_vars]) + return all([correct_eq.vals[var].coefficients_stability < incorrect_eq.vals[var].coefficients_stability for var in + all_vars]) + + +def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: + sparsity = LASSOSparsity() + coeff_calc = LinRegBasedCoeffsEquation() + + # sparsity = map_operator_between_levels(sparsity, 'gene level', 'chromosome level') + # coeff_calc = map_operator_between_levels(coeff_calc, 'gene level', 'chromosome level') + + fitness_operator.set_suboperators({'sparsity': sparsity, + 'coeff_calc': coeff_calc}) + fitness_cond = lambda x: not getattr(x, 'fitness_calculated') + fitness_operator.params = operator_params + fitness_operator = map_operator_between_levels(fitness_operator, 'gene level', 'chromosome level', + objective_condition=fitness_cond) + return fitness_operator + + +def autocatalysis_gs_discovery(noise_level): + data_file = os.path.join(os.path.dirname(__file__), 'autocatalytic-gene-switching.npz') + data = np.load(data_file) + t = data['t'] + u = data['u'] + + u = u[..., 0] + dimensionality = u.ndim - 1 + + trig_tokens = TrigonometricTokens(freq=(2 - 1e-8, 2 + 1e-8), + dimensionality=dimensionality) + grid_tokens = GridTokens(['x_0', ], dimensionality=dimensionality, max_power=2) + + epde_search_obj = EpdeSearch(use_solver=False, multiobjective_mode=True, use_pic=True, boundary=15, + coordinate_tensors=(t,), verbose_params={'show_iter_idx': True}, + device='cuda') + + epde_search_obj.set_preprocessor(default_preprocessor_type='FD', + preprocessor_kwargs={}) + + popsize = 16 + epde_search_obj.set_moeadd_params(population_size=popsize, training_epochs=2) + + factors_max_number = {'factors_num': [1, 2], 'probas' : [0.8, 0.2]} + + epde_search_obj.fit(data=[u], variable_names=['u'], max_deriv_order=(3,), + equation_terms_max_number=7, data_fun_pow=3, additional_tokens=[], + equation_factors_max_number=factors_max_number, + eq_sparsity_interval=(1e-8, 1e-0)) # + + epde_search_obj.equations(only_print=True, num=1) + epde_search_obj.visualize_solutions() + + return epde_search_obj + +def autocatalysis_discovery(noise_level): + data_file = os.path.join(os.path.dirname(__file__), 'autocatalysis.npz') + data = np.load(data_file) + t = data['t'] + u = data['u'] + + u = u[..., 0] + dimensionality = u.ndim - 1 + + trig_tokens = TrigonometricTokens(freq=(2 - 1e-8, 2 + 1e-8), + dimensionality=dimensionality) + grid_tokens = GridTokens(['x_0', ], dimensionality=dimensionality, max_power=2) + + epde_search_obj = EpdeSearch(use_solver=False, multiobjective_mode=True, use_pic=True, boundary=15, + coordinate_tensors=(t,), verbose_params={'show_iter_idx': True}, + device='cuda') + + epde_search_obj.set_preprocessor(default_preprocessor_type='FD', + preprocessor_kwargs={}) + + popsize = 16 + epde_search_obj.set_moeadd_params(population_size=popsize, training_epochs=2) + + factors_max_number = {'factors_num': [1, 2], 'probas' : [0.8, 0.2]} + + epde_search_obj.fit(data=[u], variable_names=['u'], max_deriv_order=(3,), + equation_terms_max_number=7, data_fun_pow=3, additional_tokens=[], + equation_factors_max_number=factors_max_number, + eq_sparsity_interval=(1e-8, 1e-0)) # + + epde_search_obj.equations(only_print=True, num=1) + epde_search_obj.visualize_solutions() + + return epde_search_obj + + +if __name__ == "__main__": + import torch + from epde.operators.utils.default_parameter_loader import EvolutionaryParams + print(torch.cuda.is_available()) + # Operator = fitness.SolverBasedFitness # Replace by the developed PIC-based operator. + # Operator = fitness.PIC + Operator = fitness.L2LRFitness + params = EvolutionaryParams() + operator_params = params.get_default_params_for_operator('DiscrepancyBasedFitnessWithCV') #{"penalty_coeff": 0.2, "pinn_loss_mult": 1e4} + print('operator_params ', operator_params) + fit_operator = prepare_suboperators(Operator(list(operator_params.keys())), operator_params) + + # autocatalysis_discovery(0) + autocatalysis_gs_discovery(0) diff --git a/projects/pic/data/autocatalysis/autocatalytic-gene-switching.npz b/projects/pic/data/autocatalysis/autocatalytic-gene-switching.npz new file mode 100644 index 00000000..1b74e9d5 Binary files /dev/null and b/projects/pic/data/autocatalysis/autocatalytic-gene-switching.npz differ diff --git a/projects/pic/data/bacterial-respiration/bacterial-respiration.npz b/projects/pic/data/bacterial-respiration/bacterial-respiration.npz new file mode 100644 index 00000000..1ced7d3e Binary files /dev/null and b/projects/pic/data/bacterial-respiration/bacterial-respiration.npz differ diff --git a/projects/pic/data/bacterial-respiration/bacterial-respiration.py b/projects/pic/data/bacterial-respiration/bacterial-respiration.py new file mode 100644 index 00000000..65ed66ee --- /dev/null +++ b/projects/pic/data/bacterial-respiration/bacterial-respiration.py @@ -0,0 +1,135 @@ +import sys +import os + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname( __file__ ), '../..'))) + +import pickle +from typing import Tuple, List +import numpy as np + +from epde.interface.prepared_tokens import CustomTokens, PhasedSine1DTokens, ConstantToken, CustomEvaluator +from epde.interface.equation_translator import translate_equation +from epde.interface.interface import EpdeSearch + +from epde.operators.common.coeff_calculation import LinRegBasedCoeffsEquation +from epde.operators.common.sparsity import LASSOSparsity + +from epde.operators.utils.operator_mappers import map_operator_between_levels +import epde.operators.common.fitness as fitness +from epde.operators.utils.template import CompoundOperator + +from epde import TrigonometricTokens, GridTokens, CacheStoredTokens +import epde.globals as global_var + +import scipy.io as scio + +def load_pretrained_PINN(ann_filename): + try: + with open(ann_filename, 'rb') as data_input_file: + data_nn = pickle.load(data_input_file) + except FileNotFoundError: + print('No model located, proceeding with ann approx. retraining.') + data_nn = None + return data_nn + + +def noise_data(data, noise_level): + # add noise level to the input data + return noise_level * 0.01 * np.std(data) * np.random.normal(size=data.shape) + data + + +def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, + search_obj: EpdeSearch, all_vars: List[str] = ['u', ]) -> bool: + metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} + + correct_eq = translate_equation(correct_symbolic, search_obj.pool, all_vars=all_vars) + for var in all_vars: + correct_eq.vals[var].main_var_to_explain = var + correct_eq.vals[var].metaparameters = metaparams + print(correct_eq.text_form) + + incorrect_eq = translate_equation(eq_incorrect_symbolic, search_obj.pool, + all_vars=all_vars) # , all_vars = ['u', 'v']) + for var in all_vars: + incorrect_eq.vals[var].main_var_to_explain = var + incorrect_eq.vals[var].metaparameters = metaparams + print(incorrect_eq.text_form) + + fit_operator.apply(correct_eq, {}) + fit_operator.apply(incorrect_eq, {}) + print([[correct_eq.vals[var].fitness_value, incorrect_eq.vals[var].fitness_value] for var in all_vars]) + print([[correct_eq.vals[var].coefficients_stability, incorrect_eq.vals[var].coefficients_stability] for var in + all_vars]) + print([[correct_eq.vals[var].aic, incorrect_eq.vals[var].aic] for var in all_vars]) + + # print([correct_eq.vals[var].coefficients_stability < incorrect_eq.vals[var].coefficients_stability for var in all_vars]) + return all([correct_eq.vals[var].coefficients_stability < incorrect_eq.vals[var].coefficients_stability for var in + all_vars]) + + +def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: + sparsity = LASSOSparsity() + coeff_calc = LinRegBasedCoeffsEquation() + + # sparsity = map_operator_between_levels(sparsity, 'gene level', 'chromosome level') + # coeff_calc = map_operator_between_levels(coeff_calc, 'gene level', 'chromosome level') + + fitness_operator.set_suboperators({'sparsity': sparsity, + 'coeff_calc': coeff_calc}) + fitness_cond = lambda x: not getattr(x, 'fitness_calculated') + fitness_operator.params = operator_params + fitness_operator = map_operator_between_levels(fitness_operator, 'gene level', 'chromosome level', + objective_condition=fitness_cond) + return fitness_operator + + +def bacterial_respiration_discovery(noise_level): + data_file = os.path.join(os.path.dirname(__file__), 'bacterial-respiration.npz') + data = np.load(data_file) + t = data['t'] + u = data['u'] + + x = u[..., 0] + y = u[..., 1] + dimensionality = x.ndim - 1 + + trig_tokens = TrigonometricTokens(freq=(2 - 1e-8, 2 + 1e-8), + dimensionality=dimensionality) + grid_tokens = GridTokens(['x_0', ], dimensionality=dimensionality, max_power=2) + + epde_search_obj = EpdeSearch(use_solver=False, multiobjective_mode=True, use_pic=True, boundary=15, + coordinate_tensors=(t,), verbose_params={'show_iter_idx': True}, + device='cuda') + + epde_search_obj.set_preprocessor(default_preprocessor_type='FD', + preprocessor_kwargs={}) + + popsize = 16 + epde_search_obj.set_moeadd_params(population_size=popsize, training_epochs=50) + + factors_max_number = {'factors_num': [1, 2], 'probas' : [0.8, 0.2]} + + epde_search_obj.fit(data=[x, y], variable_names=['x', 'y'], max_deriv_order=(1,), + equation_terms_max_number=7, data_fun_pow=1, additional_tokens=[], + equation_factors_max_number=factors_max_number, + eq_sparsity_interval=(1e-8, 1e-0)) # + + epde_search_obj.equations(only_print=True, num=1) + epde_search_obj.visualize_solutions() + + return epde_search_obj + + +if __name__ == "__main__": + import torch + from epde.operators.utils.default_parameter_loader import EvolutionaryParams + print(torch.cuda.is_available()) + # Operator = fitness.SolverBasedFitness # Replace by the developed PIC-based operator. + # Operator = fitness.PIC + Operator = fitness.L2LRFitness + params = EvolutionaryParams() + operator_params = params.get_default_params_for_operator('DiscrepancyBasedFitnessWithCV') #{"penalty_coeff": 0.2, "pinn_loss_mult": 1e4} + print('operator_params ', operator_params) + fit_operator = prepare_suboperators(Operator(list(operator_params.keys())), operator_params) + + bacterial_respiration_discovery(0) diff --git a/projects/pic/data/burgers/burgers_sln_100.csv b/projects/pic/data/burgers/burgers_sln_100.csv new file mode 100644 index 00000000..c5f6b9da --- /dev/null +++ b/projects/pic/data/burgers/burgers_sln_100.csv @@ -0,0 +1,101 @@ +500.,502.5125628140703,505.0505050505051,507.6142131979695,510.2040816326531,512.8205128205129,515.4639175257732,518.1347150259068,520.8333333333334,523.5602094240837,526.3157894736842,529.1005291005292,531.9148936170212,534.7593582887699,537.6344086021506,540.5405405405404,543.4782608695651,546.4480874316939,549.4505494505494,552.4861878453039,555.5555555555555,558.659217877095,561.7977528089888,564.9717514124294,568.1818181818182,571.4285714285714,574.7126436781609,578.0346820809249,581.3953488372093,584.7953216374269,588.2352941176471,591.7159763313609,595.2380952380952,598.8023952095809,602.4096385542169,606.0606060606061,609.7560975609756,613.4969325153374,617.283950617284,621.1180124223603,625.,628.930817610063,632.9113924050632,636.9426751592356,641.025641025641,645.1612903225806,649.3506493506493,653.5947712418301,657.8947368421053,662.2516556291391,666.6666666666666,671.1409395973154,675.6756756756756,680.2721088435374,684.931506849315,689.6551724137931,694.4444444444445,699.3006993006994,704.2253521126761,709.2198581560283,714.2857142857143,719.4244604316547,724.6376811594204,729.92700729927,735.2941176470589,740.7407407407406,746.2686567164179,751.8796992481202,757.5757575757577,763.3587786259542,769.2307692307694,775.1937984496124,781.25,787.4015748031495,793.6507936507936,800.,806.4516129032259,813.0081300813008,819.672131147541,826.4462809917355,833.3333333333334,840.3361344537815,847.4576271186442,854.7008547008547,862.0689655172413,869.5652173913045,877.1929824561403,884.9557522123894,892.8571428571428,900.900900900901,909.090909090909,917.4311926605507,925.9259259259259,934.5794392523366,943.3962264150942,952.3809523809525,961.5384615384614,970.873786407767,980.3921568627451,990.09900990099,1000. +495.,497.4874371859296,500.00000000000006,502.5380710659898,505.10204081632656,507.69230769230774,510.3092783505155,512.9533678756477,515.625,518.3246073298429,521.0526315789473,523.8095238095239,526.595744680851,529.4117647058823,532.2580645161291,535.1351351351351,538.0434782608695,540.983606557377,543.9560439560439,546.9613259668508,550.,553.072625698324,556.1797752808989,559.3220338983051,562.5,565.7142857142857,568.9655172413793,572.2543352601157,575.5813953488372,578.9473684210526,582.3529411764706,585.7988165680474,589.2857142857143,592.814371257485,596.3855421686748,600.,603.6585365853658,607.361963190184,611.1111111111111,614.9068322981367,618.75,622.6415094339624,626.5822784810126,630.5732484076433,634.6153846153845,638.7096774193549,642.8571428571429,647.0588235294118,651.3157894736843,655.6291390728477,660.,664.4295302013423,668.9189189189188,673.469387755102,678.0821917808219,682.7586206896552,687.5,692.3076923076924,697.1830985915493,702.127659574468,707.1428571428571,712.230215827338,717.3913043478262,722.6277372262774,727.9411764705883,733.3333333333333,738.8059701492538,744.3609022556391,750.0000000000001,755.7251908396946,761.5384615384617,767.4418604651162,773.4375,779.527559055118,785.7142857142857,792.,798.3870967741937,804.8780487804878,811.4754098360656,818.1818181818182,825.,831.9327731092437,838.9830508474577,846.1538461538462,853.448275862069,860.8695652173914,868.4210526315788,876.1061946902655,883.9285714285713,891.891891891892,900.,908.2568807339451,916.6666666666665,925.2336448598132,933.9622641509433,942.857142857143,951.9230769230768,961.1650485436893,970.5882352941176,980.1980198019802,990. +490.,492.4623115577889,494.949494949495,497.46192893401013,500.,502.56410256410265,505.1546391752578,507.77202072538864,510.4166666666667,513.0890052356021,515.7894736842105,518.5185185185186,521.2765957446809,524.0641711229946,526.8817204301076,529.7297297297297,532.6086956521739,535.51912568306,538.4615384615385,541.4364640883978,544.4444444444445,547.4860335195531,550.5617977528091,553.6723163841808,556.8181818181819,560.,563.2183908045977,566.4739884393064,569.7674418604652,573.0994152046783,576.4705882352941,579.8816568047338,583.3333333333334,586.8263473053893,590.3614457831326,593.939393939394,597.560975609756,601.2269938650306,604.9382716049382,608.6956521739131,612.5,616.3522012578617,620.2531645569619,624.2038216560509,628.2051282051282,632.258064516129,636.3636363636364,640.5228758169935,644.7368421052632,649.0066225165564,653.3333333333333,657.7181208053692,662.1621621621622,666.6666666666666,671.2328767123287,675.8620689655173,680.5555555555555,685.3146853146854,690.1408450704226,695.0354609929077,700.,705.0359712230215,710.144927536232,715.3284671532847,720.5882352941177,725.9259259259259,731.3432835820896,736.8421052631578,742.4242424242425,748.091603053435,753.846153846154,759.6899224806201,765.625,771.6535433070866,777.7777777777777,784.,790.3225806451613,796.7479674796748,803.2786885245902,809.9173553719008,816.6666666666667,823.5294117647059,830.5084745762713,837.6068376068376,844.8275862068965,852.1739130434784,859.6491228070175,867.2566371681417,874.9999999999999,882.882882882883,890.9090909090909,899.0825688073396,907.4074074074073,915.8878504672898,924.5283018867924,933.3333333333335,942.3076923076923,951.4563106796116,960.7843137254902,970.2970297029702,980. +485.,487.4371859296482,489.89898989898995,492.38578680203045,494.8979591836735,497.4358974358975,500.00000000000006,502.5906735751296,505.20833333333337,507.85340314136124,510.52631578947364,513.2275132275133,515.9574468085107,518.7165775401069,521.5053763440861,524.3243243243243,527.1739130434783,530.0546448087431,532.9670329670329,535.9116022099448,538.8888888888889,541.8994413407821,544.9438202247192,548.0225988700565,551.1363636363637,554.2857142857142,557.471264367816,560.6936416184972,563.953488372093,567.2514619883041,570.5882352941177,573.9644970414201,577.3809523809524,580.8383233532935,584.3373493975904,587.8787878787879,591.4634146341464,595.0920245398773,598.7654320987654,602.4844720496895,606.25,610.0628930817611,613.9240506329113,617.8343949044586,621.7948717948717,625.8064516129032,629.8701298701299,633.9869281045752,638.1578947368422,642.384105960265,646.6666666666666,651.006711409396,655.4054054054054,659.8639455782313,664.3835616438356,668.9655172413793,673.6111111111111,678.3216783216784,683.0985915492959,687.9432624113474,692.8571428571429,697.841726618705,702.8985507246377,708.029197080292,713.2352941176471,718.5185185185185,723.8805970149255,729.3233082706766,734.848484848485,740.4580152671755,746.1538461538463,751.9379844961239,757.8125,763.779527559055,769.8412698412698,776.,782.2580645161291,788.6178861788618,795.0819672131148,801.6528925619835,808.3333333333334,815.1260504201681,822.0338983050848,829.0598290598291,836.2068965517241,843.4782608695652,850.8771929824561,858.4070796460178,866.0714285714286,873.873873873874,881.8181818181818,889.9082568807341,898.148148148148,906.5420560747665,915.0943396226414,923.809523809524,932.6923076923076,941.747572815534,950.9803921568626,960.3960396039604,970. +480.,482.4120603015075,484.8484848484849,487.3096446700507,489.7959183673469,492.3076923076924,494.84536082474233,497.4093264248705,500.00000000000006,502.6178010471204,505.2631578947368,507.936507936508,510.63829787234044,513.3689839572191,516.1290322580646,518.9189189189188,521.7391304347826,524.5901639344262,527.4725274725274,530.3867403314918,533.3333333333334,536.3128491620112,539.3258426966293,542.3728813559322,545.4545454545455,548.5714285714286,551.7241379310344,554.9132947976879,558.139534883721,561.4035087719298,564.7058823529412,568.0473372781065,571.4285714285714,574.8502994011976,578.3132530120482,581.8181818181819,585.3658536585366,588.9570552147239,592.5925925925926,596.2732919254659,600.,603.7735849056604,607.5949367088607,611.4649681528662,615.3846153846154,619.3548387096774,623.3766233766233,627.4509803921569,631.578947368421,635.7615894039735,640.,644.2953020134229,648.6486486486486,653.0612244897959,657.5342465753424,662.0689655172414,666.6666666666666,671.3286713286714,676.0563380281691,680.8510638297871,685.7142857142858,690.6474820143885,695.6521739130436,700.7299270072992,705.8823529411766,711.1111111111111,716.4179104477612,721.8045112781955,727.2727272727274,732.824427480916,738.4615384615386,744.1860465116279,750.,755.9055118110235,761.9047619047618,768.,774.1935483870968,780.4878048780488,786.8852459016393,793.3884297520661,800.,806.7226890756302,813.5593220338984,820.5128205128206,827.5862068965516,834.7826086956522,842.1052631578947,849.5575221238938,857.1428571428571,864.864864864865,872.7272727272727,880.7339449541286,888.8888888888888,897.1962616822431,905.6603773584905,914.2857142857144,923.076923076923,932.0388349514564,941.1764705882352,950.4950495049504,960. +475.,477.3869346733668,479.79797979797985,482.23350253807104,484.6938775510204,487.17948717948724,489.6907216494846,492.22797927461147,494.7916666666667,497.3821989528796,500.,502.64550264550275,505.3191489361702,508.0213903743315,510.75268817204307,513.5135135135134,516.3043478260869,519.1256830601093,521.978021978022,524.8618784530387,527.7777777777778,530.7262569832402,533.7078651685393,536.723163841808,539.7727272727274,542.8571428571428,545.9770114942528,549.1329479768787,552.3255813953489,555.5555555555555,558.8235294117648,562.1301775147929,565.4761904761905,568.8622754491018,572.289156626506,575.7575757575758,579.2682926829268,582.8220858895705,586.4197530864197,590.0621118012423,593.75,597.4842767295598,601.2658227848101,605.0955414012739,608.9743589743589,612.9032258064516,616.8831168831168,620.9150326797386,625.,629.1390728476822,633.3333333333333,637.5838926174497,641.8918918918919,646.2585034013605,650.6849315068492,655.1724137931035,659.7222222222222,664.3356643356644,669.0140845070423,673.7588652482268,678.5714285714286,683.4532374100719,688.4057971014494,693.4306569343065,698.529411764706,703.7037037037037,708.955223880597,714.2857142857142,719.6969696969697,725.1908396946565,730.769230769231,736.4341085271317,742.1875,748.0314960629921,753.968253968254,760.,766.1290322580646,772.3577235772358,778.688524590164,785.1239669421487,791.6666666666667,798.3193277310925,805.0847457627119,811.965811965812,818.9655172413793,826.0869565217392,833.3333333333333,840.70796460177,848.2142857142857,855.855855855856,863.6363636363636,871.5596330275231,879.6296296296296,887.8504672897197,896.2264150943395,904.7619047619049,913.4615384615383,922.3300970873787,931.3725490196078,940.5940594059406,950. +470.,472.3618090452261,474.74747474747477,477.15736040609136,479.5918367346939,482.0512820512821,484.5360824742269,487.0466321243524,489.58333333333337,492.14659685863876,494.7368421052631,497.3544973544974,500.,502.67379679144375,505.37634408602156,508.108108108108,510.86956521739125,513.6612021857923,516.4835164835164,519.3370165745856,522.2222222222223,525.1396648044692,528.0898876404494,531.0734463276837,534.0909090909091,537.1428571428571,540.2298850574713,543.3526011560695,546.5116279069767,549.7076023391812,552.9411764705883,556.2130177514794,559.5238095238095,562.8742514970061,566.2650602409639,569.6969696969697,573.170731707317,576.6871165644172,580.2469135802469,583.8509316770187,587.5,591.1949685534591,594.9367088607594,598.7261146496814,602.5641025641025,606.4516129032257,610.3896103896104,614.3790849673203,618.421052631579,622.5165562913908,626.6666666666666,630.8724832214765,635.1351351351351,639.4557823129252,643.8356164383562,648.2758620689656,652.7777777777777,657.3426573426574,661.9718309859155,666.6666666666666,671.4285714285714,676.2589928057554,681.1594202898551,686.1313868613139,691.1764705882354,696.2962962962963,701.4925373134329,706.7669172932331,712.1212121212122,717.5572519083969,723.0769230769232,728.6821705426356,734.375,740.1574803149606,746.031746031746,752.,758.0645161290323,764.2276422764228,770.4918032786885,776.8595041322315,783.3333333333334,789.9159663865546,796.6101694915255,803.4188034188035,810.3448275862069,817.3913043478261,824.5614035087718,831.8584070796461,839.2857142857142,846.8468468468469,854.5454545454545,862.3853211009176,870.3703703703703,878.5046728971963,886.7924528301886,895.2380952380954,903.8461538461538,912.6213592233009,921.5686274509803,930.6930693069307,940. +465.,467.3366834170854,469.69696969696975,472.0812182741116,474.48979591836735,476.92307692307696,479.38144329896915,481.8652849740933,484.37500000000006,486.9109947643979,489.4736842105263,492.06349206349216,494.6808510638298,497.3262032085561,500.00000000000006,502.70270270270265,505.4347826086956,508.19672131147536,510.9890109890109,513.8121546961327,516.6666666666667,519.5530726256983,522.4719101123596,525.4237288135594,528.409090909091,531.4285714285714,534.4827586206897,537.5722543352601,540.6976744186047,543.859649122807,547.0588235294118,550.2958579881657,553.5714285714286,556.8862275449102,560.2409638554217,563.6363636363636,567.0731707317073,570.5521472392638,574.074074074074,577.6397515527951,581.25,584.9056603773586,588.6075949367088,592.3566878980891,596.1538461538461,600.,603.8961038961039,607.843137254902,611.8421052631579,615.8940397350993,620.,624.1610738255034,628.3783783783783,632.6530612244898,636.986301369863,641.3793103448277,645.8333333333333,650.3496503496505,654.9295774647887,659.5744680851063,664.2857142857143,669.0647482014389,673.913043478261,678.8321167883212,683.8235294117648,688.8888888888888,694.0298507462687,699.2481203007518,704.5454545454546,709.9236641221373,715.3846153846155,720.9302325581394,726.5625,732.2834645669291,738.0952380952381,744.,750.,756.0975609756098,762.2950819672132,768.595041322314,775.,781.5126050420168,788.135593220339,794.8717948717949,801.7241379310344,808.6956521739131,815.7894736842105,823.0088495575221,830.3571428571428,837.8378378378379,845.4545454545454,853.211009174312,861.111111111111,869.158878504673,877.3584905660376,885.7142857142859,894.2307692307692,902.9126213592233,911.7647058823529,920.7920792079208,930. +460.,462.3115577889447,464.64646464646466,467.00507614213194,469.38775510204084,471.7948717948719,474.2268041237114,476.68393782383424,479.1666666666667,481.67539267015707,484.2105263157894,486.77248677248684,489.36170212765956,491.97860962566835,494.62365591397855,497.29729729729723,500.,502.7322404371584,505.49450549450546,508.2872928176796,511.11111111111114,513.9664804469273,516.8539325842697,519.7740112994351,522.7272727272727,525.7142857142857,528.735632183908,531.7919075144509,534.8837209302326,538.0116959064327,541.1764705882354,544.3786982248521,547.6190476190476,550.8982035928144,554.2168674698796,557.5757575757576,560.9756097560976,564.4171779141104,567.9012345679012,571.4285714285714,575.,578.6163522012579,582.2784810126582,585.9872611464967,589.7435897435897,593.5483870967741,597.4025974025974,601.3071895424837,605.2631578947369,609.271523178808,613.3333333333333,617.4496644295302,621.6216216216216,625.8503401360545,630.1369863013698,634.4827586206897,638.8888888888889,643.3566433566434,647.8873239436621,652.482269503546,657.1428571428571,661.8705035971223,666.6666666666667,671.5328467153284,676.4705882352941,681.4814814814814,686.5671641791046,691.7293233082706,696.9696969696971,702.2900763358778,707.6923076923078,713.1782945736434,718.75,724.4094488188975,730.1587301587301,736.,741.9354838709678,747.9674796747968,754.0983606557377,760.3305785123968,766.6666666666667,773.109243697479,779.6610169491526,786.3247863247864,793.103448275862,800.0000000000001,807.0175438596491,814.1592920353983,821.4285714285713,828.8288288288289,836.3636363636364,844.0366972477066,851.8518518518517,859.8130841121497,867.9245283018867,876.1904761904764,884.6153846153845,893.2038834951456,901.9607843137254,910.8910891089109,920. +455.,457.286432160804,459.59595959595964,461.92893401015226,464.2857142857143,466.66666666666674,469.07216494845363,471.5025906735752,473.95833333333337,476.4397905759162,478.9473684210526,481.4814814814816,484.04255319148933,486.6310160427807,489.24731182795705,491.8918918918918,494.5652173913043,497.2677595628415,499.99999999999994,502.76243093922653,505.5555555555556,508.3798882681564,511.2359550561798,514.1242937853108,517.0454545454546,520.,522.9885057471264,526.0115606936416,529.0697674418604,532.1637426900585,535.2941176470589,538.4615384615385,541.6666666666666,544.9101796407186,548.1927710843374,551.5151515151515,554.8780487804878,558.282208588957,561.7283950617284,565.2173913043479,568.75,572.3270440251573,575.9493670886076,579.6178343949044,583.3333333333333,587.0967741935484,590.9090909090909,594.7712418300654,598.6842105263158,602.6490066225166,606.6666666666666,610.738255033557,614.8648648648649,619.047619047619,623.2876712328767,627.5862068965517,631.9444444444445,636.3636363636365,640.8450704225353,645.3900709219857,650.,654.6762589928057,659.4202898550726,664.2335766423357,669.1176470588235,674.074074074074,679.1044776119403,684.2105263157895,689.3939393939395,694.6564885496183,700.0000000000001,705.4263565891472,710.9375,716.5354330708661,722.2222222222222,728.,733.8709677419355,739.8373983739838,745.9016393442623,752.0661157024794,758.3333333333334,764.7058823529412,771.1864406779662,777.7777777777778,784.4827586206897,791.304347826087,798.2456140350877,805.3097345132744,812.4999999999999,819.8198198198199,827.2727272727273,834.8623853211011,842.5925925925925,850.4672897196263,858.4905660377358,866.6666666666669,874.9999999999999,883.495145631068,892.156862745098,900.990099009901,910. +450.,452.2613065326633,454.54545454545456,456.8527918781726,459.18367346938777,461.5384615384616,463.9175257731959,466.3212435233161,468.75000000000006,471.2041884816754,473.6842105263158,476.19047619047626,478.72340425531917,481.28342245989296,483.87096774193554,486.4864864864864,489.1304347826087,491.80327868852453,494.5054945054944,497.2375690607735,500.,502.7932960893854,505.6179775280899,508.47457627118644,511.36363636363643,514.2857142857142,517.2413793103448,520.2312138728324,523.2558139534884,526.3157894736842,529.4117647058823,532.5443786982248,535.7142857142857,538.9221556886228,542.1686746987953,545.4545454545455,548.780487804878,552.1472392638037,555.5555555555555,559.0062111801243,562.5,566.0377358490567,569.6202531645569,573.248407643312,576.9230769230769,580.6451612903226,584.4155844155844,588.2352941176471,592.1052631578948,596.0264900662252,600.,604.026845637584,608.1081081081081,612.2448979591836,616.4383561643835,620.6896551724138,625.,629.3706293706294,633.8028169014085,638.2978723404254,642.8571428571429,647.4820143884892,652.1739130434784,656.9343065693431,661.764705882353,666.6666666666666,671.6417910447761,676.6917293233082,681.8181818181819,687.0229007633587,692.3076923076925,697.6744186046511,703.125,708.6614173228346,714.2857142857142,720.,725.8064516129033,731.7073170731708,737.7049180327868,743.8016528925621,750.,756.3025210084033,762.7118644067797,769.2307692307693,775.8620689655172,782.608695652174,789.4736842105262,796.4601769911504,803.5714285714286,810.810810810811,818.1818181818181,825.6880733944955,833.3333333333333,841.1214953271029,849.0566037735848,857.1428571428573,865.3846153846154,873.7864077669904,882.3529411764705,891.0891089108911,900. +445.,447.2361809045226,449.49494949494954,451.77664974619285,454.08163265306126,456.41025641025647,458.7628865979382,461.13989637305707,463.5416666666667,465.9685863874345,468.4210526315789,470.899470899471,473.40425531914894,475.9358288770053,478.49462365591404,481.081081081081,483.695652173913,486.3387978142076,489.01098901098896,491.71270718232046,494.44444444444446,497.2067039106145,500.00000000000006,502.82485875706215,505.68181818181824,508.57142857142856,511.4942528735632,514.4508670520231,517.4418604651163,520.46783625731,523.5294117647059,526.6272189349113,529.7619047619047,532.934131736527,536.1445783132531,539.3939393939394,542.6829268292682,546.0122699386503,549.3827160493827,552.7950310559007,556.25,559.748427672956,563.2911392405063,566.8789808917197,570.5128205128204,574.1935483870967,577.922077922078,581.6993464052288,585.5263157894738,589.4039735099338,593.3333333333333,597.3154362416108,601.3513513513514,605.4421768707483,609.5890410958904,613.7931034482759,618.0555555555555,622.3776223776224,626.7605633802817,631.2056737588651,635.7142857142858,640.2877697841726,644.9275362318841,649.6350364963504,654.4117647058824,659.2592592592592,664.179104477612,669.172932330827,674.2424242424244,679.3893129770992,684.6153846153848,689.9224806201549,695.3125,700.7874015748031,706.3492063492063,712.,717.741935483871,723.5772357723577,729.5081967213115,735.5371900826447,741.6666666666667,747.8991596638656,754.2372881355933,760.6837606837607,767.2413793103448,773.913043478261,780.7017543859648,787.6106194690266,794.6428571428571,801.801801801802,809.090909090909,816.51376146789,824.074074074074,831.7757009345795,839.6226415094338,847.6190476190478,855.7692307692307,864.0776699029126,872.5490196078431,881.1881188118812,890. +440.,442.2110552763819,444.44444444444446,446.70050761421317,448.9795918367347,451.2820512820513,453.60824742268045,455.95854922279796,458.33333333333337,460.73298429319374,463.1578947368421,465.6084656084657,468.0851063829787,470.58823529411757,473.11827956989254,475.6756756756756,478.2608695652174,480.87431693989066,483.51648351648345,486.18784530386745,488.8888888888889,491.62011173184356,494.38202247191015,497.17514124293785,500.00000000000006,502.85714285714283,505.74712643678157,508.6705202312139,511.6279069767442,514.6198830409356,517.6470588235294,520.7100591715977,523.8095238095239,526.9461077844312,530.1204819277109,533.3333333333334,536.5853658536586,539.8773006134969,543.2098765432098,546.583850931677,550.,553.4591194968555,556.9620253164557,560.5095541401273,564.1025641025641,567.741935483871,571.4285714285714,575.1633986928105,578.9473684210527,582.7814569536424,586.6666666666666,590.6040268456376,594.5945945945946,598.6394557823129,602.7397260273972,606.896551724138,611.1111111111111,615.3846153846155,619.7183098591549,624.1134751773048,628.5714285714286,633.0935251798561,637.68115942029,642.3357664233577,647.0588235294118,651.8518518518518,656.7164179104478,661.6541353383458,666.6666666666667,671.7557251908396,676.923076923077,682.1705426356589,687.5,692.9133858267716,698.4126984126983,704.,709.6774193548388,715.4471544715446,721.3114754098361,727.2727272727273,733.3333333333334,739.4957983193277,745.7627118644068,752.1367521367522,758.6206896551723,765.2173913043479,771.9298245614035,778.7610619469027,785.7142857142857,792.7927927927929,800.,807.3394495412846,814.8148148148147,822.4299065420562,830.188679245283,838.0952380952383,846.1538461538461,854.3689320388349,862.7450980392157,871.2871287128713,880. +435.,437.18592964824114,439.39393939393943,441.6243654822335,443.8775510204082,446.1538461538462,448.4536082474227,450.7772020725389,453.12500000000006,455.4973821989529,457.89473684210526,460.3174603174604,462.7659574468085,465.2406417112299,467.74193548387103,470.2702702702702,472.8260869565217,475.4098360655737,478.021978021978,480.6629834254144,483.33333333333337,486.0335195530726,488.76404494382024,491.52542372881356,494.31818181818187,497.1428571428571,499.99999999999994,502.89017341040466,505.81395348837214,508.77192982456137,511.764705882353,514.792899408284,517.8571428571429,520.9580838323354,524.0963855421687,527.2727272727273,530.4878048780488,533.7423312883436,537.037037037037,540.3726708074535,543.75,547.1698113207548,550.632911392405,554.140127388535,557.6923076923076,561.2903225806451,564.9350649350649,568.6274509803922,572.3684210526317,576.158940397351,580.,583.8926174496645,587.8378378378378,591.8367346938776,595.8904109589041,600.,604.1666666666666,608.3916083916084,612.6760563380283,617.0212765957446,621.4285714285714,625.8992805755396,630.4347826086957,635.0364963503649,639.7058823529412,644.4444444444445,649.2537313432837,654.1353383458646,659.0909090909092,664.1221374045801,669.2307692307694,674.4186046511627,679.6875,685.0393700787401,690.4761904761905,696.,701.6129032258065,707.3170731707316,713.1147540983607,719.00826446281,725.,731.09243697479,737.2881355932204,743.5897435897436,750.,756.5217391304349,763.1578947368421,769.9115044247789,776.7857142857142,783.7837837837839,790.9090909090909,798.165137614679,805.5555555555554,813.0841121495328,820.754716981132,828.5714285714287,836.5384615384614,844.6601941747573,852.9411764705882,861.3861386138614,870. +430.,432.16080402010044,434.34343434343435,436.5482233502538,438.7755102040816,441.0256410256411,443.298969072165,445.59585492227984,447.9166666666667,450.26178010471205,452.6315789473684,455.0264550264551,457.4468085106383,459.8930481283422,462.3655913978495,464.8648648648648,467.39130434782606,469.9453551912568,472.52747252747247,475.1381215469614,477.77777777777777,480.44692737430165,483.1460674157304,485.8757062146893,488.6363636363637,491.4285714285714,494.2528735632184,497.1098265895954,500.00000000000006,502.9239766081871,505.88235294117646,508.87573964497045,511.9047619047619,514.9700598802395,518.0722891566265,521.2121212121212,524.390243902439,527.6073619631902,530.8641975308642,534.1614906832299,537.5,540.8805031446542,544.3037974683543,547.7707006369426,551.2820512820513,554.8387096774194,558.4415584415584,562.0915032679738,565.7894736842105,569.5364238410597,573.3333333333333,577.1812080536913,581.081081081081,585.0340136054422,589.0410958904109,593.1034482758621,597.2222222222222,601.3986013986015,605.6338028169015,609.9290780141844,614.2857142857143,618.705035971223,623.1884057971015,627.7372262773723,632.3529411764706,637.037037037037,641.7910447761194,646.6165413533835,651.5151515151516,656.4885496183206,661.5384615384617,666.6666666666666,671.875,677.1653543307086,682.5396825396825,688.,693.5483870967743,699.1869918699186,704.9180327868853,710.7438016528926,716.6666666666667,722.6890756302521,728.8135593220339,735.0427350427351,741.3793103448276,747.8260869565219,754.3859649122807,761.0619469026549,767.8571428571428,774.7747747747749,781.8181818181818,788.9908256880735,796.2962962962962,803.7383177570094,811.320754716981,819.0476190476192,826.9230769230769,834.9514563106796,843.1372549019608,851.4851485148515,860. +425.,427.13567839195974,429.29292929292933,431.47208121827407,433.6734693877551,435.89743589743597,438.14432989690727,440.4145077720208,442.70833333333337,445.0261780104712,447.36842105263156,449.7354497354498,452.12765957446805,454.54545454545445,456.989247311828,459.45945945945937,461.95652173913044,464.48087431693983,467.03296703296695,469.6132596685083,472.22222222222223,474.8603351955307,477.5280898876405,480.225988700565,482.9545454545455,485.71428571428567,488.50574712643675,491.32947976878614,494.1860465116279,497.07602339181284,500.,502.9585798816568,505.95238095238096,508.9820359281438,512.0481927710844,515.1515151515151,518.2926829268292,521.4723926380368,524.6913580246913,527.9503105590063,531.25,534.5911949685535,537.9746835443037,541.4012738853503,544.8717948717948,548.3870967741935,551.9480519480519,555.5555555555555,559.2105263157895,562.9139072847682,566.6666666666666,570.4697986577181,574.3243243243243,578.2312925170068,582.1917808219177,586.2068965517242,590.2777777777777,594.4055944055945,598.5915492957747,602.8368794326241,607.1428571428571,611.5107913669065,615.9420289855074,620.4379562043796,625.,629.6296296296296,634.3283582089553,639.0977443609022,643.939393939394,648.854961832061,653.8461538461539,658.9147286821704,664.0625,669.2913385826771,674.6031746031746,680.,685.483870967742,691.0569105691056,696.7213114754098,702.4793388429753,708.3333333333334,714.2857142857143,720.3389830508476,726.4957264957266,732.7586206896551,739.1304347826087,745.6140350877192,752.212389380531,758.9285714285713,765.7657657657659,772.7272727272727,779.8165137614681,787.037037037037,794.3925233644861,801.8867924528302,809.5238095238096,817.3076923076923,825.242718446602,833.3333333333333,841.5841584158416,850. +420.,422.11055276381904,424.24242424242425,426.3959390862944,428.57142857142856,430.76923076923083,432.98969072164954,435.2331606217617,437.50000000000006,439.79057591623035,442.10526315789474,444.4444444444445,446.8085106382979,449.1978609625668,451.6129032258065,454.054054054054,456.52173913043475,459.0163934426229,461.5384615384615,464.0883977900553,466.6666666666667,469.27374301675974,471.91011235955057,474.5762711864407,477.2727272727273,480.,482.7586206896551,485.54913294797694,488.37209302325584,491.2280701754386,494.11764705882354,497.04142011834324,500.,502.99401197604794,506.02409638554224,509.0909090909091,512.1951219512194,515.3374233128834,518.5185185185185,521.7391304347826,525.,528.3018867924529,531.6455696202531,535.031847133758,538.4615384615385,541.9354838709677,545.4545454545455,549.0196078431372,552.6315789473684,556.2913907284768,560.,563.758389261745,567.5675675675676,571.4285714285714,575.3424657534247,579.3103448275863,583.3333333333333,587.4125874125875,591.5492957746479,595.7446808510638,600.,604.31654676259,608.6956521739131,613.1386861313869,617.6470588235295,622.2222222222222,626.8656716417911,631.578947368421,636.3636363636365,641.2213740458014,646.1538461538463,651.1627906976744,656.25,661.4173228346456,666.6666666666666,672.,677.4193548387098,682.9268292682926,688.5245901639345,694.2148760330579,700.,705.8823529411765,711.8644067796611,717.948717948718,724.1379310344827,730.4347826086957,736.8421052631578,743.3628318584072,750.,756.7567567567569,763.6363636363636,770.6422018348625,777.7777777777777,785.0467289719627,792.4528301886792,800.0000000000001,807.6923076923076,815.5339805825242,823.5294117647059,831.6831683168317,840. +415.,417.08542713567834,419.1919191919192,421.3197969543147,423.46938775510205,425.6410256410257,427.8350515463918,430.05181347150267,432.2916666666667,434.5549738219895,436.84210526315786,439.1534391534392,441.48936170212767,443.85026737967905,446.236559139785,448.6486486486486,451.0869565217391,453.55191256830597,456.043956043956,458.56353591160223,461.11111111111114,463.6871508379888,466.2921348314607,468.9265536723164,471.5909090909091,474.2857142857143,477.01149425287355,479.7687861271677,482.55813953488376,485.3801169590643,488.2352941176471,491.1242603550296,494.04761904761904,497.00598802395217,500.00000000000006,503.03030303030306,506.0975609756097,509.20245398773005,512.3456790123456,515.527950310559,518.75,522.0125786163522,525.3164556962025,528.6624203821656,532.051282051282,535.483870967742,538.961038961039,542.4836601307189,546.0526315789474,549.6688741721855,553.3333333333333,557.0469798657718,560.8108108108108,564.625850340136,568.4931506849315,572.4137931034484,576.3888888888889,580.4195804195805,584.5070422535211,588.6524822695035,592.8571428571429,597.1223021582733,601.449275362319,605.8394160583941,610.2941176470589,614.8148148148148,619.4029850746269,624.0601503759398,628.7878787878789,633.587786259542,638.4615384615386,643.4108527131782,648.4375,653.5433070866142,658.7301587301587,664.,669.3548387096774,674.7967479674796,680.327868852459,685.9504132231405,691.6666666666667,697.4789915966387,703.3898305084747,709.4017094017095,715.5172413793103,721.7391304347827,728.0701754385965,734.5132743362832,741.0714285714286,747.7477477477479,754.5454545454545,761.467889908257,768.5185185185185,775.7009345794394,783.0188679245282,790.4761904761906,798.076923076923,805.8252427184466,813.7254901960785,821.7821782178218,830. +410.,412.06030150753764,414.14141414141415,416.243654822335,418.36734693877554,420.51282051282055,422.6804123711341,424.87046632124355,427.08333333333337,429.3193717277487,431.57894736842104,433.86243386243393,436.17021276595744,438.5026737967914,440.8602150537635,443.24324324324317,445.65217391304344,448.087431693989,450.5494505494505,453.0386740331492,455.5555555555556,458.1005586592178,460.6741573033708,463.2768361581921,465.90909090909093,468.57142857142856,471.2643678160919,473.98843930635843,476.74418604651163,479.53216374269005,482.3529411764706,485.20710059171597,488.0952380952381,491.01796407185634,493.9759036144579,496.969696969697,500.,503.0674846625767,506.1728395061728,509.3167701863355,512.5,515.7232704402517,518.9873417721518,522.2929936305733,525.6410256410256,529.0322580645161,532.4675324675325,535.9477124183006,539.4736842105264,543.0463576158941,546.6666666666666,550.3355704697987,554.0540540540541,557.8231292517006,561.6438356164383,565.5172413793103,569.4444444444445,573.4265734265734,577.4647887323944,581.5602836879432,585.7142857142858,589.9280575539568,594.2028985507247,598.5401459854014,602.9411764705883,607.4074074074074,611.9402985074627,616.5413533834586,621.2121212121212,625.9541984732824,630.769230769231,635.6589147286821,640.625,645.6692913385826,650.7936507936507,656.,661.2903225806452,666.6666666666666,672.1311475409836,677.6859504132232,683.3333333333334,689.0756302521008,694.9152542372882,700.8547008547009,706.8965517241379,713.0434782608696,719.2982456140351,725.6637168141593,732.1428571428571,738.7387387387388,745.4545454545454,752.2935779816515,759.2592592592591,766.355140186916,773.5849056603773,780.9523809523811,788.4615384615383,796.1165048543689,803.9215686274509,811.8811881188119,820. +405.,407.03517587939695,409.0909090909091,411.1675126903553,413.265306122449,415.3846153846154,417.52577319587635,419.6891191709845,421.87500000000006,424.08376963350787,426.31578947368416,428.5714285714286,430.8510638297872,433.15508021390366,435.483870967742,437.83783783783775,440.2173913043478,442.6229508196721,445.054945054945,447.51381215469615,450.,452.5139664804469,455.05617977528095,457.6271186440678,460.22727272727275,462.85714285714283,465.5172413793103,468.2080924855492,470.93023255813955,473.6842105263158,476.47058823529414,479.2899408284024,482.1428571428571,485.0299401197605,487.9518072289157,490.90909090909093,493.9024390243902,496.9325153374233,500.,503.10559006211184,506.25,509.43396226415103,512.6582278481012,515.9235668789809,519.2307692307692,522.5806451612904,525.974025974026,529.4117647058823,532.8947368421053,536.4238410596026,540.,543.6241610738256,547.2972972972973,551.0204081632653,554.7945205479451,558.6206896551724,562.5,566.4335664335665,570.4225352112677,574.4680851063829,578.5714285714286,582.7338129496403,586.9565217391305,591.2408759124088,595.5882352941177,600.,604.4776119402985,609.0225563909775,613.6363636363637,618.3206106870228,623.0769230769232,627.906976744186,632.8125,637.7952755905511,642.8571428571428,648.,653.2258064516129,658.5365853658536,663.9344262295082,669.4214876033058,675.,680.6722689075631,686.4406779661017,692.3076923076924,698.2758620689655,704.3478260869566,710.5263157894736,716.8141592920355,723.2142857142857,729.7297297297298,736.3636363636364,743.119266055046,749.9999999999999,757.0093457943926,764.1509433962264,771.4285714285716,778.8461538461538,786.4077669902913,794.1176470588235,801.980198019802,810. +400.,402.01005025125625,404.04040404040404,406.0913705583756,408.16326530612247,410.25641025641033,412.3711340206186,414.50777202072544,416.6666666666667,418.848167539267,421.05263157894734,423.28042328042335,425.531914893617,427.807486631016,430.1075268817205,432.4324324324324,434.78260869565213,437.15846994535514,439.56043956043953,441.98895027624314,444.44444444444446,446.92737430167597,449.43820224719104,451.97740112994353,454.54545454545456,457.1428571428571,459.7701149425287,462.4277456647399,465.1162790697675,467.8362573099415,470.5882352941177,473.37278106508876,476.1904761904762,479.04191616766474,481.9277108433735,484.8484848484849,487.8048780487805,490.79754601226995,493.82716049382714,496.89440993788827,500.,503.1446540880504,506.3291139240506,509.5541401273885,512.8205128205128,516.1290322580645,519.4805194805194,522.875816993464,526.3157894736843,529.8013245033113,533.3333333333333,536.9127516778524,540.5405405405405,544.2176870748299,547.945205479452,551.7241379310345,555.5555555555555,559.4405594405595,563.3802816901409,567.3758865248226,571.4285714285714,575.5395683453237,579.7101449275364,583.9416058394161,588.2352941176471,592.5925925925926,597.0149253731344,601.5037593984962,606.0606060606061,610.6870229007633,615.3846153846155,620.1550387596899,625.,629.9212598425196,634.9206349206349,640.,645.1612903225807,650.4065040650406,655.7377049180328,661.1570247933885,666.6666666666667,672.2689075630252,677.9661016949153,683.7606837606838,689.655172413793,695.6521739130435,701.7543859649122,707.9646017699115,714.2857142857142,720.7207207207208,727.2727272727273,733.9449541284405,740.7407407407406,747.6635514018692,754.7169811320754,761.904761904762,769.2307692307692,776.6990291262136,784.313725490196,792.0792079207921,800. +395.,396.98492462311555,398.989898989899,401.01522842639594,403.0612244897959,405.1282051282052,407.21649484536084,409.3264248704664,411.45833333333337,413.6125654450262,415.7894736842105,417.98941798941803,420.2127659574468,422.45989304812827,424.731182795699,427.027027027027,429.3478260869565,431.69398907103823,434.065934065934,436.4640883977901,438.8888888888889,441.340782122905,443.82022471910113,446.32768361581924,448.86363636363643,451.4285714285714,454.0229885057471,456.64739884393066,459.3023255813954,461.98830409356725,464.7058823529412,467.4556213017752,470.23809523809524,473.0538922155689,475.90361445783134,478.7878787878788,481.7073170731707,484.6625766871166,487.6543209876543,490.68322981366464,493.75,496.85534591194977,499.99999999999994,503.18471337579615,506.41025641025635,509.6774193548387,512.987012987013,516.3398692810457,519.7368421052632,523.1788079470199,526.6666666666666,530.2013422818792,533.7837837837837,537.4149659863946,541.0958904109589,544.8275862068966,548.6111111111111,552.4475524475525,556.3380281690141,560.2836879432623,564.2857142857143,568.3453237410072,572.4637681159421,576.6423357664233,580.8823529411765,585.1851851851851,589.5522388059702,593.984962406015,598.4848484848486,603.0534351145037,607.6923076923078,612.4031007751937,617.1875,622.0472440944882,626.984126984127,632.,637.0967741935484,642.2764227642276,647.5409836065573,652.8925619834711,658.3333333333334,663.8655462184875,669.4915254237288,675.2136752136753,681.0344827586207,686.9565217391305,692.9824561403508,699.1150442477876,705.3571428571428,711.7117117117118,718.1818181818181,724.770642201835,731.4814814814814,738.3177570093459,745.2830188679244,752.3809523809525,759.6153846153845,766.9902912621359,774.5098039215686,782.1782178217821,790. +390.,391.95979899497485,393.93939393939394,395.9390862944162,397.9591836734694,400.00000000000006,402.0618556701031,404.1450777202073,406.25000000000006,408.37696335078533,410.52631578947364,412.69841269841277,414.8936170212766,417.1122994652406,419.3548387096775,421.62162162162156,423.9130434782609,426.22950819672127,428.5714285714285,430.93922651933707,433.33333333333337,435.75418994413405,438.2022471910113,440.67796610169495,443.18181818181824,445.71428571428567,448.27586206896547,450.8670520231214,453.48837209302326,456.14035087719293,458.8235294117647,461.53846153846155,464.2857142857143,467.0658682634731,469.8795180722892,472.72727272727275,475.609756097561,478.5276073619632,481.48148148148147,484.47204968944106,487.5,490.56603773584914,493.67088607594934,496.8152866242038,499.99999999999994,503.22580645161287,506.4935064935065,509.80392156862746,513.1578947368422,516.5562913907285,520.,523.4899328859061,527.027027027027,530.6122448979592,534.2465753424657,537.9310344827586,541.6666666666666,545.4545454545455,549.2957746478874,553.1914893617021,557.1428571428571,561.1510791366907,565.2173913043479,569.3430656934306,573.529411764706,577.7777777777777,582.089552238806,586.4661654135338,590.909090909091,595.4198473282443,600.0000000000001,604.6511627906976,609.375,614.1732283464567,619.047619047619,624.,629.0322580645162,634.1463414634146,639.344262295082,644.6280991735538,650.,655.4621848739496,661.0169491525425,666.6666666666667,672.4137931034483,678.2608695652175,684.2105263157895,690.2654867256638,696.4285714285713,702.7027027027028,709.0909090909091,715.5963302752294,722.2222222222222,728.9719626168226,735.8490566037735,742.857142857143,749.9999999999999,757.2815533980582,764.7058823529411,772.2772277227723,780. +385.,386.93467336683415,388.8888888888889,390.8629441624365,392.8571428571429,394.8717948717949,396.9072164948454,398.9637305699482,401.0416666666667,403.1413612565445,405.2631578947368,407.40740740740745,409.5744680851064,411.76470588235287,413.978494623656,416.21621621621614,418.4782608695652,420.7650273224043,423.07692307692304,425.414364640884,427.77777777777777,430.1675977653631,432.5842696629214,435.02824858757066,437.50000000000006,440.,442.5287356321839,445.08670520231215,447.6744186046512,450.29239766081866,452.94117647058823,455.62130177514797,458.3333333333333,461.0778443113773,463.85542168674704,466.6666666666667,469.5121951219512,472.39263803680984,475.3086419753086,478.26086956521743,481.25,484.2767295597485,487.3417721518987,490.44585987261144,493.58974358974353,496.7741935483871,500.,503.26797385620915,506.5789473684211,509.93377483443714,513.3333333333333,516.7785234899329,520.2702702702702,523.8095238095237,527.3972602739726,531.0344827586207,534.7222222222222,538.4615384615386,542.2535211267606,546.0992907801418,550.,553.956834532374,557.9710144927537,562.043795620438,566.1764705882354,570.3703703703703,574.6268656716418,578.9473684210526,583.3333333333334,587.7862595419847,592.3076923076924,596.8992248062015,601.5625,606.2992125984251,611.1111111111111,616.,620.9677419354839,626.0162601626016,631.1475409836065,636.3636363636364,641.6666666666667,647.0588235294118,652.542372881356,658.1196581196582,663.7931034482758,669.5652173913044,675.438596491228,681.4159292035398,687.5,693.6936936936938,700.,706.422018348624,712.9629629629629,719.6261682242991,726.4150943396226,733.3333333333335,740.3846153846154,747.5728155339806,754.9019607843137,762.3762376237623,770. +380.,381.90954773869345,383.8383838383839,385.78680203045684,387.7551020408163,389.7435897435898,391.75257731958766,393.78238341968915,395.83333333333337,397.90575916230364,400.,402.1164021164022,404.25531914893617,406.4171122994652,408.6021505376345,410.8108108108107,413.04347826086956,415.3005464480874,417.5824175824175,419.88950276243094,422.22222222222223,424.58100558659214,426.9662921348315,429.3785310734463,431.81818181818187,434.2857142857143,436.78160919540227,439.30635838150295,441.8604651162791,444.4444444444444,447.05882352941177,449.70414201183434,452.38095238095235,455.0898203592815,457.83132530120486,460.6060606060606,463.4146341463414,466.25766871165644,469.1358024691358,472.04968944099386,475.,477.98742138364787,481.012658227848,484.0764331210191,487.1794871794871,490.3225806451613,493.5064935064935,496.73202614379085,500.00000000000006,503.3112582781457,506.66666666666663,510.06711409395973,513.5135135135135,517.0068027210884,520.5479452054794,524.1379310344828,527.7777777777777,531.4685314685315,535.2112676056338,539.0070921985815,542.8571428571429,546.7625899280575,550.7246376811595,554.7445255474453,558.8235294117648,562.9629629629629,567.1641791044776,571.4285714285714,575.7575757575759,580.1526717557251,584.6153846153848,589.1472868217054,593.75,598.4251968503936,603.1746031746031,608.,612.9032258064517,617.8861788617886,622.9508196721312,628.099173553719,633.3333333333334,638.655462184874,644.0677966101696,649.5726495726497,655.1724137931034,660.8695652173914,666.6666666666666,672.566371681416,678.5714285714286,684.6846846846847,690.9090909090909,697.2477064220185,703.7037037037036,710.2803738317758,716.9811320754716,723.809523809524,730.7692307692307,737.8640776699029,745.0980392156863,752.4752475247525,760. +375.,376.88442211055275,378.7878787878788,380.71065989847716,382.6530612244898,384.61538461538464,386.59793814432993,388.6010362694301,390.625,392.67015706806285,394.7368421052631,396.82539682539687,398.93617021276594,401.0695187165775,403.225806451613,405.40540540540536,407.6086956521739,409.83606557377044,412.08791208791206,414.3646408839779,416.6666666666667,418.9944134078212,421.3483146067416,423.728813559322,426.1363636363637,428.57142857142856,431.03448275862064,433.5260115606937,436.046511627907,438.59649122807014,441.1764705882353,443.7869822485207,446.42857142857144,449.10179640718565,451.8072289156627,454.54545454545456,457.3170731707317,460.1226993865031,462.96296296296293,465.8385093167702,468.75,471.69811320754724,474.6835443037974,477.7070063694267,480.7692307692307,483.8709677419355,487.012987012987,490.19607843137254,493.42105263157896,496.68874172185434,500.,503.3557046979866,506.7567567567567,510.2040816326531,513.6986301369863,517.2413793103449,520.8333333333334,524.4755244755245,528.1690140845071,531.9148936170212,535.7142857142858,539.568345323741,543.4782608695652,547.4452554744526,551.4705882352941,555.5555555555555,559.7014925373135,563.9097744360902,568.1818181818182,572.5190839694656,576.923076923077,581.3953488372092,585.9375,590.5511811023622,595.2380952380952,600.,604.8387096774194,609.7560975609756,614.7540983606558,619.8347107438017,625.,630.2521008403362,635.5932203389831,641.0256410256411,646.551724137931,652.1739130434784,657.8947368421052,663.7168141592921,669.6428571428571,675.6756756756757,681.8181818181818,688.0733944954129,694.4444444444443,700.9345794392524,707.5471698113207,714.2857142857144,721.1538461538461,728.1553398058253,735.2941176470588,742.5742574257425,750. +370.,371.85929648241205,373.7373737373738,375.6345177664974,377.55102040816325,379.48717948717956,381.4432989690722,383.41968911917104,385.4166666666667,387.434554973822,389.4736842105263,391.5343915343916,393.6170212765957,395.72192513368975,397.8494623655915,399.99999999999994,402.17391304347825,404.37158469945354,406.59340659340654,408.83977900552486,411.11111111111114,413.4078212290502,415.7303370786517,418.07909604519773,420.4545454545455,422.85714285714283,425.28735632183907,427.74566473988443,430.2325581395349,432.74853801169587,435.29411764705884,437.8698224852071,440.4761904761905,443.1137724550899,445.7831325301205,448.4848484848485,451.2195121951219,453.9877300613497,456.79012345679007,459.62732919254665,462.5,465.4088050314466,468.35443037974676,471.33757961783436,474.3589743589743,477.41935483870964,480.5194805194805,483.66013071895424,486.8421052631579,490.06622516556297,493.3333333333333,496.64429530201346,500.,503.40136054421765,506.8493150684931,510.3448275862069,513.8888888888889,517.4825174825176,521.1267605633803,524.8226950354609,528.5714285714286,532.3741007194244,536.2318840579711,540.1459854014598,544.1176470588235,548.1481481481482,552.2388059701493,556.390977443609,560.6060606060607,564.8854961832061,569.2307692307694,573.6434108527131,578.125,582.6771653543307,587.3015873015872,592.,596.7741935483872,601.6260162601626,606.5573770491803,611.5702479338843,616.6666666666667,621.8487394957983,627.1186440677967,632.4786324786326,637.9310344827586,643.4782608695652,649.1228070175438,654.8672566371682,660.7142857142857,666.6666666666667,672.7272727272727,678.8990825688074,685.1851851851851,691.5887850467291,698.1132075471697,704.7619047619049,711.5384615384614,718.4466019417475,725.4901960784314,732.6732673267327,740. +365.,366.83417085427135,368.6868686868687,370.55837563451774,372.44897959183675,374.3589743589744,376.2886597938145,378.238341968912,380.20833333333337,382.19895287958116,384.2105263157895,386.2433862433863,388.2978723404255,390.3743315508021,392.47311827957,394.5945945945945,396.73913043478257,398.9071038251366,401.098901098901,403.31491712707185,405.5555555555556,407.8212290502793,410.11235955056185,412.42937853107344,414.7727272727273,417.1428571428571,419.54022988505744,421.9653179190752,424.4186046511628,426.9005847953216,429.4117647058824,431.9526627218935,434.5238095238095,437.12574850299404,439.7590361445784,442.42424242424244,445.1219512195122,447.85276073619633,450.61728395061726,453.416149068323,456.25,459.119496855346,462.02531645569616,464.968152866242,467.9487179487179,470.96774193548384,474.025974025974,477.12418300653593,480.2631578947369,483.44370860927154,486.66666666666663,489.93288590604027,493.2432432432432,496.5986394557823,500.,503.448275862069,506.9444444444444,510.48951048951056,514.0845070422536,517.7304964539006,521.4285714285714,525.1798561151079,528.9855072463769,532.8467153284671,536.7647058823529,540.7407407407408,544.7761194029852,548.8721804511277,553.0303030303031,557.2519083969465,561.5384615384617,565.891472868217,570.3125,574.8031496062991,579.3650793650793,584.,588.7096774193549,593.4959349593496,598.360655737705,603.305785123967,608.3333333333334,613.4453781512605,618.6440677966102,623.931623931624,629.3103448275862,634.7826086956522,640.3508771929825,646.0176991150443,651.7857142857142,657.6576576576578,663.6363636363636,669.724770642202,675.9259259259259,682.2429906542056,688.6792452830188,695.2380952380954,701.9230769230769,708.7378640776699,715.6862745098039,722.7722772277227,730. +360.,361.8090452261306,363.6363636363637,365.48223350253807,367.34693877551024,369.2307692307693,371.13402061855675,373.0569948186529,375.,376.9633507853403,378.9473684210526,380.952380952381,382.97872340425533,385.02673796791436,387.09677419354847,389.1891891891891,391.30434782608694,393.4426229508196,395.60439560439556,397.7900552486188,400.,402.23463687150837,404.49438202247194,406.77966101694915,409.0909090909091,411.4285714285714,413.7931034482758,416.1849710982659,418.60465116279073,421.05263157894734,423.5294117647059,426.0355029585799,428.57142857142856,431.13772455089827,433.7349397590362,436.3636363636364,439.0243902439024,441.717791411043,444.4444444444444,447.20496894409945,450.,452.83018867924534,455.6962025316455,458.59872611464965,461.5384615384615,464.51612903225805,467.53246753246754,470.5882352941177,473.68421052631584,476.82119205298017,480.,483.22147651006713,486.48648648648646,489.7959183673469,493.1506849315068,496.55172413793105,500.,503.4965034965036,507.0422535211268,510.6382978723404,514.2857142857143,517.9856115107914,521.7391304347827,525.5474452554745,529.4117647058824,533.3333333333333,537.3134328358209,541.3533834586466,545.4545454545455,549.618320610687,553.8461538461539,558.1395348837209,562.5,566.9291338582676,571.4285714285714,576.,580.6451612903227,585.3658536585366,590.1639344262295,595.0413223140496,600.,605.0420168067227,610.1694915254238,615.3846153846155,620.6896551724137,626.0869565217392,631.578947368421,637.1681415929204,642.8571428571428,648.6486486486488,654.5454545454545,660.5504587155964,666.6666666666666,672.8971962616823,679.2452830188679,685.7142857142858,692.3076923076923,699.0291262135922,705.8823529411765,712.8712871287129,720. +355.,356.7839195979899,358.5858585858586,360.40609137055833,362.2448979591837,364.10256410256414,365.979381443299,367.8756476683938,369.7916666666667,371.72774869109946,373.6842105263158,375.6613756613757,377.6595744680851,379.6791443850267,381.7204301075269,383.78378378378375,385.8695652173913,387.9781420765027,390.10989010989005,392.2651933701658,394.44444444444446,396.6480446927374,398.87640449438203,401.12994350282486,403.40909090909093,405.71428571428567,408.04597701149424,410.40462427745666,412.79069767441865,415.2046783625731,417.64705882352945,420.1183431952663,422.6190476190476,425.14970059880244,427.710843373494,430.3030303030303,432.9268292682927,435.5828220858896,438.2716049382716,440.9937888198758,443.75,446.5408805031447,449.3670886075949,452.2292993630573,455.1282051282051,458.06451612903226,461.038961038961,464.0522875816994,467.1052631578948,470.1986754966888,473.3333333333333,476.510067114094,479.72972972972974,482.99319727891157,486.3013698630137,489.65517241379314,493.05555555555554,496.50349650349654,500.00000000000006,503.5460992907801,507.14285714285717,510.7913669064748,514.4927536231885,518.2481751824818,522.0588235294118,525.9259259259259,529.8507462686567,533.8345864661654,537.878787878788,541.9847328244274,546.1538461538463,550.3875968992247,554.6875,559.0551181102362,563.4920634920635,568.,572.5806451612904,577.2357723577236,581.9672131147541,586.7768595041323,591.6666666666667,596.6386554621848,601.6949152542373,606.8376068376069,612.0689655172414,617.3913043478261,622.8070175438596,628.3185840707965,633.9285714285713,639.6396396396398,645.4545454545454,651.3761467889909,657.4074074074073,663.551401869159,669.8113207547169,676.1904761904763,682.6923076923076,689.3203883495146,696.078431372549,702.9702970297029,710. +350.,351.7587939698492,353.5353535353536,355.32994923857865,357.14285714285717,358.974358974359,360.8247422680413,362.69430051813475,364.58333333333337,366.4921465968586,368.4210526315789,370.37037037037044,372.3404255319149,374.33155080213896,376.3440860215054,378.37837837837833,380.4347826086956,382.51366120218574,384.6153846153846,386.7403314917127,388.8888888888889,391.06145251396646,393.2584269662922,395.48022598870057,397.72727272727275,400.,402.2988505747126,404.6242774566474,406.9767441860465,409.3567251461988,411.7647058823529,414.2011834319527,416.6666666666667,419.1616766467066,421.68674698795184,424.24242424242425,426.8292682926829,429.4478527607362,432.09876543209873,434.78260869565224,437.5,440.2515723270441,443.03797468354423,445.85987261146494,448.7179487179487,451.61290322580646,454.54545454545456,457.51633986928107,460.5263157894737,463.57615894039736,466.66666666666663,469.7986577181208,472.97297297297297,476.19047619047615,479.4520547945205,482.75862068965523,486.1111111111111,489.51048951048955,492.9577464788733,496.4539007092198,500.,503.59712230215825,507.2463768115943,510.94890510948903,514.7058823529412,518.5185185185185,522.3880597014926,526.3157894736842,530.3030303030304,534.3511450381679,538.4615384615386,542.6356589147287,546.875,551.1811023622047,555.5555555555555,560.,564.5161290322582,569.1056910569106,573.7704918032787,578.5123966942149,583.3333333333334,588.2352941176471,593.2203389830509,598.2905982905984,603.448275862069,608.6956521739131,614.0350877192982,619.4690265486726,625.,630.6306306306307,636.3636363636364,642.2018348623855,648.148148148148,654.2056074766356,660.3773584905659,666.6666666666667,673.076923076923,679.6116504854369,686.2745098039215,693.0693069306931,700. +345.,346.7336683417085,348.4848484848485,350.25380710659897,352.0408163265306,353.84615384615387,355.67010309278356,357.5129533678757,359.375,361.25654450261777,363.1578947368421,365.0793650793651,367.02127659574467,368.9839572192513,370.9677419354839,372.9729729729729,375.,377.04918032786884,379.12087912087907,381.2154696132597,383.33333333333337,385.4748603351955,387.64044943820227,389.8305084745763,392.04545454545456,394.2857142857143,396.551724137931,398.8439306358382,401.16279069767444,403.50877192982455,405.88235294117646,408.28402366863907,410.7142857142857,413.17365269461084,415.66265060240966,418.1818181818182,420.7317073170732,423.3128834355828,425.9259259259259,428.5714285714286,431.25,433.96226415094344,436.70886075949363,439.4904458598726,442.30769230769226,445.1612903225806,448.05194805194805,450.98039215686276,453.94736842105266,456.953642384106,460.,463.0872483221477,466.2162162162162,469.3877551020408,472.6027397260274,475.86206896551727,479.16666666666663,482.51748251748256,485.9154929577465,489.3617021276595,492.8571428571429,496.4028776978417,500.00000000000006,503.64963503649636,507.3529411764706,511.1111111111111,514.9253731343284,518.796992481203,522.7272727272727,526.7175572519084,530.7692307692308,534.8837209302325,539.0625,543.3070866141732,547.6190476190476,552.,556.4516129032259,560.9756097560976,565.5737704918033,570.2479338842975,575.,579.8319327731092,584.7457627118645,589.7435897435898,594.8275862068965,600.0000000000001,605.2631578947368,610.6194690265487,616.0714285714286,621.6216216216217,627.2727272727273,633.02752293578,638.8888888888888,644.8598130841123,650.9433962264151,657.1428571428572,663.4615384615385,669.9029126213592,676.4705882352941,683.1683168316831,690. +340.,341.7085427135678,343.4343434343435,345.1776649746193,346.9387755102041,348.7179487179488,350.51546391752584,352.33160621761664,354.1666666666667,356.020942408377,357.89473684210526,359.78835978835986,361.70212765957444,363.63636363636357,365.5913978494624,367.5675675675675,369.5652173913043,371.5846994535519,373.6263736263736,375.69060773480663,377.77777777777777,379.88826815642454,382.02247191011236,384.180790960452,386.3636363636364,388.57142857142856,390.8045977011494,393.06358381502895,395.34883720930236,397.6608187134503,400.,402.3668639053255,404.76190476190476,407.185628742515,409.63855421686753,412.1212121212121,414.6341463414634,417.17791411042947,419.75308641975306,422.36024844720504,425.,427.6729559748428,430.379746835443,433.1210191082802,435.89743589743586,438.7096774193548,441.55844155844153,444.44444444444446,447.3684210526316,450.3311258278146,453.3333333333333,456.37583892617454,459.4594594594594,462.5850340136054,465.7534246575342,468.96551724137936,472.22222222222223,475.5244755244756,478.87323943661977,482.26950354609926,485.7142857142857,489.2086330935252,492.75362318840587,496.35036496350364,500.00000000000006,503.7037037037037,507.4626865671642,511.2781954887218,515.1515151515152,519.0839694656488,523.0769230769232,527.1317829457364,531.25,535.4330708661416,539.6825396825396,544.,548.3870967741935,552.8455284552846,557.3770491803278,561.9834710743802,566.6666666666667,571.4285714285714,576.271186440678,581.1965811965813,586.2068965517241,591.304347826087,596.4912280701755,601.7699115044248,607.1428571428571,612.6126126126127,618.1818181818181,623.8532110091744,629.6296296296296,635.5140186915888,641.5094339622641,647.6190476190477,653.8461538461538,660.1941747572815,666.6666666666666,673.2673267326733,680. +335.,336.6834170854271,338.3838383838384,340.10152284263955,341.83673469387753,343.58974358974365,345.36082474226805,347.1502590673576,348.95833333333337,350.78534031413614,352.6315789473684,354.49735449735454,356.3829787234042,358.2887700534759,360.2150537634409,362.16216216216213,364.1304347826087,366.1202185792349,368.1318681318681,370.1657458563536,372.22222222222223,374.3016759776536,376.4044943820225,378.5310734463277,380.68181818181824,382.85714285714283,385.0574712643678,387.2832369942197,389.5348837209303,391.812865497076,394.11764705882354,396.44970414201185,398.8095238095238,401.1976047904192,403.61445783132535,406.06060606060606,408.5365853658536,411.04294478527606,413.5802469135802,416.1490683229814,418.75,421.3836477987422,424.05063291139237,426.75159235668787,429.48717948717945,432.258064516129,435.06493506493507,437.90849673202615,440.7894736842106,443.7086092715232,446.66666666666663,449.66442953020135,452.7027027027027,455.78231292517006,458.90410958904107,462.0689655172414,465.27777777777777,468.5314685314686,471.830985915493,475.17730496453896,478.57142857142856,482.0143884892086,485.5072463768117,489.05109489051097,492.64705882352945,496.29629629629625,500.00000000000006,503.7593984962406,507.5757575757576,511.4503816793893,515.3846153846155,519.3798449612402,523.4375,527.5590551181102,531.7460317460317,536.,540.3225806451613,544.7154471544716,549.1803278688525,553.7190082644628,558.3333333333334,563.0252100840336,567.7966101694916,572.6495726495727,577.5862068965517,582.608695652174,587.719298245614,592.9203539823009,598.2142857142857,603.6036036036037,609.0909090909091,614.6788990825689,620.3703703703703,626.1682242990655,632.0754716981131,638.0952380952382,644.2307692307692,650.4854368932039,656.8627450980392,663.3663366336633,670. +330.,331.6582914572864,333.33333333333337,335.0253807106599,336.734693877551,338.4615384615385,340.2061855670103,341.96891191709847,343.75,345.5497382198953,347.36842105263156,349.2063492063493,351.06382978723406,352.9411764705882,354.8387096774194,356.7567567567567,358.695652173913,360.655737704918,362.63736263736257,364.64088397790056,366.6666666666667,368.71508379888263,370.7865168539326,372.8813559322034,375.00000000000006,377.1428571428571,379.31034482758616,381.50289017341044,383.72093023255815,385.96491228070175,388.2352941176471,390.5325443786982,392.85714285714283,395.2095808383234,397.5903614457832,400.,402.4390243902439,404.9079754601227,407.4074074074074,409.93788819875783,412.5,415.09433962264154,417.7215189873417,420.3821656050955,423.07692307692304,425.80645161290323,428.57142857142856,431.37254901960785,434.2105263157895,437.0860927152318,440.,442.9530201342282,445.94594594594594,448.9795918367347,452.05479452054794,455.1724137931035,458.3333333333333,461.5384615384616,464.78873239436626,468.08510638297867,471.42857142857144,474.82014388489205,478.2608695652175,481.75182481751824,485.29411764705884,488.88888888888886,492.53731343283584,496.24060150375936,500.00000000000006,503.8167938931297,507.6923076923078,511.62790697674416,515.625,519.6850393700787,523.8095238095237,528.,532.258064516129,536.5853658536586,540.983606557377,545.4545454545455,550.,554.6218487394958,559.3220338983051,564.1025641025642,568.9655172413793,573.9130434782609,578.9473684210526,584.070796460177,589.2857142857142,594.5945945945947,600.,605.5045871559635,611.1111111111111,616.8224299065421,622.6415094339623,628.5714285714287,634.6153846153845,640.7766990291262,647.0588235294117,653.4653465346535,660. +325.,326.6331658291457,328.2828282828283,329.9492385786802,331.6326530612245,333.33333333333337,335.0515463917526,336.7875647668394,338.5416666666667,340.31413612565444,342.10526315789474,343.91534391534395,345.74468085106383,347.5935828877005,349.4623655913979,351.3513513513513,353.2608695652174,355.19125683060105,357.1428571428571,359.11602209944755,361.11111111111114,363.12849162011173,365.16853932584274,367.2316384180791,369.31818181818187,371.4285714285714,373.5632183908046,375.7225433526012,377.90697674418607,380.1169590643275,382.3529411764706,384.61538461538464,386.9047619047619,389.2215568862276,391.566265060241,393.93939393939394,396.3414634146341,398.7730061349693,401.2345679012345,403.7267080745342,406.25,408.8050314465409,411.3924050632911,414.01273885350315,416.66666666666663,419.3548387096774,422.0779220779221,424.83660130718954,427.63157894736844,430.46357615894044,433.3333333333333,436.2416107382551,439.18918918918916,442.1768707482993,445.20547945205476,448.2758620689655,451.38888888888886,454.5454545454546,457.7464788732395,460.99290780141837,464.2857142857143,467.62589928057554,471.01449275362324,474.45255474452557,477.9411764705883,481.48148148148147,485.0746268656717,488.72180451127815,492.4242424242425,496.1832061068702,500.0000000000001,503.87596899224803,507.8125,511.8110236220472,515.8730158730158,520.,524.1935483870968,528.4552845528456,532.7868852459017,537.1900826446281,541.6666666666667,546.218487394958,550.8474576271187,555.5555555555557,560.3448275862069,565.2173913043479,570.1754385964912,575.2212389380531,580.3571428571428,585.5855855855857,590.9090909090909,596.3302752293579,601.8518518518517,607.4766355140188,613.2075471698113,619.0476190476192,625.,631.0679611650486,637.2549019607843,643.5643564356435,650. +320.,321.608040201005,323.23232323232327,324.8730964467005,326.53061224489795,328.20512820512823,329.89690721649487,331.60621761658035,333.33333333333337,335.0785340314136,336.84210526315786,338.6243386243387,340.4255319148936,342.2459893048128,344.0860215053764,345.9459459459459,347.82608695652175,349.7267759562841,351.6483516483516,353.5911602209945,355.55555555555554,357.5418994413408,359.55056179775283,361.5819209039548,363.6363636363637,365.71428571428567,367.81609195402297,369.9421965317919,372.093023255814,374.26900584795317,376.47058823529414,378.698224852071,380.95238095238096,383.23353293413174,385.5421686746988,387.8787878787879,390.2439024390244,392.63803680981596,395.0617283950617,397.51552795031057,400.,402.5157232704403,405.06329113924045,407.6433121019108,410.2564102564102,412.9032258064516,415.5844155844156,418.30065359477123,421.0526315789474,423.841059602649,426.66666666666663,429.5302013422819,432.4324324324324,435.3741496598639,438.35616438356163,441.3793103448276,444.44444444444446,447.5524475524476,450.7042253521127,453.9007092198581,457.14285714285717,460.431654676259,463.76811594202906,467.15328467153284,470.5882352941177,474.074074074074,477.6119402985075,481.203007518797,484.84848484848493,488.54961832061065,492.3076923076924,496.1240310077519,500.,503.9370078740157,507.9365079365079,512.,516.1290322580645,520.3252032520325,524.5901639344263,528.9256198347108,533.3333333333334,537.8151260504202,542.3728813559322,547.0085470085471,551.7241379310344,556.5217391304349,561.4035087719298,566.3716814159293,571.4285714285713,576.5765765765766,581.8181818181818,587.1559633027524,592.5925925925925,598.1308411214955,603.7735849056603,609.5238095238096,615.3846153846154,621.3592233009708,627.4509803921568,633.6633663366337,640. +315.,316.5829145728643,318.1818181818182,319.7969543147208,321.42857142857144,323.0769230769231,324.74226804123714,326.4248704663213,328.125,329.84293193717275,331.57894736842104,333.33333333333337,335.1063829787234,336.89839572192506,338.7096774193549,340.54054054054046,342.39130434782606,344.2622950819672,346.15384615384613,348.0662983425415,350.,351.9553072625698,353.9325842696629,355.93220338983053,357.9545454545455,360.,362.06896551724134,364.16184971098266,366.27906976744185,368.4210526315789,370.5882352941177,372.78106508875743,375.,377.245508982036,379.51807228915663,381.8181818181818,384.1463414634146,386.5030674846626,388.88888888888886,391.304347826087,393.75,396.2264150943397,398.73417721518985,401.27388535031844,403.8461538461538,406.4516129032258,409.09090909090907,411.7647058823529,414.47368421052636,417.21854304635764,420.,422.81879194630875,425.6756756756757,428.57142857142856,431.50684931506845,434.4827586206897,437.5,440.5594405594406,443.661971830986,446.80851063829783,450.,453.2374100719424,456.52173913043487,459.8540145985401,463.2352941176471,466.66666666666663,470.1492537313433,473.6842105263158,477.27272727272737,480.9160305343511,484.6153846153847,488.3720930232558,492.1875,496.06299212598424,500.,504.,508.0645161290323,512.1951219512194,516.3934426229508,520.6611570247934,525.,529.4117647058823,533.8983050847459,538.4615384615385,543.103448275862,547.8260869565217,552.6315789473684,557.5221238938053,562.5,567.5675675675676,572.7272727272727,577.9816513761469,583.3333333333333,588.785046728972,594.3396226415093,600.0000000000001,605.7692307692307,611.6504854368932,617.6470588235294,623.7623762376238,630. +310.,311.5577889447236,313.13131313131316,314.7208121827411,316.3265306122449,317.948717948718,319.5876288659794,321.24352331606224,322.9166666666667,324.60732984293196,326.3157894736842,328.0423280423281,329.78723404255317,331.5508021390374,333.33333333333337,335.1351351351351,336.95652173913044,338.7978142076502,340.6593406593406,342.5414364640884,344.44444444444446,346.36871508379886,348.3146067415731,350.28248587570624,352.2727272727273,354.2857142857143,356.32183908045977,358.38150289017346,360.4651162790698,362.57309941520464,364.7058823529412,366.8639053254438,369.04761904761904,371.25748502994014,373.4939759036145,375.75757575757575,378.0487804878049,380.3680981595092,382.71604938271605,385.09316770186336,387.5,389.93710691823907,392.4050632911392,394.9044585987261,397.4358974358974,400.,402.5974025974026,405.2287581699346,407.8947368421053,410.59602649006627,413.3333333333333,416.1073825503356,418.9189189189189,421.7687074829932,424.6575342465753,427.58620689655174,430.55555555555554,433.5664335664336,436.6197183098592,439.71631205673754,442.8571428571429,446.0431654676259,449.2753623188406,452.55474452554745,455.8823529411765,459.25925925925924,462.6865671641791,466.16541353383457,469.69696969696975,473.28244274809157,476.923076923077,480.62015503875966,484.375,488.1889763779527,492.06349206349205,496.,500.00000000000006,504.0650406504065,508.1967213114754,512.396694214876,516.6666666666667,521.0084033613446,525.4237288135594,529.9145299145299,534.4827586206897,539.1304347826087,543.859649122807,548.6725663716815,553.5714285714286,558.5585585585586,563.6363636363636,568.8073394495414,574.074074074074,579.4392523364487,584.9056603773585,590.4761904761906,596.1538461538461,601.9417475728155,607.843137254902,613.8613861386139,620. +305.,306.5326633165829,308.0808080808081,309.6446700507614,311.2244897959184,312.8205128205129,314.4329896907217,316.0621761658031,317.70833333333337,319.3717277486911,321.05263157894734,322.7513227513228,324.468085106383,326.20320855614966,327.95698924731187,329.7297297297297,331.52173913043475,333.3333333333333,335.16483516483515,337.0165745856354,338.8888888888889,340.7821229050279,342.69662921348316,344.63276836158195,346.5909090909091,348.57142857142856,350.57471264367814,352.6011560693642,354.6511627906977,356.7251461988304,358.8235294117647,360.9467455621302,363.0952380952381,365.26946107784437,367.46987951807233,369.6969696969697,371.9512195121951,374.23312883435585,376.5432098765432,378.8819875776398,381.25,383.64779874213843,386.0759493670886,388.5350318471337,391.025641025641,393.5483870967742,396.1038961038961,398.6928104575164,401.3157894736842,403.9735099337749,406.66666666666663,409.3959731543624,412.16216216216213,414.96598639455783,417.80821917808214,420.68965517241384,423.6111111111111,426.5734265734266,429.5774647887324,432.62411347517724,435.7142857142857,438.84892086330933,442.02898550724643,445.2554744525547,448.5294117647059,451.85185185185185,455.22388059701495,458.64661654135335,462.1212121212122,465.648854961832,469.23076923076934,472.86821705426354,476.5625,480.31496062992125,484.1269841269841,488.,491.9354838709678,495.9349593495935,500.,504.1322314049587,508.33333333333337,512.6050420168067,516.9491525423729,521.3675213675214,525.8620689655172,530.4347826086957,535.0877192982456,539.8230088495576,544.6428571428571,549.5495495495496,554.5454545454545,559.6330275229359,564.8148148148148,570.0934579439253,575.4716981132075,580.9523809523811,586.5384615384614,592.2330097087379,598.0392156862745,603.960396039604,610. +300.,301.5075376884422,303.03030303030306,304.5685279187817,306.12244897959187,307.69230769230774,309.27835051546396,310.88082901554407,312.5,314.13612565445027,315.7894736842105,317.4603174603175,319.1489361702128,320.855614973262,322.58064516129036,324.32432432432427,326.0869565217391,327.86885245901635,329.67032967032964,331.49171270718233,333.33333333333337,335.19553072625695,337.0786516853933,338.98305084745766,340.90909090909093,342.85714285714283,344.8275862068965,346.82080924855495,348.8372093023256,350.8771929824561,352.94117647058823,355.0295857988166,357.14285714285717,359.28143712574854,361.44578313253015,363.6363636363636,365.8536585365854,368.09815950920245,370.3703703703703,372.67080745341616,375.,377.3584905660378,379.7468354430379,382.16560509554137,384.6153846153846,387.09677419354836,389.61038961038963,392.15686274509807,394.7368421052632,397.35099337748346,400.,402.6845637583893,405.4054054054054,408.1632653061224,410.958904109589,413.7931034482759,416.66666666666663,419.58041958041963,422.5352112676057,425.53191489361694,428.57142857142856,431.65467625899277,434.78260869565224,437.95620437956205,441.1764705882353,444.4444444444444,447.7611940298508,451.12781954887214,454.5454545454546,458.0152671755725,461.5384615384616,465.1162790697674,468.75,472.4409448818897,476.19047619047615,480.,483.87096774193554,487.8048780487805,491.8032786885246,495.86776859504135,500.,504.20168067226894,508.4745762711865,512.8205128205128,517.2413793103448,521.7391304347826,526.3157894736842,530.9734513274336,535.7142857142857,540.5405405405406,545.4545454545454,550.4587155963304,555.5555555555555,560.747663551402,566.0377358490565,571.4285714285716,576.9230769230769,582.5242718446602,588.2352941176471,594.059405940594,600. +295.,296.4824120603015,297.979797979798,299.492385786802,301.0204081632653,302.5641025641026,304.1237113402062,305.699481865285,307.2916666666667,308.9005235602094,310.52631578947364,312.1693121693122,313.82978723404256,315.50802139037427,317.20430107526886,318.91891891891885,320.65217391304344,322.4043715846994,324.1758241758241,325.9668508287293,327.77777777777777,329.608938547486,331.4606741573034,333.3333333333333,335.22727272727275,337.1428571428571,339.08045977011494,341.0404624277457,343.0232558139535,345.02923976608184,347.05882352941177,349.11242603550295,351.1904761904762,353.2934131736527,355.421686746988,357.57575757575756,359.7560975609756,361.9631901840491,364.1975308641975,366.4596273291926,368.75,371.06918238993717,373.4177215189873,375.796178343949,378.2051282051282,380.64516129032256,383.1168831168831,385.62091503267976,388.15789473684214,390.7284768211921,393.3333333333333,395.97315436241615,398.64864864864865,401.36054421768705,404.1095890410959,406.89655172413796,409.72222222222223,412.58741258741264,415.4929577464789,418.4397163120567,421.42857142857144,424.46043165467626,427.53623188405805,430.6569343065693,433.82352941176475,437.037037037037,440.2985074626866,443.6090225563909,446.96969696969705,450.3816793893129,453.8461538461539,457.3643410852713,460.9375,464.56692913385825,468.25396825396825,472.,475.8064516129033,479.6747967479675,483.60655737704917,487.603305785124,491.6666666666667,495.7983193277311,500.00000000000006,504.27350427350433,508.6206896551724,513.0434782608696,517.5438596491227,522.1238938053098,526.7857142857142,531.5315315315316,536.3636363636364,541.2844036697248,546.2962962962962,551.4018691588785,556.6037735849056,561.904761904762,567.3076923076923,572.8155339805826,578.4313725490196,584.1584158415842,590. +290.,291.4572864321608,292.92929292929296,294.4162436548223,295.9183673469388,297.43589743589746,298.9690721649485,300.51813471502595,302.08333333333337,303.6649214659686,305.2631578947368,306.87830687830694,308.51063829787233,310.1604278074866,311.82795698924735,313.5135135135135,315.2173913043478,316.9398907103825,318.68131868131866,320.44198895027625,322.22222222222223,324.0223463687151,325.8426966292135,327.683615819209,329.54545454545456,331.4285714285714,333.3333333333333,335.26011560693644,337.2093023255814,339.1812865497076,341.1764705882353,343.1952662721894,345.23809523809524,347.30538922155694,349.3975903614458,351.5151515151515,353.6585365853658,355.8282208588957,358.02469135802465,360.24844720496895,362.5,364.77987421383654,367.08860759493666,369.42675159235665,371.79487179487177,374.19354838709677,376.6233766233766,379.08496732026146,381.5789473684211,384.1059602649007,386.66666666666663,389.26174496644296,391.8918918918919,394.5578231292517,397.2602739726027,400.,402.77777777777777,405.59440559440566,408.4507042253521,411.3475177304964,414.2857142857143,417.2661870503597,420.2898550724638,423.35766423357666,426.47058823529414,429.6296296296296,432.83582089552243,436.09022556390977,439.39393939393943,442.7480916030534,446.15384615384625,449.61240310077517,453.125,456.6929133858267,460.3174603174603,464.,467.741935483871,471.5447154471545,475.40983606557376,479.3388429752066,483.33333333333337,487.3949579831933,491.5254237288136,495.7264957264958,499.99999999999994,504.34782608695656,508.77192982456137,513.2743362831859,517.8571428571428,522.5225225225226,527.2727272727273,532.1100917431194,537.037037037037,542.0560747663552,547.1698113207547,552.3809523809525,557.6923076923076,563.1067961165048,568.6274509803922,574.2574257425742,580. +285.,286.4321608040201,287.8787878787879,289.34010152284264,290.81632653061223,292.3076923076923,293.8144329896908,295.3367875647669,296.875,298.42931937172773,300.,301.5873015873016,303.1914893617021,304.8128342245989,306.45161290322585,308.10810810810807,309.78260869565213,311.4754098360655,313.18681318681314,314.91712707182324,316.6666666666667,318.43575418994413,320.22471910112364,322.03389830508473,323.8636363636364,325.7142857142857,327.5862068965517,329.4797687861272,331.3953488372093,333.3333333333333,335.29411764705884,337.27810650887574,339.2857142857143,341.3173652694611,343.37349397590367,345.45454545454544,347.5609756097561,349.69325153374234,351.85185185185185,354.0372670807454,356.25,358.4905660377359,360.75949367088606,363.0573248407643,365.38461538461536,367.741935483871,370.12987012987014,372.54901960784315,375.,377.4834437086093,380.,382.5503355704698,385.1351351351351,387.7551020408163,390.4109589041096,393.1034482758621,395.8333333333333,398.60139860139867,401.4084507042254,404.2553191489361,407.14285714285717,410.0719424460431,413.0434782608696,416.05839416058393,419.11764705882354,422.2222222222222,425.3731343283582,428.57142857142856,431.81818181818187,435.11450381679384,438.46153846153857,441.86046511627904,445.3125,448.81889763779526,452.38095238095235,456.,459.6774193548387,463.4146341463414,467.2131147540984,471.07438016528926,475.,478.9915966386555,483.0508474576272,487.17948717948724,491.37931034482756,495.65217391304355,500.,504.424778761062,508.9285714285714,513.5135135135135,518.1818181818181,522.9357798165139,527.7777777777777,532.7102803738318,537.7358490566037,542.857142857143,548.076923076923,553.3980582524272,558.8235294117646,564.3564356435644,570. +280.,281.4070351758794,282.82828282828285,284.2639593908629,285.7142857142857,287.17948717948724,288.65979381443304,290.1554404145078,291.6666666666667,293.1937172774869,294.7368421052631,296.29629629629636,297.8723404255319,299.4652406417112,301.07526881720435,302.70270270270265,304.3478260869565,306.0109289617486,307.6923076923077,309.3922651933702,311.11111111111114,312.8491620111732,314.60674157303373,316.38418079096044,318.1818181818182,320.,321.8390804597701,323.6994219653179,325.58139534883725,327.48538011695905,329.4117647058824,331.36094674556216,333.3333333333333,335.3293413173653,337.3493975903615,339.3939393939394,341.4634146341463,343.55828220858893,345.679012345679,347.82608695652175,350.,352.20125786163527,354.4303797468354,356.68789808917194,358.97435897435895,361.2903225806451,363.6363636363636,366.01307189542484,368.42105263157896,370.8609271523179,373.3333333333333,375.83892617449663,378.3783783783784,380.95238095238096,383.5616438356164,386.2068965517242,388.88888888888886,391.6083916083916,394.3661971830986,397.1631205673758,400.,402.8776978417266,405.79710144927543,408.75912408759126,411.764705882353,414.8148148148148,417.91044776119406,421.05263157894734,424.2424242424243,427.48091603053433,430.76923076923083,434.1085271317829,437.5,440.94488188976374,444.4444444444444,448.,451.61290322580646,455.2845528455284,459.016393442623,462.8099173553719,466.6666666666667,470.5882352941177,474.5762711864407,478.6324786324787,482.7586206896551,486.9565217391305,491.2280701754386,495.57522123893807,499.99999999999994,504.50450450450455,509.09090909090907,513.7614678899083,518.5185185185185,523.3644859813085,528.3018867924528,533.3333333333335,538.4615384615385,543.6893203883495,549.0196078431372,554.4554455445544,560. +275.,276.38190954773864,277.77777777777777,279.18781725888323,280.6122448979592,282.0512820512821,283.5051546391753,284.9740932642487,286.45833333333337,287.9581151832461,289.4736842105263,291.00529100529104,292.5531914893617,294.1176470588235,295.69892473118284,297.29729729729723,298.9130434782609,300.54644808743166,302.19780219780216,303.86740331491717,305.55555555555554,307.2625698324022,308.9887640449438,310.73446327683615,312.5,314.2857142857143,316.0919540229885,317.91907514450867,319.7674418604651,321.6374269005848,323.5294117647059,325.4437869822485,327.38095238095235,329.3413173652695,331.3253012048193,333.3333333333333,335.3658536585366,337.4233128834356,339.5061728395062,341.6149068322982,343.75,345.91194968553464,348.1012658227848,350.3184713375796,352.56410256410254,354.83870967741933,357.14285714285717,359.47712418300654,361.8421052631579,364.23841059602654,366.66666666666663,369.1275167785235,371.6216216216216,374.14965986394554,376.71232876712327,379.3103448275862,381.94444444444446,384.61538461538464,387.32394366197184,390.0709219858156,392.8571428571429,395.68345323741005,398.55072463768124,401.45985401459853,404.4117647058824,407.4074074074074,410.44776119402985,413.5338345864661,416.66666666666674,419.84732824427476,423.07692307692315,426.3565891472868,429.6875,433.07086614173227,436.5079365079365,440.,443.5483870967742,447.1544715447154,450.81967213114757,454.54545454545456,458.33333333333337,462.18487394957987,466.10169491525426,470.08547008547015,474.13793103448273,478.26086956521743,482.45614035087715,486.7256637168142,491.07142857142856,495.49549549549556,500.,504.58715596330285,509.2592592592592,514.0186915887851,518.8679245283018,523.8095238095239,528.8461538461538,533.9805825242719,539.2156862745098,544.5544554455446,550. +270.,271.35678391959794,272.72727272727275,274.11167512690355,275.51020408163265,276.92307692307696,278.35051546391753,279.79274611398966,281.25,282.72251308900525,284.2105263157895,285.7142857142858,287.2340425531915,288.7700534759358,290.32258064516134,291.8918918918919,293.4782608695652,295.0819672131147,296.70329670329664,298.3425414364641,300.,301.67597765363126,303.37078651685397,305.08474576271186,306.81818181818187,308.57142857142856,310.34482758620686,312.13872832369947,313.95348837209303,315.7894736842105,317.64705882352945,319.52662721893495,321.42857142857144,323.3532934131737,325.30120481927713,327.2727272727273,329.2682926829268,331.28834355828224,333.3333333333333,335.40372670807454,337.5,339.622641509434,341.77215189873414,343.9490445859872,346.15384615384613,348.38709677419354,350.64935064935065,352.94117647058823,355.2631578947369,357.6158940397351,360.,362.41610738255036,364.86486486486484,367.3469387755102,369.86301369863014,372.4137931034483,375.,377.62237762237766,380.2816901408451,382.9787234042553,385.7142857142857,388.4892086330935,391.304347826087,394.16058394160586,397.05882352941177,400.,402.9850746268657,406.0150375939849,409.0909090909091,412.21374045801525,415.3846153846155,418.6046511627907,421.875,425.19685039370074,428.57142857142856,432.,435.48387096774195,439.0243902439024,442.62295081967216,446.2809917355372,450.,453.781512605042,457.6271186440678,461.5384615384616,465.5172413793103,469.5652173913044,473.6842105263158,477.8761061946903,482.1428571428571,486.48648648648657,490.9090909090909,495.4128440366973,499.99999999999994,504.6728971962617,509.43396226415086,514.2857142857143,519.2307692307692,524.2718446601942,529.4117647058823,534.6534653465346,540. +265.,266.33165829145725,267.67676767676767,269.0355329949238,270.40816326530614,271.7948717948718,273.1958762886598,274.6113989637306,276.0416666666667,277.4869109947644,278.9473684210526,280.42328042328046,281.9148936170213,283.4224598930481,284.94623655913983,286.48648648648646,288.04347826086956,289.6174863387978,291.2087912087912,292.8176795580111,294.44444444444446,296.0893854748603,297.75280898876406,299.43502824858757,301.1363636363637,302.85714285714283,304.5977011494253,306.3583815028902,308.13953488372096,309.94152046783626,311.7647058823529,313.6094674556213,315.4761904761905,317.3652694610779,319.27710843373495,321.21212121212125,323.1707317073171,325.15337423312883,327.16049382716045,329.19254658385097,331.25,333.33333333333337,335.44303797468353,337.57961783439487,339.7435897435897,341.93548387096774,344.15584415584414,346.4052287581699,348.68421052631584,350.99337748344374,353.3333333333333,355.7046979865772,358.1081081081081,360.5442176870748,363.01369863013696,365.51724137931035,368.05555555555554,370.62937062937067,373.23943661971833,375.886524822695,378.57142857142856,381.294964028777,384.0579710144928,386.86131386861314,389.7058823529412,392.59259259259255,395.52238805970154,398.49624060150376,401.51515151515156,404.5801526717557,407.6923076923078,410.85271317829455,414.0625,417.3228346456693,420.6349206349206,424.,427.4193548387097,430.8943089430894,434.42622950819674,438.01652892561987,441.6666666666667,445.3781512605042,449.1525423728814,452.991452991453,456.8965517241379,460.86956521739137,464.91228070175436,469.0265486725664,473.21428571428567,477.4774774774775,481.8181818181818,486.23853211009185,490.7407407407407,495.3271028037384,499.99999999999994,504.7619047619049,509.6153846153846,514.5631067961165,519.6078431372549,524.7524752475248,530. +260.,261.30653266331655,262.62626262626264,263.95939086294413,265.3061224489796,266.6666666666667,268.0412371134021,269.43005181347155,270.83333333333337,272.25130890052355,273.6842105263158,275.1322751322752,276.59574468085106,278.07486631016036,279.56989247311833,281.08108108108104,282.6086956521739,284.1530054644808,285.71428571428567,287.292817679558,288.8888888888889,290.50279329608935,292.13483146067415,293.7853107344633,295.4545454545455,297.1428571428571,298.85057471264366,300.57803468208095,302.3255813953489,304.093567251462,305.88235294117646,307.6923076923077,309.5238095238095,311.37724550898207,313.2530120481928,315.1515151515152,317.0731707317073,319.0184049079755,320.98765432098764,322.98136645962734,325.,327.04402515723274,329.1139240506329,331.2101910828025,333.3333333333333,335.48387096774195,337.6623376623377,339.8692810457516,342.10526315789474,344.37086092715236,346.66666666666663,348.99328859060404,351.35135135135135,353.74149659863946,356.16438356164383,358.62068965517244,361.1111111111111,363.6363636363637,366.1971830985916,368.7943262411347,371.42857142857144,374.1007194244604,376.8115942028986,379.5620437956204,382.3529411764706,385.18518518518516,388.05970149253733,390.97744360902254,393.939393939394,396.9465648854962,400.00000000000006,403.1007751937984,406.25,409.44881889763775,412.69841269841265,416.,419.35483870967744,422.7642276422764,426.2295081967213,429.7520661157025,433.33333333333337,436.97478991596637,440.67796610169495,444.44444444444446,448.27586206896547,452.1739130434783,456.14035087719293,460.1769911504425,464.2857142857143,468.46846846846853,472.7272727272727,477.0642201834863,481.4814814814814,485.981308411215,490.566037735849,495.2380952380953,499.99999999999994,504.8543689320388,509.8039215686274,514.8514851485148,520. +255.,256.28140703517585,257.5757575757576,258.88324873096445,260.2040816326531,261.53846153846155,262.88659793814435,264.2487046632125,265.625,267.0157068062827,268.42105263157896,269.8412698412699,271.27659574468083,272.7272727272727,274.1935483870968,275.6756756756756,277.17391304347825,278.6885245901639,280.2197802197802,281.76795580110496,283.33333333333337,284.9162011173184,286.5168539325843,288.135593220339,289.7727272727273,291.4285714285714,293.10344827586204,294.7976878612717,296.51162790697674,298.2456140350877,300.,301.7751479289941,303.57142857142856,305.38922155688624,307.22891566265065,309.0909090909091,310.9756097560975,312.8834355828221,314.8148148148148,316.77018633540376,318.75,320.7547169811321,322.7848101265822,324.84076433121015,326.9230769230769,329.0322580645161,331.16883116883116,333.3333333333333,335.5263157894737,337.74834437086093,340.,342.2818791946309,344.5945945945946,346.9387755102041,349.31506849315065,351.72413793103453,354.16666666666663,356.6433566433567,359.1549295774648,361.70212765957444,364.2857142857143,366.90647482014384,369.56521739130443,372.26277372262774,375.,377.77777777777777,380.5970149253732,383.45864661654133,386.36363636363643,389.3129770992366,392.3076923076924,395.3488372093023,398.4375,401.5748031496063,404.76190476190476,408.,411.2903225806452,414.6341463414634,418.0327868852459,421.4876033057851,425.,428.57142857142856,432.2033898305085,435.8974358974359,439.6551724137931,443.47826086956525,447.36842105263156,451.3274336283186,455.35714285714283,459.45945945945954,463.6363636363636,467.8899082568808,472.2222222222222,476.63551401869165,481.13207547169804,485.7142857142858,490.38461538461536,495.1456310679612,500.,504.9504950495049,510. +250.,251.25628140703515,252.52525252525254,253.80710659898475,255.10204081632654,256.41025641025647,257.7319587628866,259.0673575129534,260.4166666666667,261.78010471204186,263.1578947368421,264.5502645502646,265.9574468085106,267.37967914438497,268.8172043010753,270.2702702702702,271.73913043478257,273.22404371584696,274.7252747252747,276.24309392265195,277.77777777777777,279.3296089385475,280.8988764044944,282.4858757062147,284.0909090909091,285.7142857142857,287.35632183908046,289.01734104046244,290.69767441860466,292.39766081871346,294.11764705882354,295.85798816568047,297.6190476190476,299.40119760479047,301.20481927710847,303.03030303030306,304.8780487804878,306.7484662576687,308.641975308642,310.55900621118013,312.5,314.4654088050315,316.4556962025316,318.4713375796178,320.5128205128205,322.5806451612903,324.67532467532465,326.79738562091507,328.94736842105266,331.12582781456956,333.3333333333333,335.5704697986577,337.8378378378378,340.1360544217687,342.4657534246575,344.82758620689657,347.22222222222223,349.6503496503497,352.11267605633805,354.60992907801415,357.14285714285717,359.71223021582733,362.3188405797102,364.963503649635,367.64705882352945,370.3703703703703,373.13432835820896,375.9398496240601,378.78787878787887,381.6793893129771,384.6153846153847,387.5968992248062,390.625,393.70078740157476,396.8253968253968,400.,403.2258064516129,406.5040650406504,409.8360655737705,413.22314049586777,416.6666666666667,420.16806722689074,423.7288135593221,427.35042735042737,431.03448275862064,434.78260869565224,438.59649122807014,442.4778761061947,446.4285714285714,450.4504504504505,454.5454545454545,458.71559633027533,462.96296296296293,467.2897196261683,471.6981132075471,476.19047619047626,480.7692307692307,485.4368932038835,490.19607843137254,495.049504950495,500. +245.,246.23115577889445,247.4747474747475,248.73096446700507,250.,251.28205128205133,252.5773195876289,253.88601036269432,255.20833333333334,256.5445026178011,257.89473684210526,259.2592592592593,260.63829787234044,262.0320855614973,263.4408602150538,264.86486486486484,266.30434782608694,267.75956284153,269.2307692307692,270.7182320441989,272.22222222222223,273.74301675977654,275.28089887640454,276.8361581920904,278.40909090909093,280.,281.60919540229884,283.2369942196532,284.8837209302326,286.54970760233914,288.2352941176471,289.9408284023669,291.6666666666667,293.41317365269464,295.1807228915663,296.969696969697,298.780487804878,300.6134969325153,302.4691358024691,304.34782608695656,306.25,308.17610062893084,310.12658227848095,312.10191082802544,314.1025641025641,316.1290322580645,318.1818181818182,320.26143790849676,322.3684210526316,324.5033112582782,326.66666666666663,328.8590604026846,331.0810810810811,333.3333333333333,335.61643835616434,337.93103448275866,340.27777777777777,342.6573426573427,345.0704225352113,347.51773049645385,350.,352.51798561151077,355.072463768116,357.66423357664235,360.29411764705884,362.96296296296293,365.6716417910448,368.4210526315789,371.21212121212125,374.0458015267175,376.923076923077,379.84496124031006,382.8125,385.8267716535433,388.88888888888886,392.,395.16129032258067,398.3739837398374,401.6393442622951,404.9586776859504,408.33333333333337,411.7647058823529,415.25423728813564,418.8034188034188,422.41379310344826,426.0869565217392,429.82456140350877,433.6283185840708,437.49999999999994,441.4414414414415,445.45454545454544,449.5412844036698,453.70370370370364,457.9439252336449,462.2641509433962,466.66666666666674,471.15384615384613,475.7281553398058,480.3921568627451,485.1485148514851,490. +240.,241.20603015075375,242.42424242424244,243.65482233502536,244.89795918367346,246.1538461538462,247.42268041237116,248.70466321243526,250.00000000000003,251.3089005235602,252.6315789473684,253.968253968254,255.31914893617022,256.68449197860957,258.0645161290323,259.4594594594594,260.8695652173913,262.2950819672131,263.7362637362637,265.1933701657459,266.6666666666667,268.1564245810056,269.6629213483146,271.1864406779661,272.72727272727275,274.2857142857143,275.8620689655172,277.4566473988439,279.0697674418605,280.7017543859649,282.3529411764706,284.02366863905326,285.7142857142857,287.4251497005988,289.1566265060241,290.90909090909093,292.6829268292683,294.47852760736197,296.2962962962963,298.1366459627329,300.,301.8867924528302,303.79746835443035,305.7324840764331,307.6923076923077,309.6774193548387,311.68831168831167,313.72549019607845,315.7894736842105,317.88079470198676,320.,322.14765100671144,324.3243243243243,326.53061224489795,328.7671232876712,331.0344827586207,333.3333333333333,335.6643356643357,338.02816901408454,340.42553191489355,342.8571428571429,345.32374100719426,347.8260869565218,350.3649635036496,352.9411764705883,355.55555555555554,358.2089552238806,360.90225563909775,363.6363636363637,366.412213740458,369.2307692307693,372.09302325581393,375.,377.95275590551176,380.9523809523809,384.,387.0967741935484,390.2439024390244,393.44262295081967,396.6942148760331,400.,403.3613445378151,406.7796610169492,410.2564102564103,413.7931034482758,417.3913043478261,421.05263157894734,424.7787610619469,428.57142857142856,432.4324324324325,436.3636363636364,440.3669724770643,444.4444444444444,448.59813084112153,452.8301886792452,457.1428571428572,461.5384615384615,466.0194174757282,470.5882352941176,475.2475247524752,480. +235.,236.18090452261305,237.37373737373738,238.57868020304568,239.79591836734696,241.02564102564105,242.26804123711344,243.5233160621762,244.79166666666669,246.07329842931938,247.36842105263156,248.6772486772487,250.,251.33689839572187,252.68817204301078,254.054054054054,255.43478260869563,256.83060109289613,258.2417582417582,259.6685082872928,261.11111111111114,262.5698324022346,264.0449438202247,265.5367231638418,267.04545454545456,268.57142857142856,270.11494252873564,271.6763005780347,273.25581395348837,274.8538011695906,276.47058823529414,278.1065088757397,279.76190476190476,281.43712574850304,283.13253012048193,284.8484848484849,286.5853658536585,288.3435582822086,290.12345679012344,291.92546583850935,293.75,295.5974842767296,297.4683544303797,299.3630573248407,301.28205128205127,303.22580645161287,305.1948051948052,307.18954248366015,309.2105263157895,311.2582781456954,313.3333333333333,315.43624161073825,317.56756756756755,319.7278911564626,321.9178082191781,324.1379310344828,326.38888888888886,328.6713286713287,330.98591549295776,333.3333333333333,335.7142857142857,338.1294964028777,340.57971014492756,343.06569343065695,345.5882352941177,348.14814814814815,350.74626865671644,353.38345864661653,356.0606060606061,358.77862595419845,361.5384615384616,364.3410852713178,367.1875,370.0787401574803,373.015873015873,376.,379.03225806451616,382.1138211382114,385.24590163934425,388.4297520661157,391.6666666666667,394.9579831932773,398.30508474576277,401.70940170940173,405.17241379310343,408.69565217391306,412.2807017543859,415.92920353982305,419.6428571428571,423.42342342342346,427.27272727272725,431.1926605504588,435.18518518518516,439.25233644859816,443.3962264150943,447.6190476190477,451.9230769230769,456.31067961165047,460.78431372549016,465.34653465346537,470. +230.,231.15577889447235,232.32323232323233,233.50253807106597,234.69387755102042,235.89743589743594,237.1134020618557,238.34196891191712,239.58333333333334,240.83769633507853,242.1052631578947,243.38624338624342,244.68085106382978,245.98930481283418,247.31182795698928,248.64864864864862,250.,251.3661202185792,252.74725274725273,254.1436464088398,255.55555555555557,256.98324022346367,258.42696629213486,259.88700564971754,261.3636363636364,262.85714285714283,264.367816091954,265.89595375722547,267.4418604651163,269.00584795321635,270.5882352941177,272.18934911242604,273.8095238095238,275.4491017964072,277.1084337349398,278.7878787878788,280.4878048780488,282.2085889570552,283.9506172839506,285.7142857142857,287.5,289.30817610062894,291.1392405063291,292.99363057324837,294.87179487179486,296.7741935483871,298.7012987012987,300.65359477124184,302.63157894736844,304.635761589404,306.66666666666663,308.7248322147651,310.8108108108108,312.9251700680272,315.0684931506849,317.2413793103448,319.44444444444446,321.6783216783217,323.94366197183103,326.241134751773,328.57142857142856,330.9352517985611,333.33333333333337,335.7664233576642,338.2352941176471,340.7407407407407,343.2835820895523,345.8646616541353,348.48484848484856,351.1450381679389,353.8461538461539,356.5891472868217,359.375,362.2047244094488,365.07936507936506,368.,370.9677419354839,373.9837398373984,377.04918032786884,380.1652892561984,383.33333333333337,386.5546218487395,389.8305084745763,393.1623931623932,396.551724137931,400.00000000000006,403.50877192982455,407.07964601769913,410.71428571428567,414.41441441441447,418.1818181818182,422.0183486238533,425.92592592592587,429.90654205607484,433.9622641509433,438.0952380952382,442.30769230769226,446.6019417475728,450.9803921568627,455.44554455445547,460. +225.,226.13065326633165,227.27272727272728,228.4263959390863,229.59183673469389,230.7692307692308,231.95876288659795,233.16062176165806,234.37500000000003,235.6020942408377,236.8421052631579,238.09523809523813,239.36170212765958,240.64171122994648,241.93548387096777,243.2432432432432,244.56521739130434,245.90163934426226,247.2527472527472,248.61878453038676,250.,251.3966480446927,252.80898876404495,254.23728813559322,255.68181818181822,257.1428571428571,258.6206896551724,260.1156069364162,261.6279069767442,263.1578947368421,264.70588235294116,266.2721893491124,267.85714285714283,269.4610778443114,271.08433734939763,272.72727272727275,274.390243902439,276.07361963190186,277.77777777777777,279.50310559006215,281.25,283.01886792452837,284.81012658227843,286.624203821656,288.46153846153845,290.3225806451613,292.2077922077922,294.11764705882354,296.0526315789474,298.0132450331126,300.,302.013422818792,304.05405405405406,306.1224489795918,308.2191780821918,310.3448275862069,312.5,314.6853146853147,316.90140845070425,319.1489361702127,321.42857142857144,323.7410071942446,326.0869565217392,328.46715328467155,330.8823529411765,333.3333333333333,335.82089552238807,338.3458646616541,340.90909090909093,343.51145038167937,346.15384615384625,348.83720930232556,351.5625,354.3307086614173,357.1428571428571,360.,362.90322580645164,365.8536585365854,368.8524590163934,371.90082644628103,375.,378.1512605042017,381.35593220338984,384.61538461538464,387.9310344827586,391.304347826087,394.7368421052631,398.2300884955752,401.7857142857143,405.4054054054055,409.09090909090907,412.84403669724776,416.66666666666663,420.56074766355147,424.5283018867924,428.57142857142867,432.6923076923077,436.8932038834952,441.17647058823525,445.54455445544556,450. +220.,221.10552763819095,222.22222222222223,223.35025380710658,224.48979591836735,225.64102564102566,226.80412371134022,227.97927461139898,229.16666666666669,230.36649214659687,231.57894736842104,232.80423280423284,234.04255319148936,235.29411764705878,236.55913978494627,237.8378378378378,239.1304347826087,240.43715846994533,241.75824175824172,243.09392265193372,244.44444444444446,245.81005586592178,247.19101123595507,248.58757062146893,250.00000000000003,251.42857142857142,252.87356321839079,254.33526011560696,255.8139534883721,257.3099415204678,258.8235294117647,260.35502958579883,261.9047619047619,263.4730538922156,265.06024096385545,266.6666666666667,268.2926829268293,269.93865030674846,271.6049382716049,273.2919254658385,275.,276.72955974842773,278.4810126582278,280.25477707006365,282.05128205128204,283.8709677419355,285.7142857142857,287.58169934640523,289.47368421052636,291.3907284768212,293.3333333333333,295.3020134228188,297.2972972972973,299.31972789115645,301.3698630136986,303.448275862069,305.55555555555554,307.69230769230774,309.85915492957747,312.0567375886524,314.2857142857143,316.54676258992805,318.840579710145,321.1678832116788,323.5294117647059,325.9259259259259,328.3582089552239,330.8270676691729,333.33333333333337,335.8778625954198,338.4615384615385,341.08527131782944,343.75,346.4566929133858,349.20634920634916,352.,354.8387096774194,357.7235772357723,360.65573770491807,363.6363636363636,366.6666666666667,369.74789915966386,372.8813559322034,376.0683760683761,379.31034482758616,382.60869565217394,385.96491228070175,389.38053097345136,392.85714285714283,396.3963963963964,400.,403.6697247706423,407.40740740740733,411.2149532710281,415.0943396226415,419.04761904761915,423.07692307692304,427.18446601941747,431.37254901960785,435.64356435643566,440. +215.,216.08040201005022,217.17171717171718,218.2741116751269,219.3877551020408,220.51282051282055,221.6494845360825,222.79792746113992,223.95833333333334,225.13089005235602,226.3157894736842,227.51322751322755,228.72340425531914,229.9465240641711,231.18279569892476,232.4324324324324,233.69565217391303,234.9726775956284,236.26373626373623,237.5690607734807,238.88888888888889,240.22346368715083,241.5730337078652,242.93785310734464,244.31818181818184,245.7142857142857,247.1264367816092,248.5549132947977,250.00000000000003,251.46198830409355,252.94117647058823,254.43786982248523,255.95238095238096,257.4850299401198,259.03614457831327,260.6060606060606,262.1951219512195,263.8036809815951,265.4320987654321,267.08074534161494,268.75,270.4402515723271,272.15189873417717,273.8853503184713,275.64102564102564,277.4193548387097,279.2207792207792,281.0457516339869,282.89473684210526,284.76821192052984,286.66666666666663,288.59060402684565,290.5405405405405,292.5170068027211,294.52054794520546,296.55172413793105,298.6111111111111,300.69930069930075,302.81690140845075,304.9645390070922,307.14285714285717,309.3525179856115,311.59420289855075,313.86861313868616,316.1764705882353,318.5185185185185,320.8955223880597,323.30827067669173,325.7575757575758,328.2442748091603,330.76923076923083,333.3333333333333,335.9375,338.5826771653543,341.26984126984127,344.,346.77419354838713,349.5934959349593,352.45901639344265,355.3719008264463,358.33333333333337,361.34453781512605,364.40677966101697,367.52136752136755,370.6896551724138,373.91304347826093,377.1929824561403,380.53097345132744,383.9285714285714,387.38738738738743,390.9090909090909,394.49541284403676,398.1481481481481,401.8691588785047,405.6603773584905,409.5238095238096,413.46153846153845,417.4757281553398,421.5686274509804,425.74257425742576,430. +210.,211.05527638190952,212.12121212121212,213.1979695431472,214.28571428571428,215.38461538461542,216.49484536082477,217.61658031088086,218.75000000000003,219.89528795811518,221.05263157894737,222.22222222222226,223.40425531914894,224.5989304812834,225.80645161290326,227.027027027027,228.26086956521738,229.50819672131146,230.76923076923075,232.04419889502765,233.33333333333334,234.63687150837987,235.95505617977528,237.28813559322035,238.63636363636365,240.,241.37931034482756,242.77456647398847,244.18604651162792,245.6140350877193,247.05882352941177,248.52071005917162,250.,251.49700598802397,253.01204819277112,254.54545454545456,256.0975609756097,257.6687116564417,259.25925925925924,260.8695652173913,262.5,264.15094339622647,265.82278481012656,267.515923566879,269.2307692307692,270.96774193548384,272.72727272727275,274.5098039215686,276.3157894736842,278.1456953642384,280.,281.8791946308725,283.7837837837838,285.7142857142857,287.67123287671234,289.65517241379314,291.66666666666663,293.70629370629376,295.77464788732397,297.8723404255319,300.,302.158273381295,304.34782608695656,306.56934306569343,308.82352941176475,311.1111111111111,313.43283582089555,315.7894736842105,318.18181818181824,320.6106870229007,323.07692307692315,325.5813953488372,328.125,330.7086614173228,333.3333333333333,336.,338.7096774193549,341.4634146341463,344.26229508196724,347.10743801652893,350.,352.94117647058823,355.93220338983053,358.974358974359,362.06896551724134,365.21739130434787,368.4210526315789,371.6814159292036,375.,378.37837837837844,381.8181818181818,385.32110091743124,388.88888888888886,392.52336448598135,396.2264150943396,400.00000000000006,403.8461538461538,407.7669902912621,411.7647058823529,415.84158415841586,420. +205.,206.03015075376882,207.07070707070707,208.1218274111675,209.18367346938777,210.25641025641028,211.34020618556704,212.43523316062178,213.54166666666669,214.65968586387436,215.78947368421052,216.93121693121697,218.08510638297872,219.2513368983957,220.43010752688176,221.62162162162159,222.82608695652172,224.0437158469945,225.27472527472526,226.5193370165746,227.7777777777778,229.0502793296089,230.3370786516854,231.63841807909606,232.95454545454547,234.28571428571428,235.63218390804596,236.99421965317921,238.37209302325581,239.76608187134502,241.1764705882353,242.60355029585799,244.04761904761904,245.50898203592817,246.98795180722894,248.4848484848485,250.,251.53374233128835,253.0864197530864,254.65838509316774,256.25,257.86163522012583,259.4936708860759,261.14649681528664,262.8205128205128,264.51612903225805,266.23376623376623,267.9738562091503,269.7368421052632,271.52317880794703,273.3333333333333,275.1677852348993,277.02702702702703,278.9115646258503,280.82191780821915,282.7586206896552,284.72222222222223,286.7132867132867,288.7323943661972,290.7801418439716,292.8571428571429,294.9640287769784,297.10144927536237,299.2700729927007,301.47058823529414,303.7037037037037,305.97014925373134,308.2706766917293,310.6060606060606,312.9770992366412,315.3846153846155,317.82945736434107,320.3125,322.8346456692913,325.39682539682536,328.,330.6451612903226,333.3333333333333,336.0655737704918,338.8429752066116,341.6666666666667,344.5378151260504,347.4576271186441,350.42735042735046,353.44827586206895,356.5217391304348,359.64912280701753,362.83185840707966,366.07142857142856,369.3693693693694,372.7272727272727,376.14678899082577,379.62962962962956,383.177570093458,386.79245283018867,390.47619047619054,394.23076923076917,398.05825242718447,401.96078431372547,405.94059405940595,410. +200.,201.00502512562812,202.02020202020202,203.0456852791878,204.08163265306123,205.12820512820517,206.1855670103093,207.25388601036272,208.33333333333334,209.4240837696335,210.52631578947367,211.64021164021167,212.7659574468085,213.903743315508,215.05376344086025,216.2162162162162,217.39130434782606,218.57923497267757,219.78021978021977,220.99447513812157,222.22222222222223,223.46368715083798,224.71910112359552,225.98870056497177,227.27272727272728,228.57142857142856,229.88505747126436,231.21387283236996,232.55813953488374,233.91812865497076,235.29411764705884,236.68639053254438,238.0952380952381,239.52095808383237,240.96385542168676,242.42424242424244,243.90243902439025,245.39877300613497,246.91358024691357,248.44720496894413,250.,251.5723270440252,253.1645569620253,254.77707006369425,256.4102564102564,258.06451612903226,259.7402597402597,261.437908496732,263.15789473684214,264.90066225165566,266.66666666666663,268.4563758389262,270.27027027027026,272.10884353741494,273.972602739726,275.86206896551727,277.77777777777777,279.72027972027973,281.69014084507046,283.6879432624113,285.7142857142857,287.76978417266184,289.8550724637682,291.97080291970804,294.11764705882354,296.2962962962963,298.5074626865672,300.7518796992481,303.03030303030306,305.34351145038164,307.69230769230774,310.07751937984494,312.5,314.9606299212598,317.46031746031747,320.,322.58064516129036,325.2032520325203,327.8688524590164,330.57851239669424,333.33333333333337,336.1344537815126,338.98305084745766,341.8803418803419,344.8275862068965,347.82608695652175,350.8771929824561,353.98230088495575,357.1428571428571,360.3603603603604,363.6363636363636,366.97247706422024,370.3703703703703,373.8317757009346,377.3584905660377,380.952380952381,384.6153846153846,388.3495145631068,392.156862745098,396.03960396039605,400. +195.,195.97989949748742,196.96969696969697,197.9695431472081,198.9795918367347,200.00000000000003,201.03092783505156,202.07253886010366,203.12500000000003,204.18848167539267,205.26315789473682,206.34920634920638,207.4468085106383,208.5561497326203,209.67741935483875,210.81081081081078,211.95652173913044,213.11475409836063,214.28571428571425,215.46961325966853,216.66666666666669,217.87709497206703,219.10112359550564,220.33898305084747,221.59090909090912,222.85714285714283,224.13793103448273,225.4335260115607,226.74418604651163,228.07017543859646,229.41176470588235,230.76923076923077,232.14285714285714,233.53293413173654,234.9397590361446,236.36363636363637,237.8048780487805,239.2638036809816,240.74074074074073,242.23602484472053,243.75,245.28301886792457,246.83544303797467,248.4076433121019,249.99999999999997,251.61290322580643,253.24675324675326,254.90196078431373,256.5789473684211,258.27814569536423,260.,261.74496644295306,263.5135135135135,265.3061224489796,267.12328767123284,268.9655172413793,270.8333333333333,272.72727272727275,274.6478873239437,276.59574468085106,278.57142857142856,280.57553956834533,282.60869565217394,284.6715328467153,286.764705882353,288.88888888888886,291.044776119403,293.2330827067669,295.4545454545455,297.70992366412213,300.00000000000006,302.3255813953488,304.6875,307.0866141732283,309.5238095238095,312.,314.5161290322581,317.0731707317073,319.672131147541,322.3140495867769,325.,327.7310924369748,330.5084745762712,333.33333333333337,336.2068965517241,339.13043478260875,342.10526315789474,345.1327433628319,348.21428571428567,351.3513513513514,354.54545454545456,357.7981651376147,361.1111111111111,364.4859813084113,367.92452830188677,371.4285714285715,374.99999999999994,378.6407766990291,382.35294117647055,386.13861386138615,390. +190.,190.95477386934672,191.91919191919195,192.89340101522842,193.87755102040816,194.8717948717949,195.87628865979383,196.89119170984458,197.91666666666669,198.95287958115182,200.,201.0582010582011,202.12765957446808,203.2085561497326,204.30107526881724,205.40540540540536,206.52173913043478,207.6502732240437,208.79120879120876,209.94475138121547,211.11111111111111,212.29050279329607,213.48314606741576,214.68926553672316,215.90909090909093,217.14285714285714,218.39080459770113,219.65317919075147,220.93023255813955,222.2222222222222,223.52941176470588,224.85207100591717,226.19047619047618,227.54491017964074,228.91566265060243,230.3030303030303,231.7073170731707,233.12883435582822,234.5679012345679,236.02484472049693,237.5,238.99371069182394,240.506329113924,242.03821656050954,243.58974358974356,245.16129032258064,246.75324675324674,248.36601307189542,250.00000000000003,251.65562913907286,253.33333333333331,255.03355704697987,256.7567567567568,258.5034013605442,260.2739726027397,262.0689655172414,263.88888888888886,265.73426573426576,267.6056338028169,269.50354609929076,271.42857142857144,273.38129496402877,275.36231884057975,277.37226277372264,279.4117647058824,281.48148148148147,283.5820895522388,285.7142857142857,287.87878787878793,290.07633587786256,292.3076923076924,294.5736434108527,296.875,299.2125984251968,301.58730158730157,304.,306.45161290322585,308.9430894308943,311.4754098360656,314.0495867768595,316.6666666666667,319.327731092437,322.0338983050848,324.7863247863248,327.5862068965517,330.4347826086957,333.3333333333333,336.283185840708,339.2857142857143,342.34234234234236,345.45454545454544,348.62385321100925,351.8518518518518,355.1401869158879,358.4905660377358,361.904761904762,365.38461538461536,368.93203883495147,372.54901960784315,376.23762376237624,380. +185.,185.92964824120602,186.8686868686869,187.8172588832487,188.77551020408163,189.74358974358978,190.7216494845361,191.70984455958552,192.70833333333334,193.717277486911,194.73684210526315,195.7671957671958,196.80851063829786,197.86096256684488,198.92473118279574,199.99999999999997,201.08695652173913,202.18579234972677,203.29670329670327,204.41988950276243,205.55555555555557,206.7039106145251,207.86516853932585,209.03954802259886,210.22727272727275,211.42857142857142,212.64367816091954,213.87283236994222,215.11627906976744,216.37426900584794,217.64705882352942,218.93491124260356,220.23809523809524,221.55688622754494,222.89156626506025,224.24242424242425,225.60975609756096,226.99386503067484,228.39506172839504,229.81366459627333,231.25,232.7044025157233,234.17721518987338,235.66878980891718,237.17948717948715,238.70967741935482,240.25974025974025,241.83006535947712,243.42105263157896,245.03311258278148,246.66666666666666,248.32214765100673,250.,251.70068027210883,253.42465753424656,255.17241379310346,256.94444444444446,258.7412587412588,260.5633802816902,262.41134751773046,264.2857142857143,266.1870503597122,268.11594202898556,270.0729927007299,272.05882352941177,274.0740740740741,276.11940298507466,278.1954887218045,280.30303030303037,282.44274809160305,284.6153846153847,286.8217054263566,289.0625,291.33858267716533,293.6507936507936,296.,298.3870967741936,300.8130081300813,303.27868852459017,305.78512396694214,308.33333333333337,310.92436974789916,313.55932203389835,316.2393162393163,318.9655172413793,321.7391304347826,324.5614035087719,327.4336283185841,330.35714285714283,333.33333333333337,336.3636363636364,339.4495412844037,342.59259259259255,345.79439252336454,349.05660377358487,352.38095238095246,355.7692307692307,359.22330097087377,362.7450980392157,366.33663366336634,370. +180.,180.9045226130653,181.81818181818184,182.74111675126903,183.67346938775512,184.61538461538464,185.56701030927837,186.52849740932646,187.5,188.48167539267016,189.4736842105263,190.4761904761905,191.48936170212767,192.51336898395718,193.54838709677423,194.59459459459455,195.65217391304347,196.7213114754098,197.80219780219778,198.8950276243094,200.,201.11731843575419,202.24719101123597,203.38983050847457,204.54545454545456,205.7142857142857,206.8965517241379,208.09248554913296,209.30232558139537,210.52631578947367,211.76470588235296,213.01775147928996,214.28571428571428,215.56886227544913,216.8674698795181,218.1818181818182,219.5121951219512,220.8588957055215,222.2222222222222,223.60248447204972,225.,226.41509433962267,227.84810126582275,229.29936305732483,230.76923076923075,232.25806451612902,233.76623376623377,235.29411764705884,236.84210526315792,238.41059602649008,240.,241.61073825503357,243.24324324324323,244.89795918367346,246.5753424657534,248.27586206896552,250.,251.7482517482518,253.5211267605634,255.3191489361702,257.14285714285717,258.9928057553957,260.86956521739137,262.77372262773724,264.7058823529412,266.66666666666663,268.65671641791045,270.6766917293233,272.72727272727275,274.8091603053435,276.92307692307696,279.06976744186045,281.25,283.4645669291338,285.7142857142857,288.,290.32258064516134,292.6829268292683,295.08196721311475,297.5206611570248,300.,302.52100840336135,305.0847457627119,307.69230769230774,310.34482758620686,313.0434782608696,315.7894736842105,318.5840707964602,321.4285714285714,324.3243243243244,327.27272727272725,330.2752293577982,333.3333333333333,336.44859813084116,339.62264150943395,342.8571428571429,346.15384615384613,349.5145631067961,352.94117647058823,356.43564356435644,360. +175.,175.8793969849246,176.7676767676768,177.66497461928932,178.57142857142858,179.4871794871795,180.41237113402065,181.34715025906738,182.29166666666669,183.2460732984293,184.21052631578945,185.18518518518522,186.17021276595744,187.16577540106948,188.1720430107527,189.18918918918916,190.2173913043478,191.25683060109287,192.3076923076923,193.37016574585635,194.44444444444446,195.53072625698323,196.6292134831461,197.74011299435028,198.86363636363637,200.,201.1494252873563,202.3121387283237,203.48837209302326,204.6783625730994,205.88235294117646,207.10059171597635,208.33333333333334,209.5808383233533,210.84337349397592,212.12121212121212,213.41463414634146,214.7239263803681,216.04938271604937,217.39130434782612,218.75,220.12578616352204,221.51898734177212,222.92993630573247,224.35897435897434,225.80645161290323,227.27272727272728,228.75816993464053,230.26315789473685,231.78807947019868,233.33333333333331,234.8993288590604,236.48648648648648,238.09523809523807,239.72602739726025,241.37931034482762,243.05555555555554,244.75524475524477,246.47887323943664,248.2269503546099,250.,251.79856115107913,253.62318840579715,255.47445255474452,257.3529411764706,259.25925925925924,261.1940298507463,263.1578947368421,265.1515151515152,267.17557251908397,269.2307692307693,271.3178294573643,273.4375,275.59055118110234,277.77777777777777,280.,282.2580645161291,284.5528455284553,286.88524590163934,289.25619834710744,291.6666666666667,294.11764705882354,296.6101694915254,299.1452991452992,301.7241379310345,304.34782608695656,307.0175438596491,309.7345132743363,312.5,315.31531531531533,318.1818181818182,321.1009174311927,324.074074074074,327.1028037383178,330.18867924528297,333.33333333333337,336.5384615384615,339.80582524271847,343.1372549019608,346.53465346534654,350. +170.,170.8542713567839,171.71717171717174,172.58883248730965,173.46938775510205,174.3589743589744,175.25773195876292,176.16580310880832,177.08333333333334,178.0104712041885,178.94736842105263,179.89417989417993,180.85106382978722,181.81818181818178,182.7956989247312,183.78378378378375,184.78260869565216,185.79234972677594,186.8131868131868,187.84530386740332,188.88888888888889,189.94413407821227,191.01123595505618,192.090395480226,193.1818181818182,194.28571428571428,195.4022988505747,196.53179190751447,197.67441860465118,198.83040935672514,200.,201.18343195266274,202.38095238095238,203.5928143712575,204.81927710843377,206.06060606060606,207.3170731707317,208.58895705521473,209.87654320987653,211.18012422360252,212.5,213.8364779874214,215.1898734177215,216.5605095541401,217.94871794871793,219.3548387096774,220.77922077922076,222.22222222222223,223.6842105263158,225.1655629139073,226.66666666666666,228.18791946308727,229.7297297297297,231.2925170068027,232.8767123287671,234.48275862068968,236.11111111111111,237.7622377622378,239.43661971830988,241.13475177304963,242.85714285714286,244.6043165467626,246.37681159420293,248.17518248175182,250.00000000000003,251.85185185185185,253.7313432835821,255.6390977443609,257.5757575757576,259.5419847328244,261.5384615384616,263.5658914728682,265.625,267.7165354330708,269.8412698412698,272.,274.19354838709677,276.4227642276423,278.6885245901639,280.9917355371901,283.33333333333337,285.7142857142857,288.135593220339,290.59829059829065,293.10344827586204,295.6521739130435,298.2456140350877,300.8849557522124,303.57142857142856,306.30630630630634,309.09090909090907,311.9266055045872,314.8148148148148,317.7570093457944,320.75471698113205,323.80952380952385,326.9230769230769,330.09708737864077,333.3333333333333,336.63366336633663,340. +165.,165.8291457286432,166.66666666666669,167.51269035532994,168.3673469387755,169.23076923076925,170.10309278350516,170.98445595854923,171.875,172.77486910994764,173.68421052631578,174.60317460317464,175.53191489361703,176.4705882352941,177.4193548387097,178.37837837837836,179.3478260869565,180.327868852459,181.31868131868129,182.32044198895028,183.33333333333334,184.35754189944132,185.3932584269663,186.4406779661017,187.50000000000003,188.57142857142856,189.65517241379308,190.75144508670522,191.86046511627907,192.98245614035088,194.11764705882354,195.2662721893491,196.42857142857142,197.6047904191617,198.7951807228916,200.,201.21951219512195,202.45398773006136,203.7037037037037,204.96894409937892,206.25,207.54716981132077,208.86075949367086,210.19108280254775,211.53846153846152,212.90322580645162,214.28571428571428,215.68627450980392,217.10526315789474,218.5430463576159,220.,221.4765100671141,222.97297297297297,224.48979591836735,226.02739726027397,227.58620689655174,229.16666666666666,230.7692307692308,232.39436619718313,234.04255319148933,235.71428571428572,237.41007194244602,239.13043478260875,240.87591240875912,242.64705882352942,244.44444444444443,246.26865671641792,248.12030075187968,250.00000000000003,251.90839694656486,253.8461538461539,255.81395348837208,257.8125,259.84251968503935,261.90476190476187,264.,266.1290322580645,268.2926829268293,270.4918032786885,272.72727272727275,275.,277.3109243697479,279.66101694915255,282.0512820512821,284.48275862068965,286.95652173913044,289.4736842105263,292.0353982300885,294.6428571428571,297.29729729729735,300.,302.75229357798173,305.55555555555554,308.41121495327104,311.3207547169811,314.28571428571433,317.30769230769226,320.3883495145631,323.52941176470586,326.73267326732673,330. +160.,160.8040201005025,161.61616161616163,162.43654822335026,163.26530612244898,164.10256410256412,164.94845360824743,165.80310880829018,166.66666666666669,167.5392670157068,168.42105263157893,169.31216931216935,170.2127659574468,171.1229946524064,172.0430107526882,172.97297297297294,173.91304347826087,174.86338797814204,175.8241758241758,176.79558011049724,177.77777777777777,178.7709497206704,179.77528089887642,180.7909604519774,181.81818181818184,182.85714285714283,183.90804597701148,184.97109826589596,186.046511627907,187.13450292397658,188.23529411764707,189.3491124260355,190.47619047619048,191.61676646706587,192.7710843373494,193.93939393939394,195.1219512195122,196.31901840490798,197.53086419753086,198.75776397515529,200.,201.25786163522014,202.53164556962022,203.8216560509554,205.1282051282051,206.4516129032258,207.7922077922078,209.15032679738562,210.5263157894737,211.9205298013245,213.33333333333331,214.76510067114094,216.2162162162162,217.68707482993196,219.17808219178082,220.6896551724138,222.22222222222223,223.7762237762238,225.35211267605635,226.95035460992904,228.57142857142858,230.2158273381295,231.88405797101453,233.57664233576642,235.29411764705884,237.037037037037,238.80597014925374,240.6015037593985,242.42424242424246,244.27480916030532,246.1538461538462,248.06201550387595,250.,251.96850393700785,253.96825396825395,256.,258.06451612903226,260.1626016260162,262.29508196721315,264.4628099173554,266.6666666666667,268.9075630252101,271.1864406779661,273.50427350427356,275.8620689655172,278.26086956521743,280.7017543859649,283.18584070796464,285.71428571428567,288.2882882882883,290.9090909090909,293.5779816513762,296.29629629629625,299.0654205607477,301.88679245283015,304.7619047619048,307.6923076923077,310.6796116504854,313.7254901960784,316.83168316831683,320. +155.,155.7788944723618,156.56565656565658,157.36040609137055,158.16326530612244,158.974358974359,159.7938144329897,160.62176165803112,161.45833333333334,162.30366492146598,163.1578947368421,164.02116402116405,164.89361702127658,165.7754010695187,166.66666666666669,167.56756756756755,168.47826086956522,169.3989071038251,170.3296703296703,171.2707182320442,172.22222222222223,173.18435754189943,174.15730337078654,175.14124293785312,176.13636363636365,177.14285714285714,178.16091954022988,179.19075144508673,180.2325581395349,181.28654970760232,182.3529411764706,183.4319526627219,184.52380952380952,185.62874251497007,186.74698795180726,187.87878787878788,189.02439024390245,190.1840490797546,191.35802469135803,192.54658385093168,193.75,194.96855345911953,196.2025316455696,197.45222929936304,198.7179487179487,200.,201.2987012987013,202.6143790849673,203.94736842105266,205.29801324503313,206.66666666666666,208.0536912751678,209.45945945945945,210.8843537414966,212.32876712328766,213.79310344827587,215.27777777777777,216.7832167832168,218.3098591549296,219.85815602836877,221.42857142857144,223.02158273381295,224.6376811594203,226.27737226277372,227.94117647058826,229.62962962962962,231.34328358208955,233.08270676691728,234.84848484848487,236.64122137404578,238.4615384615385,240.31007751937983,242.1875,244.09448818897636,246.03174603174602,248.,250.00000000000003,252.03252032520325,254.0983606557377,256.198347107438,258.33333333333337,260.5042016806723,262.7118644067797,264.95726495726495,267.2413793103448,269.5652173913044,271.9298245614035,274.3362831858407,276.7857142857143,279.2792792792793,281.8181818181818,284.4036697247707,287.037037037037,289.71962616822435,292.45283018867923,295.2380952380953,298.07692307692304,300.97087378640776,303.921568627451,306.9306930693069,310. +150.,150.7537688442211,151.51515151515153,152.28426395939084,153.06122448979593,153.84615384615387,154.63917525773198,155.44041450777203,156.25,157.06806282722513,157.89473684210526,158.73015873015876,159.5744680851064,160.427807486631,161.29032258064518,162.16216216216213,163.04347826086956,163.93442622950818,164.83516483516482,165.74585635359117,166.66666666666669,167.59776536312847,168.53932584269666,169.49152542372883,170.45454545454547,171.42857142857142,172.41379310344826,173.41040462427748,174.4186046511628,175.43859649122805,176.47058823529412,177.5147928994083,178.57142857142858,179.64071856287427,180.72289156626508,181.8181818181818,182.9268292682927,184.04907975460122,185.18518518518516,186.33540372670808,187.5,188.6792452830189,189.87341772151896,191.08280254777068,192.3076923076923,193.54838709677418,194.80519480519482,196.07843137254903,197.3684210526316,198.67549668874173,200.,201.34228187919464,202.7027027027027,204.0816326530612,205.4794520547945,206.89655172413794,208.33333333333331,209.79020979020981,211.26760563380284,212.76595744680847,214.28571428571428,215.82733812949638,217.39130434782612,218.97810218978103,220.58823529411765,222.2222222222222,223.8805970149254,225.56390977443607,227.2727272727273,229.00763358778624,230.7692307692308,232.5581395348837,234.375,236.22047244094486,238.09523809523807,240.,241.93548387096777,243.90243902439025,245.9016393442623,247.93388429752068,250.,252.10084033613447,254.23728813559325,256.4102564102564,258.6206896551724,260.8695652173913,263.1578947368421,265.4867256637168,267.85714285714283,270.2702702702703,272.7272727272727,275.2293577981652,277.77777777777777,280.373831775701,283.01886792452825,285.7142857142858,288.46153846153845,291.2621359223301,294.11764705882354,297.029702970297,300. +145.,145.7286432160804,146.46464646464648,147.20812182741116,147.9591836734694,148.71794871794873,149.48453608247425,150.25906735751298,151.04166666666669,151.8324607329843,152.6315789473684,153.43915343915347,154.25531914893617,155.0802139037433,155.91397849462368,156.75675675675674,157.6086956521739,158.46994535519124,159.34065934065933,160.22099447513813,161.11111111111111,162.01117318435755,162.92134831460675,163.8418079096045,164.77272727272728,165.7142857142857,166.66666666666666,167.63005780346822,168.6046511627907,169.5906432748538,170.58823529411765,171.5976331360947,172.61904761904762,173.65269461077847,174.6987951807229,175.75757575757575,176.8292682926829,177.91411042944785,179.01234567901233,180.12422360248448,181.25,182.38993710691827,183.54430379746833,184.71337579617833,185.89743589743588,187.09677419354838,188.3116883116883,189.54248366013073,190.78947368421055,192.05298013245036,193.33333333333331,194.63087248322148,195.94594594594594,197.27891156462584,198.63013698630135,200.,201.38888888888889,202.79720279720283,204.22535211267606,205.6737588652482,207.14285714285714,208.63309352517985,210.1449275362319,211.67883211678833,213.23529411764707,214.8148148148148,216.41791044776122,218.04511278195488,219.69696969696972,221.3740458015267,223.07692307692312,224.80620155038758,226.5625,228.34645669291336,230.15873015873015,232.,233.8709677419355,235.77235772357724,237.70491803278688,239.6694214876033,241.66666666666669,243.69747899159665,245.7627118644068,247.8632478632479,249.99999999999997,252.17391304347828,254.38596491228068,256.63716814159295,258.9285714285714,261.2612612612613,263.6363636363636,266.0550458715597,268.5185185185185,271.0280373831776,273.58490566037733,276.19047619047626,278.8461538461538,281.5533980582524,284.3137254901961,287.1287128712871,290. +140.,140.7035175879397,141.41414141414143,142.13197969543145,142.85714285714286,143.58974358974362,144.32989690721652,145.0777202072539,145.83333333333334,146.59685863874344,147.36842105263156,148.14814814814818,148.93617021276594,149.7326203208556,150.53763440860217,151.35135135135133,152.17391304347825,153.0054644808743,153.84615384615384,154.6961325966851,155.55555555555557,156.4245810055866,157.30337078651687,158.19209039548022,159.0909090909091,160.,160.91954022988506,161.84971098265896,162.79069767441862,163.74269005847952,164.7058823529412,165.68047337278108,166.66666666666666,167.66467065868264,168.67469879518075,169.6969696969697,170.73170731707316,171.77914110429447,172.8395061728395,173.91304347826087,175.,176.10062893081763,177.2151898734177,178.34394904458597,179.48717948717947,180.64516129032256,181.8181818181818,183.00653594771242,184.21052631578948,185.43046357615896,186.66666666666666,187.91946308724832,189.1891891891892,190.47619047619048,191.7808219178082,193.1034482758621,194.44444444444443,195.8041958041958,197.1830985915493,198.5815602836879,200.,201.4388489208633,202.89855072463772,204.37956204379563,205.8823529411765,207.4074074074074,208.95522388059703,210.52631578947367,212.12121212121215,213.74045801526717,215.38461538461542,217.05426356589146,218.75,220.47244094488187,222.2222222222222,224.,225.80645161290323,227.6422764227642,229.5081967213115,231.40495867768595,233.33333333333334,235.29411764705884,237.28813559322035,239.31623931623935,241.37931034482756,243.47826086956525,245.6140350877193,247.78761061946904,249.99999999999997,252.25225225225228,254.54545454545453,256.88073394495416,259.25925925925924,261.68224299065423,264.1509433962264,266.66666666666674,269.2307692307692,271.84466019417476,274.5098039215686,277.2277227722772,280. +135.,135.67839195979897,136.36363636363637,137.05583756345177,137.75510204081633,138.46153846153848,139.17525773195877,139.89637305699483,140.625,141.36125654450262,142.10526315789474,142.8571428571429,143.61702127659575,144.3850267379679,145.16129032258067,145.94594594594594,146.7391304347826,147.54098360655735,148.35164835164832,149.17127071823205,150.,150.83798882681563,151.68539325842698,152.54237288135593,153.40909090909093,154.28571428571428,155.17241379310343,156.06936416184973,156.97674418604652,157.89473684210526,158.82352941176472,159.76331360946747,160.71428571428572,161.67664670658684,162.65060240963857,163.63636363636365,164.6341463414634,165.64417177914112,166.66666666666666,167.70186335403727,168.75,169.811320754717,170.88607594936707,171.9745222929936,173.07692307692307,174.19354838709677,175.32467532467533,176.47058823529412,177.63157894736844,178.80794701986756,180.,181.20805369127518,182.43243243243242,183.6734693877551,184.93150684931507,186.20689655172416,187.5,188.81118881118883,190.14084507042256,191.48936170212764,192.85714285714286,194.24460431654674,195.6521739130435,197.08029197080293,198.52941176470588,200.,201.49253731343285,203.00751879699246,204.54545454545456,206.10687022900763,207.69230769230774,209.30232558139534,210.9375,212.59842519685037,214.28571428571428,216.,217.74193548387098,219.5121951219512,221.31147540983608,223.1404958677686,225.,226.890756302521,228.8135593220339,230.7692307692308,232.75862068965515,234.7826086956522,236.8421052631579,238.93805309734515,241.07142857142856,243.24324324324328,245.45454545454544,247.70642201834866,249.99999999999997,252.33644859813086,254.71698113207543,257.14285714285717,259.6153846153846,262.1359223300971,264.70588235294116,267.3267326732673,270. +130.,130.65326633165827,131.31313131313132,131.97969543147207,132.6530612244898,133.33333333333334,134.02061855670104,134.71502590673578,135.41666666666669,136.12565445026178,136.8421052631579,137.5661375661376,138.29787234042553,139.03743315508018,139.78494623655916,140.54054054054052,141.30434782608694,142.0765027322404,142.85714285714283,143.646408839779,144.44444444444446,145.25139664804468,146.06741573033707,146.89265536723164,147.72727272727275,148.57142857142856,149.42528735632183,150.28901734104048,151.16279069767444,152.046783625731,152.94117647058823,153.84615384615384,154.76190476190476,155.68862275449104,156.6265060240964,157.5757575757576,158.53658536585365,159.50920245398774,160.49382716049382,161.49068322981367,162.5,163.52201257861637,164.55696202531644,165.60509554140125,166.66666666666666,167.74193548387098,168.83116883116884,169.9346405228758,171.05263157894737,172.18543046357618,173.33333333333331,174.49664429530202,175.67567567567568,176.87074829931973,178.08219178082192,179.31034482758622,180.55555555555554,181.81818181818184,183.0985915492958,184.39716312056734,185.71428571428572,187.0503597122302,188.4057971014493,189.7810218978102,191.1764705882353,192.59259259259258,194.02985074626866,195.48872180451127,196.969696969697,198.4732824427481,200.00000000000003,201.5503875968992,203.125,204.72440944881888,206.34920634920633,208.,209.67741935483872,211.3821138211382,213.11475409836066,214.87603305785126,216.66666666666669,218.48739495798318,220.33898305084747,222.22222222222223,224.13793103448273,226.08695652173915,228.07017543859646,230.08849557522126,232.14285714285714,234.23423423423426,236.36363636363635,238.53211009174316,240.7407407407407,242.9906542056075,245.2830188679245,247.61904761904765,249.99999999999997,252.4271844660194,254.9019607843137,257.4257425742574,260. +125.,125.62814070351757,126.26262626262627,126.90355329949237,127.55102040816327,128.20512820512823,128.8659793814433,129.5336787564767,130.20833333333334,130.89005235602093,131.57894736842104,132.2751322751323,132.9787234042553,133.68983957219248,134.40860215053766,135.1351351351351,135.86956521739128,136.61202185792348,137.36263736263734,138.12154696132598,138.88888888888889,139.66480446927375,140.4494382022472,141.24293785310735,142.04545454545456,142.85714285714286,143.67816091954023,144.50867052023122,145.34883720930233,146.19883040935673,147.05882352941177,147.92899408284023,148.8095238095238,149.70059880239523,150.60240963855424,151.51515151515153,152.4390243902439,153.37423312883436,154.320987654321,155.27950310559007,156.25,157.23270440251574,158.2278481012658,159.2356687898089,160.25641025641025,161.29032258064515,162.33766233766232,163.39869281045753,164.47368421052633,165.56291390728478,166.66666666666666,167.78523489932886,168.9189189189189,170.06802721088434,171.23287671232876,172.41379310344828,173.61111111111111,174.82517482517486,176.05633802816902,177.30496453900707,178.57142857142858,179.85611510791367,181.1594202898551,182.4817518248175,183.82352941176472,185.18518518518516,186.56716417910448,187.96992481203006,189.39393939393943,190.83969465648855,192.30769230769235,193.7984496124031,195.3125,196.85039370078738,198.4126984126984,200.,201.61290322580646,203.2520325203252,204.91803278688525,206.61157024793388,208.33333333333334,210.08403361344537,211.86440677966104,213.67521367521368,215.51724137931032,217.39130434782612,219.29824561403507,221.23893805309734,223.2142857142857,225.22522522522524,227.27272727272725,229.35779816513767,231.48148148148147,233.64485981308414,235.84905660377356,238.09523809523813,240.38461538461536,242.71844660194176,245.09803921568627,247.5247524752475,250. +120.,120.60301507537687,121.21212121212122,121.82741116751268,122.44897959183673,123.0769230769231,123.71134020618558,124.35233160621763,125.00000000000001,125.6544502617801,126.3157894736842,126.984126984127,127.65957446808511,128.34224598930479,129.03225806451616,129.7297297297297,130.43478260869566,131.14754098360655,131.86813186813185,132.59668508287294,133.33333333333334,134.0782122905028,134.8314606741573,135.59322033898306,136.36363636363637,137.14285714285714,137.9310344827586,138.72832369942196,139.53488372093025,140.35087719298244,141.1764705882353,142.01183431952663,142.85714285714286,143.7125748502994,144.57831325301206,145.45454545454547,146.34146341463415,147.23926380368098,148.14814814814815,149.06832298136646,150.,150.9433962264151,151.89873417721518,152.86624203821654,153.84615384615384,154.83870967741936,155.84415584415584,156.86274509803923,157.89473684210526,158.94039735099338,160.,161.07382550335572,162.16216216216216,163.26530612244898,164.3835616438356,165.51724137931035,166.66666666666666,167.83216783216784,169.01408450704227,170.21276595744678,171.42857142857144,172.66187050359713,173.9130434782609,175.1824817518248,176.47058823529414,177.77777777777777,179.1044776119403,180.45112781954887,181.81818181818184,183.206106870229,184.61538461538464,186.04651162790697,187.5,188.97637795275588,190.47619047619045,192.,193.5483870967742,195.1219512195122,196.72131147540983,198.34710743801654,200.,201.68067226890756,203.3898305084746,205.12820512820514,206.8965517241379,208.69565217391306,210.52631578947367,212.38938053097345,214.28571428571428,216.21621621621625,218.1818181818182,220.18348623853214,222.2222222222222,224.29906542056077,226.4150943396226,228.5714285714286,230.76923076923075,233.0097087378641,235.2941176470588,237.6237623762376,240. +115.,115.57788944723617,116.16161616161617,116.75126903553299,117.34693877551021,117.94871794871797,118.55670103092785,119.17098445595856,119.79166666666667,120.41884816753927,121.05263157894736,121.69312169312171,122.34042553191489,122.99465240641709,123.65591397849464,124.32432432432431,125.,125.6830601092896,126.37362637362637,127.0718232044199,127.77777777777779,128.49162011173183,129.21348314606743,129.94350282485877,130.6818181818182,131.42857142857142,132.183908045977,132.94797687861274,133.72093023255815,134.50292397660817,135.29411764705884,136.09467455621302,136.9047619047619,137.7245508982036,138.5542168674699,139.3939393939394,140.2439024390244,141.1042944785276,141.9753086419753,142.85714285714286,143.75,144.65408805031447,145.56962025316454,146.49681528662418,147.43589743589743,148.38709677419354,149.35064935064935,150.32679738562092,151.31578947368422,152.317880794702,153.33333333333331,154.36241610738256,155.4054054054054,156.4625850340136,157.53424657534245,158.6206896551724,159.72222222222223,160.83916083916085,161.97183098591552,163.1205673758865,164.28571428571428,165.46762589928056,166.66666666666669,167.8832116788321,169.11764705882354,170.37037037037035,171.64179104477614,172.93233082706766,174.24242424242428,175.57251908396944,176.92307692307696,178.29457364341084,179.6875,181.1023622047244,182.53968253968253,184.,185.48387096774195,186.9918699186992,188.52459016393442,190.0826446280992,191.66666666666669,193.27731092436974,194.91525423728814,196.5811965811966,198.2758620689655,200.00000000000003,201.75438596491227,203.53982300884957,205.35714285714283,207.20720720720723,209.0909090909091,211.00917431192664,212.96296296296293,214.95327102803742,216.98113207547166,219.0476190476191,221.15384615384613,223.3009708737864,225.49019607843135,227.72277227722773,230. +110.,110.55276381909547,111.11111111111111,111.67512690355329,112.24489795918367,112.82051282051283,113.40206185567011,113.98963730569949,114.58333333333334,115.18324607329843,115.78947368421052,116.40211640211642,117.02127659574468,117.64705882352939,118.27956989247313,118.9189189189189,119.56521739130434,120.21857923497267,120.87912087912086,121.54696132596686,122.22222222222223,122.90502793296089,123.59550561797754,124.29378531073446,125.00000000000001,125.71428571428571,126.43678160919539,127.16763005780348,127.90697674418605,128.6549707602339,129.41176470588235,130.17751479289942,130.95238095238096,131.7365269461078,132.53012048192772,133.33333333333334,134.14634146341464,134.96932515337423,135.80246913580245,136.64596273291926,137.5,138.36477987421387,139.2405063291139,140.12738853503183,141.02564102564102,141.93548387096774,142.85714285714286,143.79084967320262,144.73684210526318,145.6953642384106,146.66666666666666,147.6510067114094,148.64864864864865,149.65986394557822,150.6849315068493,151.7241379310345,152.77777777777777,153.84615384615387,154.92957746478874,156.0283687943262,157.14285714285714,158.27338129496403,159.4202898550725,160.5839416058394,161.76470588235296,162.96296296296296,164.17910447761196,165.41353383458645,166.66666666666669,167.9389312977099,169.23076923076925,170.54263565891472,171.875,173.2283464566929,174.60317460317458,176.,177.4193548387097,178.86178861788616,180.32786885245903,181.8181818181818,183.33333333333334,184.87394957983193,186.4406779661017,188.03418803418805,189.65517241379308,191.30434782608697,192.98245614035088,194.69026548672568,196.42857142857142,198.1981981981982,200.,201.83486238532114,203.70370370370367,205.60747663551405,207.54716981132074,209.52380952380958,211.53846153846152,213.59223300970874,215.68627450980392,217.82178217821783,220. +105.,105.52763819095476,106.06060606060606,106.5989847715736,107.14285714285714,107.69230769230771,108.24742268041238,108.80829015544043,109.37500000000001,109.94764397905759,110.52631578947368,111.11111111111113,111.70212765957447,112.2994652406417,112.90322580645163,113.5135135135135,114.13043478260869,114.75409836065573,115.38461538461537,116.02209944751382,116.66666666666667,117.31843575418993,117.97752808988764,118.64406779661017,119.31818181818183,120.,120.68965517241378,121.38728323699424,122.09302325581396,122.80701754385964,123.52941176470588,124.26035502958581,125.,125.74850299401199,126.50602409638556,127.27272727272728,128.04878048780486,128.83435582822085,129.62962962962962,130.43478260869566,131.25,132.07547169811323,132.91139240506328,133.7579617834395,134.6153846153846,135.48387096774192,136.36363636363637,137.2549019607843,138.1578947368421,139.0728476821192,140.,140.93959731543626,141.8918918918919,142.85714285714286,143.83561643835617,144.82758620689657,145.83333333333331,146.85314685314688,147.88732394366198,148.93617021276594,150.,151.0791366906475,152.17391304347828,153.28467153284672,154.41176470588238,155.55555555555554,156.71641791044777,157.89473684210526,159.09090909090912,160.30534351145036,161.53846153846158,162.7906976744186,164.0625,165.3543307086614,166.66666666666666,168.,169.35483870967744,170.73170731707316,172.13114754098362,173.55371900826447,175.,176.47058823529412,177.96610169491527,179.4871794871795,181.03448275862067,182.60869565217394,184.21052631578945,185.8407079646018,187.5,189.18918918918922,190.9090909090909,192.66055045871562,194.44444444444443,196.26168224299067,198.1132075471698,200.00000000000003,201.9230769230769,203.88349514563106,205.88235294117646,207.92079207920793,210. +100.,100.50251256281406,101.01010101010101,101.5228426395939,102.04081632653062,102.56410256410258,103.09278350515466,103.62694300518136,104.16666666666667,104.71204188481676,105.26315789473684,105.82010582010584,106.38297872340425,106.951871657754,107.52688172043013,108.1081081081081,108.69565217391303,109.28961748633878,109.89010989010988,110.49723756906079,111.11111111111111,111.73184357541899,112.35955056179776,112.99435028248588,113.63636363636364,114.28571428571428,114.94252873563218,115.60693641618498,116.27906976744187,116.95906432748538,117.64705882352942,118.34319526627219,119.04761904761905,119.76047904191618,120.48192771084338,121.21212121212122,121.95121951219512,122.69938650306749,123.45679012345678,124.22360248447207,125.,125.7861635220126,126.58227848101265,127.38853503184713,128.2051282051282,129.03225806451613,129.87012987012986,130.718954248366,131.57894736842107,132.45033112582783,133.33333333333331,134.2281879194631,135.13513513513513,136.05442176870747,136.986301369863,137.93103448275863,138.88888888888889,139.86013986013987,140.84507042253523,141.84397163120565,142.85714285714286,143.88489208633092,144.9275362318841,145.98540145985402,147.05882352941177,148.14814814814815,149.2537313432836,150.37593984962405,151.51515151515153,152.67175572519082,153.84615384615387,155.03875968992247,156.25,157.4803149606299,158.73015873015873,160.,161.29032258064518,162.60162601626016,163.9344262295082,165.28925619834712,166.66666666666669,168.0672268907563,169.49152542372883,170.94017094017096,172.41379310344826,173.91304347826087,175.43859649122805,176.99115044247787,178.57142857142856,180.1801801801802,181.8181818181818,183.48623853211012,185.18518518518516,186.9158878504673,188.67924528301884,190.4761904761905,192.3076923076923,194.1747572815534,196.078431372549,198.01980198019803,200. +95.,95.47738693467336,95.95959595959597,96.44670050761421,96.93877551020408,97.43589743589745,97.93814432989691,98.44559585492229,98.95833333333334,99.47643979057591,100.,100.52910052910055,101.06382978723404,101.6042780748663,102.15053763440862,102.70270270270268,103.26086956521739,103.82513661202185,104.39560439560438,104.97237569060773,105.55555555555556,106.14525139664804,106.74157303370788,107.34463276836158,107.95454545454547,108.57142857142857,109.19540229885057,109.82658959537574,110.46511627906978,111.1111111111111,111.76470588235294,112.42603550295858,113.09523809523809,113.77245508982037,114.45783132530121,115.15151515151516,115.85365853658536,116.56441717791411,117.28395061728395,118.01242236024846,118.75,119.49685534591197,120.253164556962,121.01910828025477,121.79487179487178,122.58064516129032,123.37662337662337,124.18300653594771,125.00000000000001,125.82781456953643,126.66666666666666,127.51677852348993,128.3783783783784,129.2517006802721,130.13698630136986,131.0344827586207,131.94444444444443,132.86713286713288,133.80281690140845,134.75177304964538,135.71428571428572,136.69064748201438,137.68115942028987,138.68613138686132,139.7058823529412,140.74074074074073,141.7910447761194,142.85714285714286,143.93939393939397,145.03816793893128,146.1538461538462,147.28682170542635,148.4375,149.6062992125984,150.79365079365078,152.,153.22580645161293,154.47154471544715,155.7377049180328,157.02479338842974,158.33333333333334,159.6638655462185,161.0169491525424,162.3931623931624,163.79310344827584,165.21739130434784,166.66666666666666,168.141592920354,169.64285714285714,171.17117117117118,172.72727272727272,174.31192660550462,175.9259259259259,177.57009345794395,179.2452830188679,180.952380952381,182.69230769230768,184.46601941747574,186.27450980392157,188.11881188118812,190. +90.,90.45226130653265,90.90909090909092,91.37055837563452,91.83673469387756,92.30769230769232,92.78350515463919,93.26424870466323,93.75,94.24083769633508,94.73684210526315,95.23809523809526,95.74468085106383,96.25668449197859,96.77419354838712,97.29729729729728,97.82608695652173,98.3606557377049,98.90109890109889,99.4475138121547,100.,100.55865921787709,101.12359550561798,101.69491525423729,102.27272727272728,102.85714285714285,103.44827586206895,104.04624277456648,104.65116279069768,105.26315789473684,105.88235294117648,106.50887573964498,107.14285714285714,107.78443113772457,108.43373493975905,109.0909090909091,109.7560975609756,110.42944785276075,111.1111111111111,111.80124223602486,112.5,113.20754716981133,113.92405063291137,114.64968152866241,115.38461538461537,116.12903225806451,116.88311688311688,117.64705882352942,118.42105263157896,119.20529801324504,120.,120.80536912751678,121.62162162162161,122.44897959183673,123.2876712328767,124.13793103448276,125.,125.8741258741259,126.7605633802817,127.6595744680851,128.57142857142858,129.49640287769785,130.43478260869568,131.38686131386862,132.3529411764706,133.33333333333331,134.32835820895522,135.33834586466165,136.36363636363637,137.40458015267174,138.46153846153848,139.53488372093022,140.625,141.7322834645669,142.85714285714286,144.,145.16129032258067,146.34146341463415,147.54098360655738,148.7603305785124,150.,151.26050420168067,152.54237288135596,153.84615384615387,155.17241379310343,156.5217391304348,157.89473684210526,159.2920353982301,160.7142857142857,162.1621621621622,163.63636363636363,165.1376146788991,166.66666666666666,168.22429906542058,169.81132075471697,171.42857142857144,173.07692307692307,174.75728155339806,176.47058823529412,178.21782178217822,180. +85.,85.42713567839195,85.85858585858587,86.29441624365482,86.73469387755102,87.1794871794872,87.62886597938146,88.08290155440416,88.54166666666667,89.00523560209425,89.47368421052632,89.94708994708996,90.42553191489361,90.90909090909089,91.3978494623656,91.89189189189187,92.39130434782608,92.89617486338797,93.4065934065934,93.92265193370166,94.44444444444444,94.97206703910614,95.50561797752809,96.045197740113,96.5909090909091,97.14285714285714,97.70114942528735,98.26589595375724,98.83720930232559,99.41520467836257,100.,100.59171597633137,101.19047619047619,101.79640718562875,102.40963855421688,103.03030303030303,103.65853658536585,104.29447852760737,104.93827160493827,105.59006211180126,106.25,106.9182389937107,107.59493670886074,108.28025477707006,108.97435897435896,109.6774193548387,110.38961038961038,111.11111111111111,111.8421052631579,112.58278145695365,113.33333333333333,114.09395973154363,114.86486486486486,115.64625850340136,116.43835616438355,117.24137931034484,118.05555555555556,118.8811188811189,119.71830985915494,120.56737588652481,121.42857142857143,122.3021582733813,123.18840579710147,124.08759124087591,125.00000000000001,125.92592592592592,126.86567164179105,127.81954887218045,128.7878787878788,129.7709923664122,130.7692307692308,131.7829457364341,132.8125,133.8582677165354,134.9206349206349,136.,137.09677419354838,138.21138211382114,139.34426229508196,140.49586776859505,141.66666666666669,142.85714285714286,144.0677966101695,145.29914529914532,146.55172413793102,147.82608695652175,149.12280701754386,150.4424778761062,151.78571428571428,153.15315315315317,154.54545454545453,155.9633027522936,157.4074074074074,158.8785046728972,160.37735849056602,161.90476190476193,163.46153846153845,165.04854368932038,166.66666666666666,168.31683168316832,170. +80.,80.40201005025125,80.80808080808082,81.21827411167513,81.63265306122449,82.05128205128206,82.47422680412372,82.90155440414509,83.33333333333334,83.7696335078534,84.21052631578947,84.65608465608467,85.1063829787234,85.5614973262032,86.0215053763441,86.48648648648647,86.95652173913044,87.43169398907102,87.9120879120879,88.39779005524862,88.88888888888889,89.3854748603352,89.88764044943821,90.3954802259887,90.90909090909092,91.42857142857142,91.95402298850574,92.48554913294798,93.0232558139535,93.56725146198829,94.11764705882354,94.67455621301775,95.23809523809524,95.80838323353294,96.3855421686747,96.96969696969697,97.5609756097561,98.15950920245399,98.76543209876543,99.37888198757764,100.,100.62893081761007,101.26582278481011,101.9108280254777,102.56410256410255,103.2258064516129,103.8961038961039,104.57516339869281,105.26315789473685,105.96026490066225,106.66666666666666,107.38255033557047,108.1081081081081,108.84353741496598,109.58904109589041,110.3448275862069,111.11111111111111,111.8881118881119,112.67605633802818,113.47517730496452,114.28571428571429,115.10791366906474,115.94202898550726,116.78832116788321,117.64705882352942,118.5185185185185,119.40298507462687,120.30075187969925,121.21212121212123,122.13740458015266,123.0769230769231,124.03100775193798,125.,125.98425196850393,126.98412698412697,128.,129.03225806451613,130.0813008130081,131.14754098360658,132.2314049586777,133.33333333333334,134.45378151260505,135.59322033898306,136.75213675213678,137.9310344827586,139.13043478260872,140.35087719298244,141.59292035398232,142.85714285714283,144.14414414414415,145.45454545454544,146.7889908256881,148.14814814814812,149.53271028037386,150.94339622641508,152.3809523809524,153.84615384615384,155.3398058252427,156.8627450980392,158.41584158415841,160. +75.,75.37688442211055,75.75757575757576,76.14213197969542,76.53061224489797,76.92307692307693,77.31958762886599,77.72020725388602,78.125,78.53403141361257,78.94736842105263,79.36507936507938,79.7872340425532,80.2139037433155,80.64516129032259,81.08108108108107,81.52173913043478,81.96721311475409,82.41758241758241,82.87292817679558,83.33333333333334,83.79888268156424,84.26966292134833,84.74576271186442,85.22727272727273,85.71428571428571,86.20689655172413,86.70520231213874,87.2093023255814,87.71929824561403,88.23529411764706,88.75739644970415,89.28571428571429,89.82035928143713,90.36144578313254,90.9090909090909,91.46341463414635,92.02453987730061,92.59259259259258,93.16770186335404,93.75,94.33962264150945,94.93670886075948,95.54140127388534,96.15384615384615,96.77419354838709,97.40259740259741,98.03921568627452,98.6842105263158,99.33774834437087,100.,100.67114093959732,101.35135135135135,102.0408163265306,102.73972602739725,103.44827586206897,104.16666666666666,104.89510489510491,105.63380281690142,106.38297872340424,107.14285714285714,107.91366906474819,108.69565217391306,109.48905109489051,110.29411764705883,111.1111111111111,111.9402985074627,112.78195488721803,113.63636363636365,114.50381679389312,115.3846153846154,116.27906976744185,117.1875,118.11023622047243,119.04761904761904,120.,120.96774193548389,121.95121951219512,122.95081967213115,123.96694214876034,125.,126.05042016806723,127.11864406779662,128.2051282051282,129.3103448275862,130.43478260869566,131.57894736842104,132.7433628318584,133.92857142857142,135.13513513513516,136.36363636363635,137.6146788990826,138.88888888888889,140.1869158878505,141.50943396226413,142.8571428571429,144.23076923076923,145.63106796116506,147.05882352941177,148.5148514851485,150. +70.,70.35175879396985,70.70707070707071,71.06598984771573,71.42857142857143,71.79487179487181,72.16494845360826,72.53886010362694,72.91666666666667,73.29842931937172,73.68421052631578,74.07407407407409,74.46808510638297,74.8663101604278,75.26881720430109,75.67567567567566,76.08695652173913,76.50273224043715,76.92307692307692,77.34806629834254,77.77777777777779,78.2122905027933,78.65168539325843,79.09604519774011,79.54545454545455,80.,80.45977011494253,80.92485549132948,81.39534883720931,81.87134502923976,82.3529411764706,82.84023668639054,83.33333333333333,83.83233532934132,84.33734939759037,84.84848484848484,85.36585365853658,85.88957055214723,86.41975308641975,86.95652173913044,87.5,88.05031446540882,88.60759493670885,89.17197452229298,89.74358974358974,90.32258064516128,90.9090909090909,91.50326797385621,92.10526315789474,92.71523178807948,93.33333333333333,93.95973154362416,94.5945945945946,95.23809523809524,95.8904109589041,96.55172413793105,97.22222222222221,97.9020979020979,98.59154929577466,99.29078014184395,100.,100.71942446043165,101.44927536231886,102.18978102189782,102.94117647058825,103.7037037037037,104.47761194029852,105.26315789473684,106.06060606060608,106.87022900763358,107.69230769230771,108.52713178294573,109.375,110.23622047244093,111.1111111111111,112.,112.90322580645162,113.8211382113821,114.75409836065575,115.70247933884298,116.66666666666667,117.64705882352942,118.64406779661017,119.65811965811967,120.68965517241378,121.73913043478262,122.80701754385964,123.89380530973452,124.99999999999999,126.12612612612614,127.27272727272727,128.44036697247708,129.62962962962962,130.84112149532712,132.0754716981132,133.33333333333337,134.6153846153846,135.92233009708738,137.2549019607843,138.6138613861386,140. +65.,65.32663316582914,65.65656565656566,65.98984771573603,66.3265306122449,66.66666666666667,67.01030927835052,67.35751295336789,67.70833333333334,68.06282722513089,68.42105263157895,68.7830687830688,69.14893617021276,69.51871657754009,69.89247311827958,70.27027027027026,70.65217391304347,71.0382513661202,71.42857142857142,71.8232044198895,72.22222222222223,72.62569832402234,73.03370786516854,73.44632768361582,73.86363636363637,74.28571428571428,74.71264367816092,75.14450867052024,75.58139534883722,76.0233918128655,76.47058823529412,76.92307692307692,77.38095238095238,77.84431137724552,78.3132530120482,78.7878787878788,79.26829268292683,79.75460122699387,80.24691358024691,80.74534161490683,81.25,81.76100628930818,82.27848101265822,82.80254777070063,83.33333333333333,83.87096774193549,84.41558441558442,84.9673202614379,85.52631578947368,86.09271523178809,86.66666666666666,87.24832214765101,87.83783783783784,88.43537414965986,89.04109589041096,89.65517241379311,90.27777777777777,90.90909090909092,91.5492957746479,92.19858156028367,92.85714285714286,93.5251798561151,94.20289855072465,94.8905109489051,95.58823529411765,96.29629629629629,97.01492537313433,97.74436090225564,98.4848484848485,99.23664122137404,100.00000000000001,100.7751937984496,101.5625,102.36220472440944,103.17460317460316,104.,104.83870967741936,105.6910569105691,106.55737704918033,107.43801652892563,108.33333333333334,109.24369747899159,110.16949152542374,111.11111111111111,112.06896551724137,113.04347826086958,114.03508771929823,115.04424778761063,116.07142857142857,117.11711711711713,118.18181818181817,119.26605504587158,120.37037037037035,121.49532710280376,122.64150943396226,123.80952380952382,124.99999999999999,126.2135922330097,127.45098039215685,128.7128712871287,130. +60.,60.30150753768844,60.60606060606061,60.91370558375634,61.224489795918366,61.53846153846155,61.85567010309279,62.176165803108816,62.50000000000001,62.82722513089005,63.1578947368421,63.4920634920635,63.829787234042556,64.17112299465239,64.51612903225808,64.86486486486486,65.21739130434783,65.57377049180327,65.93406593406593,66.29834254143647,66.66666666666667,67.0391061452514,67.41573033707866,67.79661016949153,68.18181818181819,68.57142857142857,68.9655172413793,69.36416184971098,69.76744186046513,70.17543859649122,70.58823529411765,71.00591715976331,71.42857142857143,71.8562874251497,72.28915662650603,72.72727272727273,73.17073170731707,73.61963190184049,74.07407407407408,74.53416149068323,75.,75.47169811320755,75.94936708860759,76.43312101910827,76.92307692307692,77.41935483870968,77.92207792207792,78.43137254901961,78.94736842105263,79.47019867549669,80.,80.53691275167786,81.08108108108108,81.63265306122449,82.1917808219178,82.75862068965517,83.33333333333333,83.91608391608392,84.50704225352113,85.10638297872339,85.71428571428572,86.33093525179856,86.95652173913045,87.5912408759124,88.23529411764707,88.88888888888889,89.55223880597015,90.22556390977444,90.90909090909092,91.6030534351145,92.30769230769232,93.02325581395348,93.75,94.48818897637794,95.23809523809523,96.,96.7741935483871,97.5609756097561,98.36065573770492,99.17355371900827,100.,100.84033613445378,101.6949152542373,102.56410256410257,103.44827586206895,104.34782608695653,105.26315789473684,106.19469026548673,107.14285714285714,108.10810810810813,109.0909090909091,110.09174311926607,111.1111111111111,112.14953271028038,113.2075471698113,114.2857142857143,115.38461538461537,116.50485436893204,117.6470588235294,118.8118811881188,120. +55.,55.27638190954774,55.55555555555556,55.837563451776646,56.12244897959184,56.410256410256416,56.701030927835056,56.994818652849744,57.29166666666667,57.59162303664922,57.89473684210526,58.20105820105821,58.51063829787234,58.823529411764696,59.13978494623657,59.45945945945945,59.78260869565217,60.10928961748633,60.43956043956043,60.77348066298343,61.111111111111114,61.452513966480446,61.79775280898877,62.14689265536723,62.50000000000001,62.857142857142854,63.218390804597696,63.58381502890174,63.95348837209303,64.32748538011695,64.70588235294117,65.08875739644971,65.47619047619048,65.8682634730539,66.26506024096386,66.66666666666667,67.07317073170732,67.48466257668711,67.90123456790123,68.32298136645963,68.75,69.18238993710693,69.62025316455696,70.06369426751591,70.51282051282051,70.96774193548387,71.42857142857143,71.89542483660131,72.36842105263159,72.8476821192053,73.33333333333333,73.8255033557047,74.32432432432432,74.82993197278911,75.34246575342465,75.86206896551725,76.38888888888889,76.92307692307693,77.46478873239437,78.0141843971631,78.57142857142857,79.13669064748201,79.71014492753625,80.2919708029197,80.88235294117648,81.48148148148148,82.08955223880598,82.70676691729322,83.33333333333334,83.96946564885495,84.61538461538463,85.27131782945736,85.9375,86.61417322834644,87.30158730158729,88.,88.70967741935485,89.43089430894308,90.16393442622952,90.9090909090909,91.66666666666667,92.43697478991596,93.22033898305085,94.01709401709402,94.82758620689654,95.65217391304348,96.49122807017544,97.34513274336284,98.21428571428571,99.0990990990991,100.,100.91743119266057,101.85185185185183,102.80373831775702,103.77358490566037,104.76190476190479,105.76923076923076,106.79611650485437,107.84313725490196,108.91089108910892,110. +50.,50.25125628140703,50.505050505050505,50.76142131979695,51.02040816326531,51.28205128205129,51.54639175257733,51.81347150259068,52.083333333333336,52.35602094240838,52.63157894736842,52.91005291005292,53.191489361702125,53.475935828877,53.76344086021506,54.05405405405405,54.347826086956516,54.64480874316939,54.94505494505494,55.24861878453039,55.55555555555556,55.865921787709496,56.17977528089888,56.49717514124294,56.81818181818182,57.14285714285714,57.47126436781609,57.80346820809249,58.139534883720934,58.47953216374269,58.82352941176471,59.171597633136095,59.523809523809526,59.88023952095809,60.24096385542169,60.60606060606061,60.97560975609756,61.34969325153374,61.72839506172839,62.111801242236034,62.5,62.8930817610063,63.291139240506325,63.69426751592356,64.1025641025641,64.51612903225806,64.93506493506493,65.359477124183,65.78947368421053,66.22516556291392,66.66666666666666,67.11409395973155,67.56756756756756,68.02721088435374,68.4931506849315,68.96551724137932,69.44444444444444,69.93006993006993,70.42253521126761,70.92198581560282,71.42857142857143,71.94244604316546,72.46376811594205,72.99270072992701,73.52941176470588,74.07407407407408,74.6268656716418,75.18796992481202,75.75757575757576,76.33587786259541,76.92307692307693,77.51937984496124,78.125,78.74015748031495,79.36507936507937,80.,80.64516129032259,81.30081300813008,81.9672131147541,82.64462809917356,83.33333333333334,84.03361344537815,84.74576271186442,85.47008547008548,86.20689655172413,86.95652173913044,87.71929824561403,88.49557522123894,89.28571428571428,90.0900900900901,90.9090909090909,91.74311926605506,92.59259259259258,93.45794392523365,94.33962264150942,95.23809523809526,96.15384615384615,97.0873786407767,98.0392156862745,99.00990099009901,100. +45.,45.226130653266324,45.45454545454546,45.68527918781726,45.91836734693878,46.15384615384616,46.39175257731959,46.632124352331616,46.875,47.12041884816754,47.368421052631575,47.61904761904763,47.87234042553192,48.128342245989295,48.38709677419356,48.64864864864864,48.91304347826087,49.18032786885245,49.450549450549445,49.72375690607735,50.,50.279329608938546,50.56179775280899,50.847457627118644,51.13636363636364,51.42857142857142,51.72413793103448,52.02312138728324,52.32558139534884,52.63157894736842,52.94117647058824,53.25443786982249,53.57142857142857,53.892215568862284,54.216867469879524,54.54545454545455,54.8780487804878,55.21472392638037,55.55555555555555,55.90062111801243,56.25,56.60377358490567,56.96202531645569,57.324840764331206,57.692307692307686,58.064516129032256,58.44155844155844,58.82352941176471,59.21052631578948,59.60264900662252,60.,60.40268456375839,60.81081081081081,61.224489795918366,61.64383561643835,62.06896551724138,62.5,62.93706293706295,63.38028169014085,63.82978723404255,64.28571428571429,64.74820143884892,65.21739130434784,65.69343065693431,66.1764705882353,66.66666666666666,67.16417910447761,67.66917293233082,68.18181818181819,68.70229007633587,69.23076923076924,69.76744186046511,70.3125,70.86614173228345,71.42857142857143,72.,72.58064516129033,73.17073170731707,73.77049180327869,74.3801652892562,75.,75.63025210084034,76.27118644067798,76.92307692307693,77.58620689655172,78.2608695652174,78.94736842105263,79.64601769911505,80.35714285714285,81.0810810810811,81.81818181818181,82.56880733944955,83.33333333333333,84.11214953271029,84.90566037735849,85.71428571428572,86.53846153846153,87.37864077669903,88.23529411764706,89.10891089108911,90. +40.,40.201005025125625,40.40404040404041,40.609137055837564,40.816326530612244,41.02564102564103,41.23711340206186,41.450777202072544,41.66666666666667,41.8848167539267,42.10526315789473,42.328042328042336,42.5531914893617,42.7807486631016,43.01075268817205,43.243243243243235,43.47826086956522,43.71584699453551,43.95604395604395,44.19889502762431,44.44444444444444,44.6927374301676,44.943820224719104,45.19774011299435,45.45454545454546,45.71428571428571,45.97701149425287,46.24277456647399,46.51162790697675,46.783625730994146,47.05882352941177,47.337278106508876,47.61904761904762,47.90419161676647,48.19277108433735,48.484848484848484,48.78048780487805,49.079754601226995,49.382716049382715,49.68944099378882,50.,50.314465408805034,50.632911392405056,50.95541401273885,51.28205128205128,51.61290322580645,51.94805194805195,52.287581699346404,52.631578947368425,52.980132450331126,53.33333333333333,53.691275167785236,54.05405405405405,54.42176870748299,54.794520547945204,55.17241379310345,55.55555555555556,55.94405594405595,56.33802816901409,56.73758865248226,57.142857142857146,57.55395683453237,57.97101449275363,58.394160583941606,58.82352941176471,59.25925925925925,59.701492537313435,60.150375939849624,60.606060606060616,61.06870229007633,61.53846153846155,62.01550387596899,62.5,62.99212598425196,63.49206349206349,64.,64.51612903225806,65.04065040650406,65.57377049180329,66.11570247933885,66.66666666666667,67.22689075630252,67.79661016949153,68.37606837606839,68.9655172413793,69.56521739130436,70.17543859649122,70.79646017699116,71.42857142857142,72.07207207207207,72.72727272727272,73.39449541284405,74.07407407407406,74.76635514018693,75.47169811320754,76.1904761904762,76.92307692307692,77.66990291262135,78.4313725490196,79.20792079207921,80. +35.,35.175879396984925,35.35353535353536,35.53299492385786,35.714285714285715,35.897435897435905,36.08247422680413,36.26943005181347,36.458333333333336,36.64921465968586,36.84210526315789,37.037037037037045,37.234042553191486,37.4331550802139,37.63440860215054,37.83783783783783,38.04347826086956,38.25136612021858,38.46153846153846,38.67403314917127,38.88888888888889,39.10614525139665,39.325842696629216,39.548022598870055,39.77272727272727,40.,40.229885057471265,40.46242774566474,40.697674418604656,40.93567251461988,41.1764705882353,41.42011834319527,41.666666666666664,41.91616766467066,42.168674698795186,42.42424242424242,42.68292682926829,42.94478527607362,43.20987654320987,43.47826086956522,43.75,44.02515723270441,44.303797468354425,44.58598726114649,44.87179487179487,45.16129032258064,45.45454545454545,45.751633986928105,46.05263157894737,46.35761589403974,46.666666666666664,46.97986577181208,47.2972972972973,47.61904761904762,47.94520547945205,48.27586206896552,48.61111111111111,48.95104895104895,49.29577464788733,49.64539007092198,50.,50.35971223021583,50.72463768115943,51.09489051094891,51.47058823529412,51.85185185185185,52.23880597014926,52.63157894736842,53.03030303030304,53.43511450381679,53.846153846153854,54.263565891472865,54.6875,55.11811023622047,55.55555555555555,56.,56.45161290322581,56.91056910569105,57.37704918032787,57.85123966942149,58.333333333333336,58.82352941176471,59.32203389830509,59.82905982905984,60.34482758620689,60.86956521739131,61.40350877192982,61.94690265486726,62.49999999999999,63.06306306306307,63.63636363636363,64.22018348623854,64.81481481481481,65.42056074766356,66.0377358490566,66.66666666666669,67.3076923076923,67.96116504854369,68.62745098039215,69.3069306930693,70. +30.,30.15075376884422,30.303030303030305,30.45685279187817,30.612244897959183,30.769230769230774,30.927835051546396,31.088082901554408,31.250000000000004,31.413612565445025,31.57894736842105,31.74603174603175,31.914893617021278,32.085561497326196,32.25806451612904,32.43243243243243,32.608695652173914,32.78688524590164,32.967032967032964,33.149171270718234,33.333333333333336,33.5195530726257,33.70786516853933,33.898305084745765,34.09090909090909,34.285714285714285,34.48275862068965,34.68208092485549,34.88372093023256,35.08771929824561,35.294117647058826,35.50295857988166,35.714285714285715,35.92814371257485,36.144578313253014,36.36363636363637,36.58536585365854,36.809815950920246,37.03703703703704,37.267080745341616,37.5,37.735849056603776,37.974683544303794,38.216560509554135,38.46153846153846,38.70967741935484,38.96103896103896,39.21568627450981,39.473684210526315,39.735099337748345,40.,40.26845637583893,40.54054054054054,40.816326530612244,41.0958904109589,41.37931034482759,41.666666666666664,41.95804195804196,42.25352112676057,42.553191489361694,42.85714285714286,43.16546762589928,43.478260869565226,43.7956204379562,44.117647058823536,44.44444444444444,44.776119402985074,45.11278195488722,45.45454545454546,45.80152671755725,46.15384615384616,46.51162790697674,46.875,47.24409448818897,47.61904761904761,48.,48.38709677419355,48.78048780487805,49.18032786885246,49.586776859504134,50.,50.42016806722689,50.84745762711865,51.282051282051285,51.72413793103448,52.173913043478265,52.63157894736842,53.097345132743364,53.57142857142857,54.05405405405406,54.54545454545455,55.045871559633035,55.55555555555555,56.07476635514019,56.60377358490565,57.14285714285715,57.692307692307686,58.25242718446602,58.8235294117647,59.4059405940594,60. +25.,25.125628140703515,25.252525252525253,25.380710659898476,25.510204081632654,25.641025641025646,25.773195876288664,25.90673575129534,26.041666666666668,26.17801047120419,26.31578947368421,26.45502645502646,26.595744680851062,26.7379679144385,26.88172043010753,27.027027027027025,27.173913043478258,27.322404371584696,27.47252747252747,27.624309392265197,27.77777777777778,27.932960893854748,28.08988764044944,28.24858757062147,28.40909090909091,28.57142857142857,28.735632183908045,28.901734104046245,29.069767441860467,29.239766081871345,29.411764705882355,29.585798816568047,29.761904761904763,29.940119760479046,30.120481927710845,30.303030303030305,30.48780487804878,30.67484662576687,30.864197530864196,31.055900621118017,31.25,31.44654088050315,31.645569620253163,31.84713375796178,32.05128205128205,32.25806451612903,32.467532467532465,32.6797385620915,32.89473684210527,33.11258278145696,33.33333333333333,33.557046979865774,33.78378378378378,34.01360544217687,34.24657534246575,34.48275862068966,34.72222222222222,34.96503496503497,35.21126760563381,35.46099290780141,35.714285714285715,35.97122302158273,36.23188405797102,36.496350364963504,36.76470588235294,37.03703703703704,37.3134328358209,37.59398496240601,37.87878787878788,38.167938931297705,38.46153846153847,38.75968992248062,39.0625,39.370078740157474,39.682539682539684,40.,40.322580645161295,40.65040650406504,40.98360655737705,41.32231404958678,41.66666666666667,42.016806722689076,42.37288135593221,42.73504273504274,43.103448275862064,43.47826086956522,43.859649122807014,44.24778761061947,44.64285714285714,45.04504504504505,45.45454545454545,45.87155963302753,46.29629629629629,46.728971962616825,47.16981132075471,47.61904761904763,48.07692307692307,48.54368932038835,49.01960784313725,49.504950495049506,50. +20.,20.100502512562812,20.202020202020204,20.304568527918782,20.408163265306122,20.512820512820515,20.61855670103093,20.725388601036272,20.833333333333336,20.94240837696335,21.052631578947366,21.164021164021168,21.27659574468085,21.3903743315508,21.505376344086024,21.621621621621617,21.73913043478261,21.857923497267755,21.978021978021975,22.099447513812155,22.22222222222222,22.3463687150838,22.471910112359552,22.598870056497177,22.72727272727273,22.857142857142854,22.988505747126435,23.121387283236995,23.255813953488374,23.391812865497073,23.529411764705884,23.668639053254438,23.80952380952381,23.952095808383234,24.096385542168676,24.242424242424242,24.390243902439025,24.539877300613497,24.691358024691358,24.84472049689441,25.,25.157232704402517,25.316455696202528,25.477707006369425,25.64102564102564,25.806451612903224,25.974025974025974,26.143790849673202,26.315789473684212,26.490066225165563,26.666666666666664,26.845637583892618,27.027027027027025,27.210884353741495,27.397260273972602,27.586206896551726,27.77777777777778,27.972027972027973,28.169014084507044,28.36879432624113,28.571428571428573,28.776978417266186,28.985507246376816,29.197080291970803,29.411764705882355,29.629629629629626,29.850746268656717,30.075187969924812,30.303030303030308,30.534351145038165,30.769230769230774,31.007751937984494,31.25,31.49606299212598,31.746031746031743,32.,32.25806451612903,32.52032520325203,32.786885245901644,33.057851239669425,33.333333333333336,33.61344537815126,33.898305084745765,34.188034188034194,34.48275862068965,34.78260869565218,35.08771929824561,35.39823008849558,35.71428571428571,36.03603603603604,36.36363636363636,36.697247706422026,37.03703703703703,37.383177570093466,37.73584905660377,38.0952380952381,38.46153846153846,38.83495145631068,39.2156862745098,39.603960396039604,40. +15.,15.07537688442211,15.151515151515152,15.228426395939085,15.306122448979592,15.384615384615387,15.463917525773198,15.544041450777204,15.625000000000002,15.706806282722512,15.789473684210526,15.873015873015875,15.957446808510639,16.042780748663098,16.12903225806452,16.216216216216214,16.304347826086957,16.39344262295082,16.483516483516482,16.574585635359117,16.666666666666668,16.75977653631285,16.853932584269664,16.949152542372882,17.045454545454547,17.142857142857142,17.241379310344826,17.341040462427745,17.44186046511628,17.543859649122805,17.647058823529413,17.75147928994083,17.857142857142858,17.964071856287426,18.072289156626507,18.181818181818183,18.29268292682927,18.404907975460123,18.51851851851852,18.633540372670808,18.75,18.867924528301888,18.987341772151897,19.108280254777068,19.23076923076923,19.35483870967742,19.48051948051948,19.607843137254903,19.736842105263158,19.867549668874172,20.,20.134228187919465,20.27027027027027,20.408163265306122,20.54794520547945,20.689655172413794,20.833333333333332,20.97902097902098,21.126760563380284,21.276595744680847,21.42857142857143,21.58273381294964,21.739130434782613,21.8978102189781,22.058823529411768,22.22222222222222,22.388059701492537,22.55639097744361,22.72727272727273,22.900763358778626,23.07692307692308,23.25581395348837,23.4375,23.622047244094485,23.809523809523807,24.,24.193548387096776,24.390243902439025,24.59016393442623,24.793388429752067,25.,25.210084033613445,25.423728813559325,25.641025641025642,25.86206896551724,26.086956521739133,26.31578947368421,26.548672566371682,26.785714285714285,27.02702702702703,27.272727272727273,27.522935779816518,27.777777777777775,28.037383177570096,28.301886792452827,28.571428571428577,28.846153846153843,29.12621359223301,29.41176470588235,29.7029702970297,30. +10.,10.050251256281406,10.101010101010102,10.152284263959391,10.204081632653061,10.256410256410257,10.309278350515465,10.362694300518136,10.416666666666668,10.471204188481675,10.526315789473683,10.582010582010584,10.638297872340425,10.6951871657754,10.752688172043012,10.810810810810809,10.869565217391305,10.928961748633878,10.989010989010987,11.049723756906078,11.11111111111111,11.1731843575419,11.235955056179776,11.299435028248588,11.363636363636365,11.428571428571427,11.494252873563218,11.560693641618498,11.627906976744187,11.695906432748536,11.764705882352942,11.834319526627219,11.904761904761905,11.976047904191617,12.048192771084338,12.121212121212121,12.195121951219512,12.269938650306749,12.345679012345679,12.422360248447205,12.5,12.578616352201259,12.658227848101264,12.738853503184712,12.82051282051282,12.903225806451612,12.987012987012987,13.071895424836601,13.157894736842106,13.245033112582782,13.333333333333332,13.422818791946309,13.513513513513512,13.605442176870747,13.698630136986301,13.793103448275863,13.88888888888889,13.986013986013987,14.084507042253522,14.184397163120565,14.285714285714286,14.388489208633093,14.492753623188408,14.598540145985401,14.705882352941178,14.814814814814813,14.925373134328359,15.037593984962406,15.151515151515154,15.267175572519083,15.384615384615387,15.503875968992247,15.625,15.74803149606299,15.873015873015872,16.,16.129032258064516,16.260162601626014,16.393442622950822,16.528925619834713,16.666666666666668,16.80672268907563,16.949152542372882,17.094017094017097,17.241379310344826,17.39130434782609,17.543859649122805,17.69911504424779,17.857142857142854,18.01801801801802,18.18181818181818,18.348623853211013,18.518518518518515,18.691588785046733,18.867924528301884,19.04761904761905,19.23076923076923,19.41747572815534,19.6078431372549,19.801980198019802,20. +5.,5.025125628140703,5.050505050505051,5.0761421319796955,5.1020408163265305,5.128205128205129,5.154639175257732,5.181347150259068,5.208333333333334,5.2356020942408374,5.263157894736842,5.291005291005292,5.319148936170213,5.3475935828877,5.376344086021506,5.405405405405404,5.434782608695652,5.464480874316939,5.494505494505494,5.524861878453039,5.555555555555555,5.58659217877095,5.617977528089888,5.649717514124294,5.6818181818181825,5.7142857142857135,5.747126436781609,5.780346820809249,5.813953488372094,5.847953216374268,5.882352941176471,5.9171597633136095,5.9523809523809526,5.9880239520958085,6.024096385542169,6.0606060606060606,6.097560975609756,6.134969325153374,6.172839506172839,6.211180124223603,6.25,6.289308176100629,6.329113924050632,6.369426751592356,6.41025641025641,6.451612903225806,6.4935064935064934,6.5359477124183005,6.578947368421053,6.622516556291391,6.666666666666666,6.7114093959731544,6.756756756756756,6.802721088435374,6.8493150684931505,6.8965517241379315,6.944444444444445,6.993006993006993,7.042253521126761,7.092198581560282,7.142857142857143,7.194244604316546,7.246376811594204,7.299270072992701,7.352941176470589,7.4074074074074066,7.462686567164179,7.518796992481203,7.575757575757577,7.633587786259541,7.692307692307693,7.751937984496124,7.8125,7.874015748031495,7.936507936507936,8.,8.064516129032258,8.130081300813007,8.196721311475411,8.264462809917356,8.333333333333334,8.403361344537815,8.474576271186441,8.547008547008549,8.620689655172413,8.695652173913045,8.771929824561402,8.849557522123895,8.928571428571427,9.00900900900901,9.09090909090909,9.174311926605506,9.259259259259258,9.345794392523366,9.433962264150942,9.523809523809526,9.615384615384615,9.70873786407767,9.80392156862745,9.900990099009901,10. +0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0. diff --git a/projects/pic/data/ns/ns.py b/projects/pic/data/ns/ns.py index b0ea4643..ce0635e4 100644 --- a/projects/pic/data/ns/ns.py +++ b/projects/pic/data/ns/ns.py @@ -242,10 +242,10 @@ def ns_discovery(foldername, noise_level): # preprocessor_kwargs={'epochs_max' : 1e3}) epde_search_obj.set_preprocessor(default_preprocessor_type='FD', preprocessor_kwargs={}) - popsize = 64 + popsize = 32 epde_search_obj.set_moeadd_params(population_size=popsize, - training_epochs=15) + training_epochs=5) custom_grid_tokens = CacheStoredTokens(token_type='grid', token_labels=['t', 'x'], diff --git a/projects/pic/data/vdp/vdp.py b/projects/pic/data/vdp/vdp.py index a98ef310..84484c0c 100644 --- a/projects/pic/data/vdp/vdp.py +++ b/projects/pic/data/vdp/vdp.py @@ -207,5 +207,5 @@ def vdp_discovery(foldername, noise_level): vdp_folder_name = os.path.join(directory) - VdP_test(fit_operator, vdp_folder_name, 0) - #vdp_discovery(vdp_folder_name, 0) \ No newline at end of file + # VdP_test(fit_operator, vdp_folder_name, 0) + vdp_discovery(vdp_folder_name, 0) \ No newline at end of file diff --git a/projects/thesis/__init__.py b/projects/thesis/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/thesis/adapters/__init__.py b/projects/thesis/adapters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/thesis/adapters/ac.py b/projects/thesis/adapters/ac.py new file mode 100644 index 00000000..8e7be980 --- /dev/null +++ b/projects/thesis/adapters/ac.py @@ -0,0 +1,16 @@ +"""Data adapter for Allen-Cahn. See configs/ac.yaml.""" + +import os +import numpy as np + +_DATA_DIR = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', 'pic', 'data', 'ac' +)) + + +def load_data(): + t = np.linspace(0., 1., 51) + x = np.linspace(-1., 0.984375, 128) + data = np.load(os.path.join(_DATA_DIR, 'ac_data.npy')) + grids = np.meshgrid(t, x, indexing='ij') + return tuple(grids), data, ['u'], 1 diff --git a/projects/thesis/adapters/burgers_inviscid.py b/projects/thesis/adapters/burgers_inviscid.py new file mode 100644 index 00000000..fb01e444 --- /dev/null +++ b/projects/thesis/adapters/burgers_inviscid.py @@ -0,0 +1,18 @@ +"""Data adapter for Burgers inviscid. See configs/burgers_inviscid.yaml.""" + +import os +import numpy as np +import pandas as pd + +_DATA_DIR = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', 'pic', 'data', 'burgers' +)) + + +def load_data(): + df = pd.read_csv(os.path.join(_DATA_DIR, 'burgers_sln_100.csv'), header=None) + data = np.transpose(df.values) + t = np.linspace(0, 1, 101) + x = np.linspace(-1000, 0, 101) + grids = np.meshgrid(t, x, indexing='ij') + return tuple(grids), data, ['u'], 1 diff --git a/projects/thesis/adapters/burgers_viscous.py b/projects/thesis/adapters/burgers_viscous.py new file mode 100644 index 00000000..32602cf9 --- /dev/null +++ b/projects/thesis/adapters/burgers_viscous.py @@ -0,0 +1,18 @@ +"""Data adapter for Burgers viscous (SINDy nu=0.1). See configs/burgers_viscous.yaml.""" + +import os +import numpy as np +from scipy.io import loadmat + +_DATA_DIR = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', 'pic', 'data', 'burgers' +)) + + +def load_data(): + burg = loadmat(os.path.join(_DATA_DIR, 'burgers.mat')) + t = np.ravel(burg['t']) + x = np.ravel(burg['x']) + data = np.transpose(np.real(burg['usol'])) + grids = np.meshgrid(t, x, indexing='ij') + return tuple(grids), data, ['u'], 1 diff --git a/projects/thesis/adapters/kdv.py b/projects/thesis/adapters/kdv.py new file mode 100644 index 00000000..607b1fd6 --- /dev/null +++ b/projects/thesis/adapters/kdv.py @@ -0,0 +1,18 @@ +"""Data adapter for KdV (SINDy benchmark). See configs/kdv.yaml.""" + +import os +import numpy as np +from scipy.io import loadmat + +_DATA_DIR = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', 'pic', 'data', 'kdv' +)) + + +def load_data(): + d = loadmat(os.path.join(_DATA_DIR, 'kdv_sindy.mat')) + t = np.ravel(d['t']) + x = np.ravel(d['x']) + u = np.transpose(np.real(d['usol'])) + grids = np.meshgrid(t, x, indexing='ij') + return tuple(grids), u, ['u'], 1 diff --git a/projects/thesis/adapters/kdv_cossin.py b/projects/thesis/adapters/kdv_cossin.py new file mode 100644 index 00000000..85ee4b80 --- /dev/null +++ b/projects/thesis/adapters/kdv_cossin.py @@ -0,0 +1,35 @@ +"""Data adapter for KdV with cos(t)*sin(x) source. See configs/kdv_cossin.yaml.""" + +import os +import numpy as np + +from epde.interface.prepared_tokens import CustomTokens, CustomEvaluator + +_DATA_DIR = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', 'pic', 'data', 'kdv' +)) + + +def load_data(): + shape = 80 + data = np.loadtxt(os.path.join(_DATA_DIR, 'data.csv'), delimiter=',').T + t = np.linspace(0, 1, shape + 1) + x = np.linspace(0, 1, shape + 1) + grids = np.meshgrid(t, x, indexing='ij') + return tuple(grids), data, ['u'], 1 + + +def build_extra_tokens(coords, dim): + custom_eval = { + 'cos(t)sin(x)': lambda *grids, **kwargs: (np.cos(grids[0]) * np.sin(grids[1])) ** kwargs['power'] + } + evaluator = CustomEvaluator(custom_eval, eval_fun_params_labels=['power']) + return [CustomTokens( + token_type='trigonometric', + token_labels=['cos(t)sin(x)'], + evaluator=evaluator, + params_ranges={'power': (1, 1)}, + params_equality_ranges={}, + meaningful=True, + unique_token_type=False, + )] diff --git a/projects/thesis/adapters/ks.py b/projects/thesis/adapters/ks.py new file mode 100644 index 00000000..7a83aae2 --- /dev/null +++ b/projects/thesis/adapters/ks.py @@ -0,0 +1,18 @@ +"""Data adapter for Kuramoto-Sivashinsky. See configs/ks.yaml.""" + +import os +import numpy as np +import scipy.io as scio + +_DATA_DIR = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', 'pic', 'data', 'ks' +)) + + +def load_data(): + d = scio.loadmat(os.path.join(_DATA_DIR, 'kuramoto_sivishinky.mat')) + t = np.ravel(d['tt']) + x = np.ravel(d['x']) + u = d['uu'].T + grids = np.meshgrid(t, x, indexing='ij') + return tuple(grids), u, ['u'], 1 diff --git a/projects/thesis/adapters/lorenz.py b/projects/thesis/adapters/lorenz.py new file mode 100644 index 00000000..817e663a --- /dev/null +++ b/projects/thesis/adapters/lorenz.py @@ -0,0 +1,14 @@ +"""Data adapter for Lorenz system. See configs/lorenz.yaml.""" + +import os +import numpy as np + +_DATA_DIR = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', 'pic', 'data', 'lorenz' +)) + + +def load_data(): + t = np.load(os.path.join(_DATA_DIR, 't.npy'))[:1000] + data = np.load(os.path.join(_DATA_DIR, 'lorenz.npy'))[:1000] + return (t,), [data[:, 0], data[:, 1], data[:, 2]], ['u', 'v', 'w'], 0 diff --git a/projects/thesis/adapters/lv.py b/projects/thesis/adapters/lv.py new file mode 100644 index 00000000..21a2745c --- /dev/null +++ b/projects/thesis/adapters/lv.py @@ -0,0 +1,14 @@ +"""Data adapter for Lotka-Volterra. See configs/lv.yaml.""" + +import os +import numpy as np + +_DATA_DIR = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', 'pic', 'data', 'lv' +)) + + +def load_data(): + t = np.load(os.path.join(_DATA_DIR, 't_20.npy'))[:150] + data = np.load(os.path.join(_DATA_DIR, 'data_20.npy'))[:150] + return (t,), [data[:, 0], data[:, 1]], ['u', 'v'], 0 diff --git a/projects/thesis/adapters/ns.py b/projects/thesis/adapters/ns.py new file mode 100644 index 00000000..4a45284c --- /dev/null +++ b/projects/thesis/adapters/ns.py @@ -0,0 +1,13 @@ +"""Data adapter for Navier-Stokes (placeholder). + +NS is excluded from smoke runs. Truth equations + loader will be filled +in once the Re-specific ground-truth pair + continuity equation are +pinned down (mirror ``ns.py:ns_data`` on cylinder_nektar_wake.mat). +""" + + +def load_data(): + raise NotImplementedError( + 'NS data loader not implemented yet; mirror ns.py:ns_data on ' + 'cylinder_nektar_wake.mat once truth tokens are pinned.' + ) diff --git a/projects/thesis/adapters/ode.py b/projects/thesis/adapters/ode.py new file mode 100644 index 00000000..2e7db9fe --- /dev/null +++ b/projects/thesis/adapters/ode.py @@ -0,0 +1,21 @@ +"""Data adapter for Forced Damped Oscillator. See configs/ode.yaml.""" + +import os +import numpy as np + +from epde import TrigonometricTokens + +_DATA_DIR = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', 'pic', 'data', 'ode' +)) + + +def load_data(): + step, n = 0.05, 320 + t = np.arange(0., step * n, step) + data = np.load(os.path.join(_DATA_DIR, 'ode_data.npy')) + return (t,), [data], ['u'], 0 + + +def build_extra_tokens(coords, dim): + return [TrigonometricTokens(freq=(2 - 1e-8, 2 + 1e-8), dimensionality=dim)] diff --git a/projects/thesis/adapters/pde_compound.py b/projects/thesis/adapters/pde_compound.py new file mode 100644 index 00000000..412c8799 --- /dev/null +++ b/projects/thesis/adapters/pde_compound.py @@ -0,0 +1,17 @@ +"""Data adapter for synthetic compound PDE. See configs/pde_compound.yaml.""" + +import os +import numpy as np + +_DATA_DIR = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', 'pic', 'data', 'pde_compound' +)) + + +def load_data(): + data = np.load(os.path.join(_DATA_DIR, 'PDE_compound.npy')) + nx, nt = 100, 251 + x = np.linspace(1, 2, nx) + t = np.linspace(0, 0.5, nt) + grids = np.meshgrid(t, x, indexing='ij') + return tuple(grids), data, ['u'], 1 diff --git a/projects/thesis/adapters/pde_divide.py b/projects/thesis/adapters/pde_divide.py new file mode 100644 index 00000000..a439195c --- /dev/null +++ b/projects/thesis/adapters/pde_divide.py @@ -0,0 +1,17 @@ +"""Data adapter for synthetic rational PDE. See configs/pde_divide.yaml.""" + +import os +import numpy as np + +_DATA_DIR = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', 'pic', 'data', 'pde_divide' +)) + + +def load_data(): + data = np.load(os.path.join(_DATA_DIR, 'PDE_divide.npy')) + nx, nt = 100, 251 + x = np.linspace(1, 2, nx) + t = np.linspace(0, 0.5, nt) + grids = np.meshgrid(t, x, indexing='ij') + return tuple(grids), data, ['u'], 1 diff --git a/projects/thesis/adapters/vdp.py b/projects/thesis/adapters/vdp.py new file mode 100644 index 00000000..7e8264bc --- /dev/null +++ b/projects/thesis/adapters/vdp.py @@ -0,0 +1,26 @@ +"""Data adapter for Van der Pol. See configs/vdp.yaml. + +build_extra_tokens supplies a tight TrigonometricTokens around freq=2 so the +search exposes ``sin(2t)`` as a factor even though the truth equation +doesn't actually use it (kept for parity with the LEGACY runner). +""" + +import os +import numpy as np + +from epde import TrigonometricTokens + +_DATA_DIR = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', 'pic', 'data', 'vdp' +)) + + +def load_data(): + step, n = 0.05, 320 + t = np.arange(0., step * n, step) + data = np.load(os.path.join(_DATA_DIR, 'vdp_data.npy')) + return (t,), [data], ['u'], 0 + + +def build_extra_tokens(coords, dim): + return [TrigonometricTokens(freq=(2 - 1e-8, 2 + 1e-8), dimensionality=dim)] diff --git a/projects/thesis/adapters/wave.py b/projects/thesis/adapters/wave.py new file mode 100644 index 00000000..9d218da2 --- /dev/null +++ b/projects/thesis/adapters/wave.py @@ -0,0 +1,17 @@ +"""Data adapter for the 1+1D wave equation. See configs/wave.yaml.""" + +import os +import numpy as np + +_DATA_DIR = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', 'pic', 'data', 'wave' +)) + + +def load_data(): + shape = 80 + data = np.loadtxt(os.path.join(_DATA_DIR, 'wave_sln_80.csv'), delimiter=',').T + t = np.linspace(0, 1, shape + 1) + x = np.linspace(0, 1, shape + 1) + grids = np.meshgrid(t, x, indexing='ij') + return tuple(grids), data, ['u'], 1 diff --git a/projects/thesis/configs/ac.yaml b/projects/thesis/configs/ac.yaml new file mode 100644 index 00000000..68c3ef85 --- /dev/null +++ b/projects/thesis/configs/ac.yaml @@ -0,0 +1,5 @@ +# Allen-Cahn (1+1D reaction-diffusion PDE) +# du/dx0 = 0.0001*d^2u/dx1^2 + 5*u - 5*u^3 +name: ac +truth_equations: + - "0.0001 * d^2u/dx1^2{power: 1.0} + -5.0 * u{power: 3.0} + 5.0 * u{power: 1.0} = du/dx0{power: 1.0}" diff --git a/projects/thesis/configs/burgers_inviscid.yaml b/projects/thesis/configs/burgers_inviscid.yaml new file mode 100644 index 00000000..529915af --- /dev/null +++ b/projects/thesis/configs/burgers_inviscid.yaml @@ -0,0 +1,6 @@ +# Burgers inviscid (1+1D PDE) +# du/dx0 = -u * du/dx1 +# Data: burgers_sln_100.csv (same CSV used by ``burgers_discovery``). +name: burgers_inviscid +truth_equations: + - "-1.0 * u{power: 1.0} * du/dx1{power: 1.0} = du/dx0{power: 1.0}" diff --git a/projects/thesis/configs/burgers_viscous.yaml b/projects/thesis/configs/burgers_viscous.yaml new file mode 100644 index 00000000..91373c8b --- /dev/null +++ b/projects/thesis/configs/burgers_viscous.yaml @@ -0,0 +1,6 @@ +# Burgers viscous (1+1D PDE; SINDy benchmark, nu=0.1) +# du/dx0 = -u*du/dx1 + 0.1*d^2u/dx1^2 +# Data: burgers.mat +name: burgers_viscous +truth_equations: + - "-1.0 * u{power: 1.0} * du/dx1{power: 1.0} + 0.1 * d^2u/dx1^2{power: 1.0} = du/dx0{power: 1.0}" diff --git a/projects/thesis/configs/kdv.yaml b/projects/thesis/configs/kdv.yaml new file mode 100644 index 00000000..65ce3e38 --- /dev/null +++ b/projects/thesis/configs/kdv.yaml @@ -0,0 +1,6 @@ +# KdV homogeneous (1+1D PDE) +# du/dx0 = -6*u*du/dx1 - d^3u/dx1^3 +# Data: kdv_sindy.mat +name: kdv +truth_equations: + - "-6.0 * du/dx1{power: 1.0} * u{power: 1.0} + -1.0 * d^3u/dx1^3{power: 1.0} = du/dx0{power: 1.0}" diff --git a/projects/thesis/configs/kdv_cossin.yaml b/projects/thesis/configs/kdv_cossin.yaml new file mode 100644 index 00000000..47943cb0 --- /dev/null +++ b/projects/thesis/configs/kdv_cossin.yaml @@ -0,0 +1,6 @@ +# KdV with cos(t)*sin(x) source term (1+1D PDE) +# du/dx0 = -6*u*du/dx1 - d^3u/dx1^3 + cos(t)*sin(x) +# Data: data.csv (loaded via np.loadtxt) +name: kdv_cossin +truth_equations: + - "-6.0 * du/dx1{power: 1.0} * u{power: 1.0} + -1.0 * d^3u/dx1^3{power: 1.0} + 1.0 * cos(t)sin(x){power: 1.0} = du/dx0{power: 1.0}" diff --git a/projects/thesis/configs/ks.yaml b/projects/thesis/configs/ks.yaml new file mode 100644 index 00000000..6b058789 --- /dev/null +++ b/projects/thesis/configs/ks.yaml @@ -0,0 +1,6 @@ +# Kuramoto-Sivashinsky (1+1D PDE) +# du/dx0 = -u*du/dx1 - d^2u/dx1^2 - d^4u/dx1^4 +# Data: kuramoto_sivishinky.mat +name: ks +truth_equations: + - "-1.0 * u{power: 1.0} * du/dx1{power: 1.0} + -1.0 * d^2u/dx1^2{power: 1.0} + -1.0 * d^4u/dx1^4{power: 1.0} = du/dx0{power: 1.0}" diff --git a/projects/thesis/configs/lorenz.yaml b/projects/thesis/configs/lorenz.yaml new file mode 100644 index 00000000..a3b84933 --- /dev/null +++ b/projects/thesis/configs/lorenz.yaml @@ -0,0 +1,9 @@ +# Lorenz system (coupled 3D ODE) +# du/dx0 = 10*v - 10*u +# dv/dx0 = 28*u - u*w - v +# dw/dx0 = u*v - (8/3)*w +name: lorenz +truth_equations: + - "10.0 * v{power: 1.0} + -10.0 * u{power: 1.0} = du/dx0{power: 1.0}" + - "28.0 * u{power: 1.0} + -1.0 * u{power: 1.0} * w{power: 1.0} + -1.0 * v{power: 1.0} = dv/dx0{power: 1.0}" + - "1.0 * u{power: 1.0} * v{power: 1.0} + -2.6666666666666665 * w{power: 1.0} = dw/dx0{power: 1.0}" diff --git a/projects/thesis/configs/lv.yaml b/projects/thesis/configs/lv.yaml new file mode 100644 index 00000000..2ecca667 --- /dev/null +++ b/projects/thesis/configs/lv.yaml @@ -0,0 +1,6 @@ +# Lotka-Volterra (coupled 2D ODE) +# Generated with alpha=2/3, beta=4/3, delta=1, gamma=1. +name: lv +truth_equations: + - "0.6666666666666666 * u{power: 1.0} + -1.3333333333333333 * u{power: 1.0} * v{power: 1.0} = du/dx0{power: 1.0}" + - "1.0 * u{power: 1.0} * v{power: 1.0} + -1.0 * v{power: 1.0} = dv/dx0{power: 1.0}" diff --git a/projects/thesis/configs/ns.yaml b/projects/thesis/configs/ns.yaml new file mode 100644 index 00000000..4826b96f --- /dev/null +++ b/projects/thesis/configs/ns.yaml @@ -0,0 +1,7 @@ +# Navier-Stokes (2+1D, coupled PDE system) +# +# NOTE: excluded from smoke runs -- runtime is much higher than the rest +# of the benchmark. Truth equations are TODO until the Re-specific +# ground-truth pair + continuity is decided. +name: ns +truth_equations: [] diff --git a/projects/thesis/configs/ode.yaml b/projects/thesis/configs/ode.yaml new file mode 100644 index 00000000..23562d1e --- /dev/null +++ b/projects/thesis/configs/ode.yaml @@ -0,0 +1,6 @@ +# Forced Damped Oscillator (scalar ODE) +# u'' + sin(2t)*u' + 4*u = 1.5*t +# i.e. d^2u/dx0^2 = -4*u - sin(2t)*du/dx0 + 1.5*t +name: ode +truth_equations: + - "-4.0 * u{power: 1.0} + -1.0 * du/dx0{power: 1.0} * sin{power: 1.0, freq: 2.0, dim: 0.0} + 1.5 * x_0{power: 1.0, dim: 0.0} = d^2u/dx0^2{power: 1.0}" diff --git a/projects/thesis/configs/pde_compound.yaml b/projects/thesis/configs/pde_compound.yaml new file mode 100644 index 00000000..3157d34b --- /dev/null +++ b/projects/thesis/configs/pde_compound.yaml @@ -0,0 +1,6 @@ +# Compound PDE (synthetic 1+1D PDE) +# du/dx0 = (du/dx1)^2 + d^2u/dx1^2 * u +# Data: PDE_compound.npy +name: pde_compound +truth_equations: + - "1.0 * du/dx1{power: 2.0} + 1.0 * d^2u/dx1^2{power: 1.0} * u{power: 1.0} = du/dx0{power: 1.0}" diff --git a/projects/thesis/configs/pde_divide.yaml b/projects/thesis/configs/pde_divide.yaml new file mode 100644 index 00000000..512a9ae6 --- /dev/null +++ b/projects/thesis/configs/pde_divide.yaml @@ -0,0 +1,6 @@ +# Rational PDE (synthetic 1+1D PDE) +# du/dx0 * x = du/dx1 + 0.25 * d^2u/dx1^2 * x +# Data: PDE_divide.npy +name: pde_divide +truth_equations: + - "1.0 * du/dx1{power: 1.0} + 0.25 * d^2u/dx1^2{power: 1.0} * x_1{power: 1.0, dim: 1.0} = du/dx0{power: 1.0} * x_1{power: 1.0, dim: 1.0}" diff --git a/projects/thesis/configs/vdp.yaml b/projects/thesis/configs/vdp.yaml new file mode 100644 index 00000000..aba81225 --- /dev/null +++ b/projects/thesis/configs/vdp.yaml @@ -0,0 +1,6 @@ +# Van der Pol oscillator (ODE) +# u'' + 0.2*(u^2 - 1)*u' + u = 0 +# i.e. d^2u/dx0^2 = -0.2*u^2*du/dx0 + 0.2*du/dx0 - u +name: vdp +truth_equations: + - "-0.2 * u{power: 2.0} * du/dx0{power: 1.0} + 0.2 * du/dx0{power: 1.0} + -1.0 * u{power: 1.0} + -0.0 = d^2u/dx0^2{power: 1.0}" diff --git a/projects/thesis/configs/wave.yaml b/projects/thesis/configs/wave.yaml new file mode 100644 index 00000000..ccbbbb14 --- /dev/null +++ b/projects/thesis/configs/wave.yaml @@ -0,0 +1,6 @@ +# Wave equation (1+1D PDE; wave speed^2 = 0.04, c = 0.2) +# d^2u/dx0^2 = 0.04 * d^2u/dx1^2 +# Data: wave_sln_80.csv +name: wave +truth_equations: + - "0.04 * d^2u/dx1^2{power: 1.0} = d^2u/dx0^2{power: 1.0}" diff --git a/projects/thesis/run.py b/projects/thesis/run.py new file mode 100644 index 00000000..2f52e4c5 --- /dev/null +++ b/projects/thesis/run.py @@ -0,0 +1,92 @@ +"""Unified CLI entry for the thesis Section 4.5 main comparison. + +Usage: + python projects/thesis/run.py [--reps N] [--pipelines legacy new] + [--outdir TAG] [--no-resume] + [--seed-base N] + +```` matches one of the YAML files in ``projects/thesis/configs/`` +(e.g. ``lv``, ``lorenz``, ``kdv``). The default pipelines are ``legacy`` and +``new``; pass ``--pipelines`` to override (the eight valid labels are +``legacy``, ``new``, and the six off-diagonal ablation labels -- see +``thesis_runner._PIPELINE_SETTINGS``). + +``--outdir TAG`` redirects results to ``results/TAG//`` so a tagged +sweep across multiple systems stays grouped under one folder. ``--outdir +/abs/path`` lands there directly. +""" + +from __future__ import annotations + +import argparse +import os +import sys + +_THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +if _THIS_DIR not in sys.path: + sys.path.insert(0, _THIS_DIR) + +from thesis_runner import ( # noqa: E402 + ABLATION_PIPELINES, + CONFIGS_DIR, + PIPELINES, + _PIPELINE_SETTINGS, + load_config, + run_smoke, +) + + +def _available_systems() -> list: + if not os.path.isdir(CONFIGS_DIR): + return [] + return sorted( + os.path.splitext(f)[0] + for f in os.listdir(CONFIGS_DIR) + if f.endswith('.yaml') + ) + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + 'system', + help=f"system name (looked up as configs/.yaml). " + f"Available: {', '.join(_available_systems()) or '(none yet)'}", + ) + parser.add_argument('--reps', type=int, default=30, + help="reps per pipeline (default: 30)") + parser.add_argument( + '--pipelines', nargs='+', default=list(PIPELINES), + choices=tuple(_PIPELINE_SETTINGS), + help=f"pipeline labels (default: {' '.join(PIPELINES)})", + ) + parser.add_argument('--outdir', default=None, + help="results tag (lands at results///) " + "or absolute path; default reuses cfg's outdir") + parser.add_argument('--no-resume', dest='resume', action='store_false', default=True, + help="overwrite existing per-rep JSONs instead of skipping") + parser.add_argument('--seed-base', type=int, default=0, + help="seed for rep 0 (rep i uses seed_base + i)") + args = parser.parse_args(argv) + + try: + cfg = load_config(args.system) + except FileNotFoundError as exc: + parser.error(str(exc)) + + run_smoke( + cfg, + reps=args.reps, + pipelines=tuple(args.pipelines), + seed_base=args.seed_base, + resume=args.resume, + outdir=args.outdir, + ) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/projects/thesis/run_ablation.py b/projects/thesis/run_ablation.py new file mode 100644 index 00000000..a2cd29bd --- /dev/null +++ b/projects/thesis/run_ablation.py @@ -0,0 +1,32 @@ +"""Ablation entry point: same CLI as ``run.py`` but defaults to the six +off-diagonal cells of the 2x2x2 factorial (``wape``, ``instab``, ``reg``, +``wape_instab``, ``wape_reg``, ``instab_reg``). + +The 000 (``legacy``) and 111 (``new``) corners are *not* run here -- they +are produced by ``run.py`` and the aggregator reads both label sets from +the same results tree. + +Usage: + python projects/thesis/run_ablation.py [--reps N] + [--outdir TAG] + [--no-resume] +""" + +from __future__ import annotations + +import sys + +from run import main as _main # noqa: E402 +from thesis_runner import ABLATION_PIPELINES # noqa: E402 + + +def main(argv=None) -> int: + argv = list(sys.argv[1:] if argv is None else argv) + # Inject the ablation pipelines unless the caller passed --pipelines explicitly. + if not any(a == '--pipelines' or a.startswith('--pipelines=') for a in argv): + argv += ['--pipelines', *ABLATION_PIPELINES] + return _main(argv) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/projects/thesis/thesis_ablation_aggregate.py b/projects/thesis/thesis_ablation_aggregate.py new file mode 100644 index 00000000..b6b0c352 --- /dev/null +++ b/projects/thesis/thesis_ablation_aggregate.py @@ -0,0 +1,221 @@ +""" +Aggregator for the thesis Section 4.5 ablation study (2x2x2 factorial). + +Walks every ``projects/thesis/results//_rep.json`` file +whose ``pipeline`` field names one of the 8 ablation cells, groups by +(system, cell), and writes a markdown summary plus a JSON snapshot. + +If your ablation runs landed under a tag (``--outdir ablation_v2`` -> +``results/ablation_v2//``), point ``--root`` at that subtree. +Either way the layout is always ``//*.json``; the 000 +(``legacy``) and 111 (``new``) corners are read from the same JSON files +that ``thesis_aggregate.py`` already consumes. + +Cell-label semantics (each label lists the NEW components that are ON): + + legacy 000 fitness=L2, sparsity=LASSO, use_pic=False + wape 100 fitness=L2LR, sparsity=LASSO, use_pic=False + instab 010 fitness=L2, sparsity=LASSO, use_pic=True + reg 001 fitness=L2, sparsity=VWSR, use_pic=False + wape_instab 110 fitness=L2LR, sparsity=LASSO, use_pic=True + wape_reg 101 fitness=L2LR, sparsity=VWSR, use_pic=False + instab_reg 011 fitness=L2, sparsity=VWSR, use_pic=True + new 111 fitness=L2LR, sparsity=VWSR, use_pic=True +""" + +from __future__ import annotations + +import argparse +import glob +import json +import os +import statistics +import sys +from collections import defaultdict + +_THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, _THIS_DIR) +from thesis_metrics import consistency_rate, wilson_ci # noqa: E402 + + +# Ordered so the report reads from "all off" to "all on" along each axis. +ABLATION_CELLS = ( + 'legacy', + 'wape', 'instab', 'reg', + 'wape_instab', 'wape_reg', 'instab_reg', + 'new', +) + +DEFAULT_RESULTS_DIR = os.path.join(_THIS_DIR, 'results') + + +def _load_records(root: str): + records = defaultdict(lambda: defaultdict(list)) # records[system][cell] -> list + pattern = os.path.join(root, '*', '*.json') + for path in sorted(glob.glob(pattern)): + try: + with open(path, 'r', encoding='utf-8') as fh: + rec = json.load(fh) + except (OSError, json.JSONDecodeError): + continue + system = rec.get('system') or os.path.basename(os.path.dirname(path)) + cell = rec.get('pipeline') + if cell not in ABLATION_CELLS: + continue + records[system][cell].append(rec) + return records + + +def _summarize_cell(reps: list) -> dict: + if not reps: + return {'n': 0} + successes = sum(1 for r in reps if r.get('structural_success')) + hammings = [r['hamming'] for r in reps if r.get('hamming') is not None] + runtimes = [r['runtime_sec'] for r in reps if 'runtime_sec' in r] + discovered_tokens = [json.dumps(r.get('discovered_tokens', []), sort_keys=True) for r in reps] + rate = successes / len(reps) + ci = wilson_ci(successes, len(reps)) + mean_h = statistics.fmean(hammings) if hammings else float('nan') + mean_t = statistics.fmean(runtimes) if runtimes else float('nan') + errors = sum(1 for r in reps if 'error' in r) + return { + 'n': len(reps), + 'successes': successes, + 'rate': rate, + 'wilson_lo': ci[0], + 'wilson_hi': ci[1], + 'mean_hamming': mean_h, + 'consistency': consistency_rate(discovered_tokens), + 'mean_runtime_sec': mean_t, + 'errors': errors, + } + + +def _cell_axes(cell: str) -> tuple: + """Return ``(wape_on, instab_on, reg_on)`` triple for a given cell label.""" + if cell == 'legacy': + return (False, False, False) + if cell == 'new': + return (True, True, True) + parts = set(cell.split('_')) + return ('wape' in parts, 'instab' in parts, 'reg' in parts) + + +def _format_table(summary: dict) -> str: + header = ( + '| System | Cell | W | I | R | n | Success | mean H | cons | runtime |' + ) + sep = '|---|---|---|---|---|---|---|---|---|---|' + rows = [header, sep] + + def _check(b: bool) -> str: + return 'X' if b else '.' + + def _success(c): + if c['n'] == 0: + return '-' + return ( + f"{c['rate']*100:.0f}% [{c['wilson_lo']*100:.0f}-{c['wilson_hi']*100:.0f}%] " + f"({c['successes']}/{c['n']})" + ) + + def _num(c, key, fmt): + if c['n'] == 0: + return '-' + v = c.get(key) + if v is None or (isinstance(v, float) and v != v): + return '-' + return fmt.format(v) + + for system in sorted(summary.keys()): + for cell in ABLATION_CELLS: + c = summary[system].get(cell, {'n': 0}) + w, i, r = _cell_axes(cell) + rows.append( + f"| {system} | {cell} | {_check(w)} | {_check(i)} | {_check(r)} | " + f"{c['n']} | {_success(c)} | " + f"{_num(c, 'mean_hamming', '{:.1f}')} | " + f"{_num(c, 'consistency', '{:.2f}')} | " + f"{_num(c, 'mean_runtime_sec', '{:.1f}')}s |" + ) + return '\n'.join(rows) + + +def _format_contributions(summary: dict) -> str: + """Render the marginal contribution of each axis per system.""" + axes = ( + ('WAPE', 0, [('legacy', 'wape'), ('instab', 'wape_instab'), + ('reg', 'wape_reg'), ('instab_reg', 'new')]), + ('Instab', 1, [('legacy', 'instab'), ('wape', 'wape_instab'), + ('reg', 'instab_reg'), ('wape_reg', 'new')]), + ('Reg', 2, [('legacy', 'reg'), ('wape', 'wape_reg'), + ('instab', 'instab_reg'), ('wape_instab', 'new')]), + ) + rows = ['| System | Axis | mean delta success | mean delta H | n pairs |', + '|---|---|---|---|---|'] + for system in sorted(summary.keys()): + for axis_name, _idx, pairs in axes: + d_rate = [] + d_h = [] + for off_cell, on_cell in pairs: + off = summary[system].get(off_cell, {'n': 0}) + on = summary[system].get(on_cell, {'n': 0}) + if off['n'] == 0 or on['n'] == 0: + continue + d_rate.append(on['rate'] - off['rate']) + if ( + on.get('mean_hamming') is not None + and off.get('mean_hamming') is not None + and on['mean_hamming'] == on['mean_hamming'] + and off['mean_hamming'] == off['mean_hamming'] + ): + d_h.append(on['mean_hamming'] - off['mean_hamming']) + if not d_rate: + rows.append(f"| {system} | {axis_name} | - | - | 0 |") + continue + mean_dr = statistics.fmean(d_rate) + mean_dh = statistics.fmean(d_h) if d_h else float('nan') + dh_str = f"{mean_dh:+.2f}" if mean_dh == mean_dh else '-' + rows.append( + f"| {system} | {axis_name} | {mean_dr*100:+.1f}pp | " + f"{dh_str} | {len(d_rate)} |" + ) + return '\n'.join(rows) + + +def aggregate(root: str = None) -> dict: + root = root or DEFAULT_RESULTS_DIR + records = _load_records(root) + summary = { + system: {cell: _summarize_cell(reps) for cell, reps in by_cell.items()} + for system, by_cell in records.items() + } + return summary + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('--root', default=DEFAULT_RESULTS_DIR, + help=f"results root to scan (default: {DEFAULT_RESULTS_DIR})") + parser.add_argument('--out', default=None, + help="path for the JSON snapshot (default: /thesis_ablation_summary.json)") + args = parser.parse_args(argv) + + summary = aggregate(args.root) + print('# Thesis Section 4.5 -- Ablation Cells') + print() + print(_format_table(summary)) + print() + print('# Marginal contribution per axis (mean delta across the 4 mutually-exclusive pairs)') + print() + print(_format_contributions(summary)) + out_path = args.out or os.path.join(_THIS_DIR, 'thesis_ablation_summary.json') + with open(out_path, 'w', encoding='utf-8') as fh: + json.dump(summary, fh, indent=2) + print(f"\nWrote {out_path}") + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/projects/thesis/thesis_aggregate.py b/projects/thesis/thesis_aggregate.py new file mode 100644 index 00000000..2384981f --- /dev/null +++ b/projects/thesis/thesis_aggregate.py @@ -0,0 +1,147 @@ +""" +Aggregator for thesis Section 4.5 smoke / full-run results. + +Walks every ``projects/thesis/results//_rep.json`` +file, groups by (system, pipeline), and writes a markdown summary plus a +JSON snapshot. Metrics per (system, pipeline) cell: + + - structural_success_rate (with Wilson 95% CI) + - mean Hamming distance + - consistency_rate (modal-set agreement) + - mean runtime + +Pass ``--root`` to point at a tagged results tree (e.g. +``projects/thesis/results/ablation_v2``) -- the layout is always +``//*.json``. +""" + +from __future__ import annotations + +import argparse +import glob +import json +import os +import statistics +import sys +from collections import defaultdict + +_THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, _THIS_DIR) +from thesis_metrics import consistency_rate, wilson_ci # noqa: E402 + + +PIPELINES = ('legacy', 'new') +DEFAULT_RESULTS_DIR = os.path.join(_THIS_DIR, 'results') + + +def _load_records(root: str): + records = defaultdict(lambda: defaultdict(list)) # records[system][pipeline] -> list + pattern = os.path.join(root, '*', '*.json') + for path in sorted(glob.glob(pattern)): + try: + with open(path, 'r', encoding='utf-8') as fh: + rec = json.load(fh) + except (OSError, json.JSONDecodeError): + continue + system = rec.get('system') or os.path.basename(os.path.dirname(path)) + pipeline = rec.get('pipeline') + if pipeline not in PIPELINES: + continue + records[system][pipeline].append(rec) + return records + + +def _summarize_cell(reps: list) -> dict: + if not reps: + return {'n': 0} + successes = sum(1 for r in reps if r.get('structural_success')) + hammings = [r['hamming'] for r in reps if r.get('hamming') is not None] + runtimes = [r['runtime_sec'] for r in reps if 'runtime_sec' in r] + discovered_tokens = [json.dumps(r.get('discovered_tokens', []), sort_keys=True) for r in reps] + rate = successes / len(reps) + ci = wilson_ci(successes, len(reps)) + mean_h = statistics.fmean(hammings) if hammings else float('nan') + mean_t = statistics.fmean(runtimes) if runtimes else float('nan') + errors = sum(1 for r in reps if 'error' in r) + return { + 'n': len(reps), + 'successes': successes, + 'rate': rate, + 'wilson_lo': ci[0], + 'wilson_hi': ci[1], + 'mean_hamming': mean_h, + 'consistency': consistency_rate(discovered_tokens), + 'mean_runtime_sec': mean_t, + 'errors': errors, + } + + +def _format_table(summary: dict) -> str: + header = ( + '| System | n | Legacy success | Legacy H | Legacy cons | ' + 'NEW success | NEW H | NEW cons | runtime (L / N) |' + ) + sep = '|---|---|---|---|---|---|---|---|---|' + rows = [header, sep] + for system in sorted(summary.keys()): + legacy = summary[system].get('legacy', {'n': 0}) + new = summary[system].get('new', {'n': 0}) + + def cell_success(c): + if c['n'] == 0: + return '-' + return ( + f"{c['rate']*100:.0f}% [{c['wilson_lo']*100:.0f}-{c['wilson_hi']*100:.0f}%] " + f"({c['successes']}/{c['n']})" + ) + + def cell_num(c, key, fmt): + if c['n'] == 0: + return '-' + v = c.get(key) + if v is None or (isinstance(v, float) and v != v): + return '-' + return fmt.format(v) + + rows.append( + f"| {system} | {max(legacy['n'], new['n'])} | " + f"{cell_success(legacy)} | {cell_num(legacy, 'mean_hamming', '{:.1f}')} | " + f"{cell_num(legacy, 'consistency', '{:.2f}')} | " + f"{cell_success(new)} | {cell_num(new, 'mean_hamming', '{:.1f}')} | " + f"{cell_num(new, 'consistency', '{:.2f}')} | " + f"{cell_num(legacy, 'mean_runtime_sec', '{:.1f}')}s / " + f"{cell_num(new, 'mean_runtime_sec', '{:.1f}')}s |" + ) + return '\n'.join(rows) + + +def aggregate(root: str = None) -> dict: + root = root or DEFAULT_RESULTS_DIR + records = _load_records(root) + summary = { + system: {pipeline: _summarize_cell(reps) for pipeline, reps in by_pipeline.items()} + for system, by_pipeline in records.items() + } + return summary + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('--root', default=DEFAULT_RESULTS_DIR, + help=f"results root to scan (default: {DEFAULT_RESULTS_DIR})") + parser.add_argument('--out', default=None, + help="path for the JSON snapshot (default: /../thesis_summary.json)") + args = parser.parse_args(argv) + + summary = aggregate(args.root) + print(_format_table(summary)) + out_path = args.out or os.path.join(_THIS_DIR, 'thesis_summary.json') + with open(out_path, 'w', encoding='utf-8') as fh: + json.dump(summary, fh, indent=2) + print(f"\nWrote {out_path}") + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/projects/thesis/thesis_metrics.py b/projects/thesis/thesis_metrics.py new file mode 100644 index 00000000..dcd13221 --- /dev/null +++ b/projects/thesis/thesis_metrics.py @@ -0,0 +1,221 @@ +""" +Structural metrics for the thesis Section 4.5 EPDE comparison. + +The metric pipeline is text-based: equations are read as strings (the +form produced by EPDE's :meth:`equations(only_str=True)`), parsed into a +canonical token representation that ignores coefficient values and term +ordering, then compared via Hamming distance / equality / modal-set +agreement across repetitions. +""" + +from __future__ import annotations + +import math +import re +from collections import Counter +from typing import Iterable, List, Sequence + +# Factor pattern: ``name{key1: val1, key2: val2, ...}`` where ``name`` can +# contain letters, digits, and the symbol characters EPDE uses for +# derivative tokens (``d``, ``u``, ``/``, ``^``, digits) and trig product +# tokens (e.g. ``cos(t)sin(x)``). +_FACTOR_RE = re.compile(r'([A-Za-z0-9_\^/\(\)]+)\s*\{([^}]*)\}') +_PARAM_RE = re.compile(r'([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([^,]+)') +_PARAM_ROUND_DIGITS = 3 + + +def _round_param(value: str): + value = value.strip() + try: + return round(float(value), _PARAM_ROUND_DIGITS) + except ValueError: + return value + + +def _parse_factor(text: str): + """Return ``(name, frozenset_of_param_items)`` or None if no factor.""" + m = _FACTOR_RE.search(text) + if m is None: + return None + name = m.group(1) + params_str = m.group(2) + params = {} + for pm in _PARAM_RE.finditer(params_str): + params[pm.group(1)] = _round_param(pm.group(2)) + return (name, frozenset(params.items())) + + +def _parse_term(term_text: str): + """Parse a single ``c * f1{...} * f2{...}`` term into a frozenset of factors. + + Pure-constant terms (e.g. ``0.0``) and terms whose leading coefficient + is numerically zero are filtered out by returning None. + """ + pieces = [p.strip() for p in term_text.split('*')] + factors = [] + coef = 1.0 + coef_seen = False + for piece in pieces: + if not piece: + continue + factor = _parse_factor(piece) + if factor is None: + # piece is a bare numeric coefficient (or unparseable scalar). + try: + val = float(piece) + coef *= val + coef_seen = True + continue + except ValueError: + # Unrecognised piece: skip rather than crash; the canonical + # set will simply omit it (and Hamming will reflect that). + continue + factors.append(factor) + + if not factors: + # Pure-constant or unparseable term -> drop. + return None + if coef_seen and abs(coef) < 1e-12: + # Zero coefficient -> term doesn't actually appear in the equation. + return None + return frozenset(factors) + + +def _canonical_equation(eq_text: str): + """Parse one equation ``rhs_sum = target`` into a canonical tuple. + + Returns ``(target_term, frozenset_of_rhs_terms)`` or None if no ``=``. + """ + if '=' not in eq_text: + return None + left, right = eq_text.split('=', 1) + target_term = _parse_term(right) + rhs_terms = [] + for term_text in left.split('+'): + term = _parse_term(term_text) + if term is not None: + rhs_terms.append(term) + return (target_term, frozenset(rhs_terms)) + + +def canonical_tokens(eq_texts: Sequence[str]) -> frozenset: + """Convert a list of equation text strings into a canonical structure. + + Each equation contributes one element to the returned frozenset: + ``(target_term, frozenset_of_rhs_terms)``. The result ignores + coefficient magnitudes, term ordering, and factor ordering within + terms; it preserves factor names + parameters (powers, freqs, dims) + rounded to :data:`_PARAM_ROUND_DIGITS` digits. + """ + out = [] + for eq in eq_texts: + if not eq.strip(): + continue + canon = _canonical_equation(eq) + if canon is not None: + out.append(canon) + return frozenset(out) + + +def hamming(discovered: frozenset, truth: frozenset) -> int: + """Term-level structural distance between two canonical equation systems. + + Equations are matched by their target (LHS) term. For each matched + target, the contribution is the cardinality of the symmetric + difference of the right-hand-side term sets — so a single missing or + extra rhs term costs 1. For equations whose target exists in only + one side, the cost is ``1 + len(rhs)`` (target mismatch plus all its + rhs terms). A pure-constant (`+ 0.0`) term is filtered out at + canonicalisation time and never contributes. + + Examples (Lorenz first equation only): + truth = {(du/dt, {a, b, c})}, discovered = {(du/dt, {a, b})} + -> hamming = 1 (one rhs term missing) + truth = {(du/dt, {a, b})}, discovered = {(dv/dt, {a, b})} + -> hamming = 1 + 2 + 1 + 2 = 6 (target differs, both sides counted) + """ + truth_by_target = {target: rhs for target, rhs in truth} + disc_by_target = {target: rhs for target, rhs in discovered} + + total = 0 + for target in set(truth_by_target) | set(disc_by_target): + truth_rhs = truth_by_target.get(target) + disc_rhs = disc_by_target.get(target) + if truth_rhs is None: + total += 1 + len(disc_rhs) + elif disc_rhs is None: + total += 1 + len(truth_rhs) + else: + total += len(truth_rhs.symmetric_difference(disc_rhs)) + return total + + +def structural_success(discovered: frozenset, truth: frozenset) -> bool: + """True iff ``discovered`` equals ``truth`` as a canonical system.""" + return hamming(discovered, truth) == 0 + + +def consistency_rate(reps_canonical: Iterable[frozenset]) -> float: + """Fraction of reps whose canonical system equals the modal canonical system.""" + reps = list(reps_canonical) + if not reps: + return 0.0 + counts = Counter(reps) + modal_count = counts.most_common(1)[0][1] + return modal_count / len(reps) + + +def wilson_ci(successes: int, n: int, z: float = 1.96): + """Wilson 95% CI for a binomial proportion.""" + if n == 0: + return (0.0, 0.0) + p = successes / n + denom = 1.0 + z * z / n + center = (p + z * z / (2 * n)) / denom + half = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denom + return (max(0.0, center - half), min(1.0, center + half)) + + +if __name__ == '__main__': + # Quick self-check: round-trip the Lorenz triple and confirm Hamming == 0 + # against itself, then perturb one term and confirm Hamming == 2. + lorenz_truth = [ + '10.0 * v{power: 1.0} + -10.0 * u{power: 1.0} = du/dx0{power: 1.0}', + '28.0 * u{power: 1.0} + -1.0 * u{power: 1.0} * w{power: 1.0} + -1.0 * v{power: 1.0} = dv/dx0{power: 1.0}', + '1.0 * u{power: 1.0} * v{power: 1.0} + -2.6666666666666665 * w{power: 1.0} = dw/dx0{power: 1.0}', + ] + canon_truth = canonical_tokens(lorenz_truth) + print('canon_truth size:', len(canon_truth)) + assert hamming(canon_truth, canon_truth) == 0 + assert structural_success(canon_truth, canon_truth) + + perturbed = list(lorenz_truth) + # Drop the -10*u term from the first equation -> one rhs term missing. + perturbed[0] = '10.0 * v{power: 1.0} = du/dx0{power: 1.0}' + canon_perturbed = canonical_tokens(perturbed) + h = hamming(canon_perturbed, canon_truth) + print('hamming(1 term missing) =', h) + assert h == 1, f"expected 1, got {h}" + + # Swap one rhs term for a different one: 1 removed + 1 added = 2. + perturbed2 = list(lorenz_truth) + perturbed2[0] = ('10.0 * v{power: 1.0} + -10.0 * u{power: 2.0} ' + '= du/dx0{power: 1.0}') + h2 = hamming(canonical_tokens(perturbed2), canon_truth) + print('hamming(1 term swapped) =', h2) + assert h2 == 2, f"expected 2, got {h2}" + + # Adding a pure-constant `+ 0.0` term must NOT change the canonical form. + with_zero = list(lorenz_truth) + with_zero[0] = '10.0 * v{power: 1.0} + -10.0 * u{power: 1.0} + 0.0 = du/dx0{power: 1.0}' + h_zero = hamming(canonical_tokens(with_zero), canon_truth) + print('hamming(+0.0 added) =', h_zero) + assert h_zero == 0, f"expected 0, got {h_zero}" + + # Drop a whole equation -> target + its 2 rhs terms = 3. + perturbed3 = list(lorenz_truth[:2]) + h3 = hamming(canonical_tokens(perturbed3), canon_truth) + print('hamming(1 equation missing) =', h3) + assert h3 == 3, f"expected 3, got {h3}" + + print('thesis_metrics self-check OK') diff --git a/projects/thesis/thesis_runner.py b/projects/thesis/thesis_runner.py new file mode 100644 index 00000000..6c8a4d65 --- /dev/null +++ b/projects/thesis/thesis_runner.py @@ -0,0 +1,522 @@ +""" +Shared runner module for the thesis Section 4.5 within-platform comparison. + +This module is the single source of truth for: + * the 8-cell pipeline table (``_PIPELINE_SETTINGS`` -> ``pipeline_settings``) + * the ``SystemCfg`` dataclass consumed by ``build_search`` / ``run_one`` + * ``run_smoke`` (batched per-rep JSON dumps with resume-on-restart) + * ``load_config`` for parsing per-system YAML configs + +Per-system configuration lives at: + + projects/thesis/configs/.yaml (declarative: truth equations, + output dir, data_fun_pow, ...) + projects/thesis/adapters/.py (Python: load_data(), + build_extra_tokens(coords, dim)) + +Pipeline selection (``legacy`` vs ``new`` is the main thesis comparison; the +six ablation cells off the 000/111 diagonal cover the 2x2x2 factorial): + + LEGACY -> L2Fitness + LASSOSparsity + use_pic=False + NEW -> L2LRFitness + VWSRSparsity + use_pic=True +""" + +from __future__ import annotations + +import importlib +import json +import os +import sys +import time +import traceback +from dataclasses import dataclass, field +from typing import Any, Callable, Iterable, Optional + +import numpy as np +import torch + +# Make sure the EPDE package is importable when running this module's CLI +# entries directly (``python projects/thesis/run.py lv``). +_THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +_REPO_ROOT = os.path.abspath(os.path.join(_THIS_DIR, '..', '..')) +if _REPO_ROOT not in sys.path: + sys.path.insert(0, _REPO_ROOT) +if _THIS_DIR not in sys.path: + sys.path.insert(0, _THIS_DIR) + +from epde.interface.interface import EpdeSearch # noqa: E402 +from epde.operators.common.fitness import L2Fitness, L2LRFitness # noqa: E402 +from epde.operators.common.sparsity import LASSOSparsity, VWSRSparsity # noqa: E402 +from epde import GridTokens # noqa: E402 + + +CONFIGS_DIR = os.path.join(_THIS_DIR, 'configs') +ADAPTERS_DIR = os.path.join(_THIS_DIR, 'adapters') +RESULTS_DIR = os.path.join(_THIS_DIR, 'results') + + +# Full 2x2x2 ablation table for the three thesis-NEW contributions: +# (1) WAPE fitness -> L2LRFitness vs LEGACY L2Fitness +# (2) Instability obj -> use_pic=True swaps MOEA/D's 2nd objective +# (equation_terms_stability) vs LEGACY +# (equation_complexity_by_factors) +# (3) Novel regularizer -> VWSRSparsity (PhysicsInformedLasso, CV-weighted) +# vs LEGACY LASSOSparsity (sklearn.Lasso) +_PIPELINE_SETTINGS = { + 'legacy': {'fitness_cls': L2Fitness, 'sparsity_cls': LASSOSparsity, 'use_pic': False}, + 'wape': {'fitness_cls': L2LRFitness, 'sparsity_cls': LASSOSparsity, 'use_pic': False}, + 'instab': {'fitness_cls': L2Fitness, 'sparsity_cls': LASSOSparsity, 'use_pic': True}, + 'reg': {'fitness_cls': L2Fitness, 'sparsity_cls': VWSRSparsity, 'use_pic': False}, + 'wape_instab': {'fitness_cls': L2LRFitness, 'sparsity_cls': LASSOSparsity, 'use_pic': True}, + 'wape_reg': {'fitness_cls': L2LRFitness, 'sparsity_cls': VWSRSparsity, 'use_pic': False}, + 'instab_reg': {'fitness_cls': L2Fitness, 'sparsity_cls': VWSRSparsity, 'use_pic': True}, + 'new': {'fitness_cls': L2LRFitness, 'sparsity_cls': VWSRSparsity, 'use_pic': True}, +} + +# Default pipelines for the main Section 4.5 comparison. +PIPELINES = ('legacy', 'new') + +# Off-diagonal cells of the 2x2x2 factorial -- pass to ``run_smoke`` as the +# ``pipelines`` argument from the ablation entry point. Excludes +# ``legacy`` and ``new`` since their reps live in the default results tree +# (the 000 and 111 corners of the cube). +ABLATION_PIPELINES = ( + 'wape', 'instab', 'reg', + 'wape_instab', 'wape_reg', 'instab_reg', +) + + +def pipeline_settings(pipeline: str) -> dict: + """Return ``EpdeSearch`` kwargs for a single pipeline label. + + Recognises the original two labels (``legacy``, ``new``) and the six + off-diagonal ablation labels. Forward the returned dict directly to + :class:`EpdeSearch` (``use_pic``, ``fitness_cls``, ``sparsity_cls``). + """ + try: + return dict(_PIPELINE_SETTINGS[pipeline]) + except KeyError: + raise ValueError( + f"Unknown pipeline {pipeline!r}; expected one of {tuple(_PIPELINE_SETTINGS)}" + ) + + +@dataclass +class SystemCfg: + """Per-system configuration consumed by :func:`run_one`. + + name: short system identifier used in output filenames. + truth_tokens: canonical token set encoding the ground-truth equations. + outdir: directory to write per-rep JSON results into. + load_data: callable returning ``(coordinate_tensors, data_list, + variable_names, dimensionality)``. ``dimensionality`` is ``0`` for + ODE systems and the number of spatial axes for PDE systems. + build_extra_tokens: optional callable returning extra EPDE tokens + beyond the auto-added ``GridTokens``. Signature + ``(coords, dim) -> list``. Default: returns ``[]``. + """ + + name: str + truth_tokens: frozenset + outdir: str + load_data: Callable[[], tuple] + build_extra_tokens: Callable[[Any, int], list] = field( + default_factory=lambda: (lambda coords, dim: []) + ) + data_fun_pow: int = 3 + early_stop_on_truth: bool = True + + +def load_config(name_or_path: str) -> SystemCfg: + """Resolve a YAML config into a fully-populated :class:`SystemCfg`. + + ``name_or_path`` may be a bare system name (e.g. ``"lv"`` -- looked up + as ``configs/lv.yaml``) or an explicit path to a YAML file (relative + paths are resolved against the current working directory). + + Schema (see ``configs/.yaml`` for examples): + name: str # required + truth_equations: list[str] # required; canonicalised at load time + adapter: str # optional; defaults to ``name`` + outdir: str # optional; defaults to ``name`` (under results/) + data_fun_pow: int # optional; default 3 + early_stop_on_truth: bool # optional; default True + + Returns the populated dataclass. The adapter module is imported via + ``importlib`` from ``projects/thesis/adapters/.py``; it must + export ``load_data`` and may optionally export ``build_extra_tokens``. + """ + import yaml # local import: only required when YAML configs are used + + yaml_path = ( + name_or_path + if os.path.sep in name_or_path or name_or_path.endswith('.yaml') + else os.path.join(CONFIGS_DIR, f'{name_or_path}.yaml') + ) + yaml_path = os.path.abspath(yaml_path) + if not os.path.exists(yaml_path): + raise FileNotFoundError(f"config not found: {yaml_path}") + + with open(yaml_path, 'r', encoding='utf-8') as f: + d = yaml.safe_load(f) or {} + + name = d.get('name') + if not name: + raise ValueError(f"{yaml_path}: 'name' is required") + + from thesis_metrics import canonical_tokens + truth_equations = d.get('truth_equations') or [] + truth_tokens = canonical_tokens(truth_equations) + + adapter_name = d.get('adapter', name) + if ADAPTERS_DIR not in sys.path: + sys.path.insert(0, _THIS_DIR) + adapter_mod = importlib.import_module(f'adapters.{adapter_name}') + + if not hasattr(adapter_mod, 'load_data'): + raise AttributeError( + f"adapter {adapter_name!r} must export load_data() -> " + "(coords, data, variable_names, dim)" + ) + + outdir_rel = d.get('outdir', name) + outdir = ( + outdir_rel + if os.path.isabs(outdir_rel) + else os.path.abspath(os.path.join(RESULTS_DIR, outdir_rel)) + ) + + kwargs: dict = dict( + name=name, + truth_tokens=truth_tokens, + outdir=outdir, + load_data=adapter_mod.load_data, + ) + if hasattr(adapter_mod, 'build_extra_tokens'): + kwargs['build_extra_tokens'] = adapter_mod.build_extra_tokens + if 'data_fun_pow' in d: + kwargs['data_fun_pow'] = int(d['data_fun_pow']) + if 'early_stop_on_truth' in d: + kwargs['early_stop_on_truth'] = bool(d['early_stop_on_truth']) + return SystemCfg(**kwargs) + + +def _boundary_for(coords) -> Any: + """Return ``10%``-of-axis boundary for the supplied EPDE coordinate tensors. + + ODE problems pass a single 1-D array via ``(t,)``; the returned + boundary is a scalar ``len(t) // 10``. PDE problems pass a meshgrid + tuple where every array has the same multidimensional shape; the + returned boundary is a per-axis tuple of ``axis_size // 10``. + """ + sample = np.asarray(coords[0]) + if sample.ndim <= 1: + return max(1, len(sample) // 10) + return tuple(max(1, n // 10) for n in sample.shape) + + +def _build_truth_match_callback(cfg: 'SystemCfg') -> Callable: + """Return a per-epoch callback that stops MOEA/D once any Pareto-0 + candidate canonically matches ``cfg.truth_tokens``. + """ + from thesis_metrics import canonical_tokens, structural_success + truth = cfg.truth_tokens + + def _cb(snapshot, epoch_idx): + for entry in snapshot: + text = entry.get('text_form', '') if isinstance(entry, dict) else str(entry) + lines = [line for line in text.split('\n') if line.strip()] + try: + canon = canonical_tokens(lines) + except Exception: + continue + if structural_success(canon, truth): + return True + return False + + return _cb + + +def build_search(cfg: 'SystemCfg', pipeline_kwargs: dict) -> EpdeSearch: + """Universal EPDE search builder for the thesis Section 4.5 comparison. + + Hyperparameters are uniform across all benchmark systems; the only + branches are ODE vs PDE (deriv order, grid-token labels, boundary + shape) and the per-system data / extra-token callbacks declared on + ``cfg``. Pipeline selection is forwarded through ``pipeline_kwargs``. + """ + coords, data, variable_names, dim = cfg.load_data() + boundary = _boundary_for(coords) + max_deriv_order = (2,) if dim == 0 else (2, 4) + + grid_labels = ['x_0'] if dim == 0 else [f'x_{i}' for i in range(dim + 1)] + grid_tokens = GridTokens(grid_labels, dimensionality=dim, max_power=2) + additional_tokens = [grid_tokens] + list(cfg.build_extra_tokens(coords, dim)) + + search = EpdeSearch( + use_solver=False, + multiobjective_mode=True, + boundary=boundary, + coordinate_tensors=coords, + verbose_params={'show_iter_idx': True}, + device='cuda', + **pipeline_kwargs, + ) + search.set_preprocessor(default_preprocessor_type='FD', preprocessor_kwargs={}) + + early_stop_cb = _build_truth_match_callback(cfg) if cfg.early_stop_on_truth else None + search.set_moeadd_params(population_size=16, training_epochs=5, + early_stopping_callback=early_stop_cb) + + search.fit( + data=data, + variable_names=variable_names, + max_deriv_order=max_deriv_order, + derivs=None, + equation_terms_max_number=10, + data_fun_pow=cfg.data_fun_pow, + deriv_fun_pow=2, + additional_tokens=additional_tokens, + equation_factors_max_number={'factors_num': [1, 2], 'probas': [0.65, 0.35]}, + eq_sparsity_interval=(1e-5, 1e0), + fourier_layers=False, + ) + return search + + +def _set_seeds(seed: int) -> None: + np.random.seed(seed) + torch.manual_seed(seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(seed) + + +def _tokens_to_json(tokens) -> list: + """Recursively convert a canonical token structure into JSON-friendly lists.""" + def factor(f): + name, params = f + return [name, sorted(([k, v] for k, v in params), key=lambda p: p[0])] + + def term(t): + return sorted([factor(f) for f in t], key=lambda f: (f[0], repr(f[1]))) + + out = [] + for target, rhs in tokens: + out.append([ + term(target), + sorted((term(t) for t in rhs), key=lambda x: repr(x)), + ]) + return sorted(out, key=lambda x: repr(x)) + + +def _discovery_epochs(final_token_sets, pareto_history) -> list: + """For each canonical token set in ``final_token_sets``, return the + first epoch index (0-based) in ``pareto_history`` whose Pareto-0 + snapshot contains a solution with the same canonical structure. + """ + from thesis_metrics import canonical_tokens + + snapshot_canon = [] + for epoch_snapshot in pareto_history: + per_epoch = [] + for sol_record in epoch_snapshot: + text = sol_record.get('text_form', '') if isinstance(sol_record, dict) else str(sol_record) + lines = [line for line in text.split('\n') if line.strip()] + per_epoch.append(canonical_tokens(lines)) + snapshot_canon.append(per_epoch) + + epochs = [] + for target in final_token_sets: + first = None + for epoch_idx, epoch_canon in enumerate(snapshot_canon): + if any(c == target for c in epoch_canon): + first = epoch_idx + break + epochs.append(first) + return epochs + + +def _extract_discovered(search: EpdeSearch) -> list: + """Return all solutions from the non-dominated Pareto level.""" + eqs = search.equations(only_print=False, only_str=True, num=1) + if not eqs: + return [] + if isinstance(eqs[0], list): + level0_solutions = eqs[0] + else: + level0_solutions = eqs + + out = [] + for solution in level0_solutions: + if not isinstance(solution, str): + solution = str(solution) + out.append([line for line in solution.split('\n') if line.strip()]) + return out + + +def _extract_objectives(search: EpdeSearch) -> list: + """Return per-solution objective vectors aligned with ``_extract_discovered``.""" + try: + level0 = search.optimizer.pareto_levels.levels[0] + except Exception: + return [] + out = [] + for sol in level0: + try: + obj = sol.obj_fun.tolist() if hasattr(sol.obj_fun, 'tolist') else list(sol.obj_fun) + except Exception: + obj = None + out.append(obj) + return out + + +def run_one(system_cfg: SystemCfg, pipeline: str, seed: int) -> dict: + """Run a single (system, pipeline, seed) repetition. + + Returns a dict suitable for JSON serialization. Exceptions are caught + and recorded as ``error`` and ``traceback`` fields so a failing rep + does not kill the batch. + """ + from thesis_metrics import canonical_tokens, hamming, structural_success + + pipeline_kwargs = pipeline_settings(pipeline) + _set_seeds(seed) + + record: dict = { + 'system': system_cfg.name, + 'pipeline': pipeline, + 'seed': seed, + 'pipeline_kwargs': { + 'use_pic': pipeline_kwargs['use_pic'], + 'fitness_cls': pipeline_kwargs['fitness_cls'].__name__, + 'sparsity_cls': pipeline_kwargs['sparsity_cls'].__name__, + }, + } + + t0 = time.time() + try: + search = build_search(system_cfg, pipeline_kwargs) + elapsed = time.time() - t0 + solutions_text = _extract_discovered(search) + objectives_per_solution = _extract_objectives(search) + per_solution_tokens = [canonical_tokens(sol) for sol in solutions_text] + pareto_history = list(getattr(search, 'pareto_history', [])) + if per_solution_tokens: + hammings = [hamming(c, system_cfg.truth_tokens) for c in per_solution_tokens] + best_idx = int(min(range(len(hammings)), key=lambda i: hammings[i])) + discovery_epochs = _discovery_epochs(per_solution_tokens, pareto_history) + best_objectives = ( + objectives_per_solution[best_idx] + if best_idx < len(objectives_per_solution) else None + ) + record.update({ + 'runtime_sec': elapsed, + 'n_pareto_solutions': len(per_solution_tokens), + 'discovered_text_per_solution': solutions_text, + 'discovered_text': solutions_text[best_idx], + 'discovered_tokens_per_solution': [_tokens_to_json(c) for c in per_solution_tokens], + 'discovered_tokens': _tokens_to_json(per_solution_tokens[best_idx]), + 'truth_tokens': _tokens_to_json(system_cfg.truth_tokens), + 'hamming_per_solution': hammings, + 'hamming': hammings[best_idx], + 'discovery_epoch_per_solution': discovery_epochs, + 'discovery_epoch': discovery_epochs[best_idx], + 'n_epochs': len(pareto_history), + 'objectives_per_solution': objectives_per_solution, + 'objectives': best_objectives, + 'structural_success': any( + structural_success(c, system_cfg.truth_tokens) for c in per_solution_tokens + ), + }) + else: + record.update({ + 'runtime_sec': elapsed, + 'n_pareto_solutions': 0, + 'discovered_text_per_solution': [], + 'discovered_text': [], + 'discovered_tokens_per_solution': [], + 'discovered_tokens': [], + 'truth_tokens': _tokens_to_json(system_cfg.truth_tokens), + 'hamming_per_solution': [], + 'hamming': None, + 'objectives_per_solution': [], + 'objectives': None, + 'structural_success': False, + }) + except Exception as exc: # pragma: no cover - smoke-time diagnostic + record.update({ + 'runtime_sec': time.time() - t0, + 'error': repr(exc), + 'traceback': traceback.format_exc(), + 'n_pareto_solutions': 0, + 'discovered_text_per_solution': [], + 'discovered_text': [], + 'discovered_tokens': [], + 'hamming': None, + 'objectives_per_solution': [], + 'objectives': None, + 'structural_success': False, + }) + return record + + +def _resolve_out_root(system_cfg: SystemCfg, outdir: Optional[str]) -> str: + """Resolve the final output directory for a batch. + + Default (``outdir is None``) -> ``system_cfg.outdir`` (typically + ``projects/thesis/results/``). + Absolute path -> used as-is. + Bare tag (e.g. ``ablation_v2``) -> ``results//``, so a + tagged sweep across all systems + stays grouped under one folder + (``results//lv``, .../lorenz, ...) + and the aggregator can scan a tag in + one glob. + """ + if outdir is None: + return system_cfg.outdir + if os.path.isabs(outdir): + return outdir + return os.path.join(RESULTS_DIR, outdir, system_cfg.name) + + +def run_smoke( + system_cfg: SystemCfg, + reps: int = 3, + pipelines: Iterable[str] = PIPELINES, + seed_base: int = 0, + resume: bool = True, + outdir: Optional[str] = None, +) -> None: + """Run ``reps`` × len(pipelines) repetitions and write JSON per rep. + + With ``resume=True`` (default) any ``(pipeline, rep)`` whose target JSON + already exists and parses as JSON is skipped. Pass ``resume=False`` to + overwrite. See :func:`_resolve_out_root` for ``outdir`` semantics. + """ + out_root = _resolve_out_root(system_cfg, outdir) + os.makedirs(out_root, exist_ok=True) + for pipeline in pipelines: + for rep in range(reps): + seed = seed_base + rep + out_path = os.path.join(out_root, f"{pipeline}_rep{rep:02d}.json") + if resume and os.path.exists(out_path): + try: + with open(out_path, 'r', encoding='utf-8') as fh: + json.load(fh) + print(f"\n========== {system_cfg.name} / {pipeline} / rep {rep} -- " + f"skipping (resume; {out_path} exists) ==========") + continue + except (json.JSONDecodeError, OSError) as exc: + print(f"[resume] {out_path} unreadable ({exc!r}); re-running rep") + print(f"\n========== {system_cfg.name} / {pipeline} / rep {rep} (seed={seed}) ==========") + record = run_one(system_cfg, pipeline, seed) + with open(out_path, 'w', encoding='utf-8') as fh: + json.dump(record, fh, indent=2, default=str) + status = 'OK' if 'error' not in record else 'FAIL' + ham = record.get('hamming') + epoch = record.get('discovery_epoch') + n_ep = record.get('n_epochs') + epoch_str = f"epoch={epoch}/{n_ep}" if epoch is not None else "epoch=?" + print(f" -> {status} hamming={ham} {epoch_str} time={record.get('runtime_sec', 0.0):.1f}s") + print(f" -> saved {out_path}") diff --git a/tests/unit/test_main_structures_characterization.py b/tests/unit/test_main_structures_characterization.py new file mode 100644 index 00000000..4d9b4c34 --- /dev/null +++ b/tests/unit/test_main_structures_characterization.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Characterization tests for ``epde/structure/main_structures.py``. + +These tests pin CURRENT behavior (correct or buggy) so the upcoming refactoring +phases can detect regressions. See ``PLAN_main_structures_refinement.md`` for +the staged roadmap they support. + +Some tests pin observed bugs (most prominently the mutable default +metaparameters in ``Equation.__init__`` at l.391-395). Phase 2 fixes those +bugs; the relevant test expectations will flip in the same commit that lands +each fix. +""" + +import copy +from collections import OrderedDict + +import numpy as np +import pytest + +import epde.globals as global_var +from epde.cache.cache import upload_grids, upload_simple_tokens +from epde.evaluators import simple_function_evaluator +from epde.interface.equation_translator import translate_equation +from epde.interface.token_family import TFPool, TokenFamily +from epde.structure.main_structures import Equation + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture(scope="module") +def basic_pool(): + """A minimal pool with two derivative-family tokens (``u`` and ``du/dx0``). + + Avoids ANN training and heavy preprocessing — we only need a pool whose + factors have valid cache labels, evaluator linkage, and pool back-references + so that ``Term``/``Equation``/``SoEq`` construction and deepcopy succeed. + """ + grid = np.linspace(0.0, 4.0 * np.pi, 50) + u = np.sin(grid) + du = np.cos(grid) + + global_var.init_caches(set_grids=True) + global_var.set_time_axis(0) + global_var.init_verbose(show_warnings=False) + global_var.tensor_cache.memory_usage_properties( + obj_test_case=u, mem_for_cache_frac=5) + global_var.grid_cache.memory_usage_properties( + obj_test_case=grid, mem_for_cache_frac=5) + + upload_grids([grid], global_var.grid_cache) + + deriv_names = ['u', 'du/dx0'] + deriv_orders = [[None,], [0,]] + deriv_tensors = np.stack([u, du], axis=0) + upload_simple_tokens(deriv_names, global_var.tensor_cache, deriv_tensors) + global_var.tensor_cache.use_structural() + + u_family = TokenFamily('u', variable='u', family_of_derivs=True) + u_family.set_status(demands_equation=True, unique_specific_token=False, + unique_token_type=False, s_and_d_merged=False, + meaningful=True) + u_family.set_params(deriv_names, OrderedDict([('power', (1, 1))]), + {'power': 0}, deriv_orders) + u_family.set_evaluator(simple_function_evaluator) + + return TFPool([u_family]) + + +def _build_soeq(pool): + text = '1.0 * u{power: 1} + 0.0 = du/dx0{power: 1}' + soeq = translate_equation(text, pool, all_vars=['u']) + # translate_equation assigns weights via the setter but does not flip + # weights_internal_evald. Set it so terms_labels_without_power can run + # without raising AttributeError ("Internal weights called before init"). + eq = soeq.vals['u'] + eq.weights_internal_evald = True + return soeq + + +@pytest.fixture +def soeq(basic_pool): + return _build_soeq(basic_pool) + + +@pytest.fixture +def equation(soeq): + return soeq.vals['u'] + + +@pytest.fixture +def term(equation): + return equation.structure[0] + + +# --------------------------------------------------------------------------- +# 1. TestTermDeepcopy +# --------------------------------------------------------------------------- + +class TestTermDeepcopy: + def test_returns_distinct_object(self, term): + copy_t = copy.deepcopy(term) + assert id(copy_t) != id(term) + + def test_equal_to_original(self, term): + copy_t = copy.deepcopy(term) + assert copy_t == term + + def test_structure_is_fresh(self, term): + copy_t = copy.deepcopy(term) + assert copy_t.structure is not term.structure + for c_factor, o_factor in zip(copy_t.structure, term.structure): + assert c_factor is not o_factor + + def test_preserves_name(self, term): + copy_t = copy.deepcopy(term) + assert copy_t.name == term.name + + def test_preserves_cache_label(self, term): + copy_t = copy.deepcopy(term) + assert copy_t.cache_label == term.cache_label + + +# --------------------------------------------------------------------------- +# 2. TestEquationDeepcopy +# --------------------------------------------------------------------------- + +class TestEquationDeepcopy: + def test_returns_distinct_object(self, equation): + copy_e = copy.deepcopy(equation) + assert id(copy_e) != id(equation) + + def test_equal_to_original(self, equation): + copy_e = copy.deepcopy(equation) + assert copy_e == equation + + def test_structure_is_fresh(self, equation): + copy_e = copy.deepcopy(equation) + assert copy_e.structure is not equation.structure + for c_term, o_term in zip(copy_e.structure, equation.structure): + assert c_term is not o_term + + def test_eval_cache_after_deepcopy_is_fresh_dict(self, equation): + """Pin: __deepcopy__ traverses the _eval_cache slot, so the copy + owns its own dict (initially empty, equal to source's empty dict). + """ + copy_e = copy.deepcopy(equation) + assert copy_e._eval_cache is not equation._eval_cache + assert copy_e._eval_cache == equation._eval_cache + + +# --------------------------------------------------------------------------- +# 3. TestSoEqDeepcopy +# --------------------------------------------------------------------------- + +class TestSoEqDeepcopy: + def test_returns_distinct_object(self, soeq): + copy_s = copy.deepcopy(soeq) + assert id(copy_s) != id(soeq) + + def test_dict_attrs_present(self, soeq): + """Pin current dual-traversal: __dict__ keys are all carried over.""" + copy_s = copy.deepcopy(soeq) + for key in soeq.__dict__: + assert hasattr(copy_s, key) + + def test_vals_independent(self, soeq): + """The chromosome is itself deepcopied, not aliased.""" + copy_s = copy.deepcopy(soeq) + assert copy_s.vals is not soeq.vals + + +# --------------------------------------------------------------------------- +# 4. TestEquationLabelProperties +# --------------------------------------------------------------------------- + +class TestEquationLabelProperties: + def test_terms_labels_is_frozenset_of_frozensets(self, equation): + labels = equation.terms_labels + assert isinstance(labels, frozenset) + for inner in labels: + assert isinstance(inner, frozenset) + + def test_terms_labels_count_matches_unique_terms(self, equation): + # Two distinct terms (u and du/dx0) → two frozenset entries. + assert len(equation.terms_labels) == len(equation.structure) + + def test_terms_labels_without_power_is_frozenset(self, equation): + labels = equation.terms_labels_without_power + assert isinstance(labels, frozenset) + + def test_terms_labels_stable_across_calls(self, equation): + # Calling twice in a row returns equal results (no hidden state). + first = equation.terms_labels + second = equation.terms_labels + assert first == second + + +# --------------------------------------------------------------------------- +# 6. TestRenameAliases (Phase 3) +# +# Pin the alias contract: deprecated old names delegate to new names with +# identical results. If a future commit drops an alias, this test catches it. +# --------------------------------------------------------------------------- + +class TestRenameAliases: + def test_term_alias_factors_labels(self, term): + assert term.factors_labels == term.term_label + + def test_term_alias_factors_labels_without_power(self, term): + assert term.factors_labels_without_power == term.term_label_without_power + + def test_soeq_alias_equations_labels(self, soeq): + assert soeq.equations_labels == soeq.terms_labels + + def test_soeq_alias_equations_labels_without_power(self, soeq): + assert soeq.equations_labels_without_power == soeq.terms_labels_without_power + + +# --------------------------------------------------------------------------- +# 7. TestEquationLabelsAfterTermMutation +# +# terms_labels / terms_labels_without_power are memoized in slot caches +# (_terms_labels_cache, _terms_labels_without_power_cache). Mutation paths +# that touch self.structure or its terms must call _invalidate_label_cache() +# afterward (15 known call sites cover this). These tests pin the new +# contract: fresh result on first access populates the cache, repeated +# access returns the same frozenset, and invalidation drops the cache. +# --------------------------------------------------------------------------- + +class TestEquationLabelsAfterTermMutation: + def test_terms_labels_reflect_structure_append(self, equation): + before = equation.terms_labels + equation.structure.append(copy.deepcopy(equation.structure[0])) + # Manual structure append bypasses Equation's mutation API and the + # cache; an explicit invalidation is the contract for callers that + # touch self.structure directly. + equation._invalidate_label_cache() + after = equation.terms_labels + # frozenset of frozensets — appending a duplicate keeps the frozenset + # the same size (set semantics) but len(structure) grows. + assert len(after) <= len(before) + 1 + assert len(after) <= len(equation.structure) + + def test_terms_labels_populates_cache(self, equation): + # First access computes and stores; subsequent accesses return the + # identical frozenset (cache hit, not a recomputation). + assert equation._terms_labels_cache is None + first = equation.terms_labels + assert equation._terms_labels_cache is first + second = equation.terms_labels + assert second is first + + def test_invalidate_helper_drops_cache(self, equation): + # Calling the helper on a populated equation drops both caches, so + # the next read recomputes from the current structure. + _ = equation.terms_labels + _ = equation.terms_labels_without_power + assert equation._terms_labels_cache is not None + assert equation._terms_labels_without_power_cache is not None + equation._invalidate_label_cache() + assert equation._terms_labels_cache is None + assert equation._terms_labels_without_power_cache is None + + def test_factors_labels_alias_on_equation(self, equation): + # Phase 3 added factors_labels on Term; mutations.py:127 also reads + # it on Equation (treating the names as interchangeable). Pin the alias. + assert equation.factors_labels == equation.terms_labels + assert equation.factors_labels_without_power == equation.terms_labels_without_power + + +# --------------------------------------------------------------------------- +# 8. TestFilterTokensByRightPartExhaustion (Phase 6) +# +# Pin: filter_tokens_by_right_part raises RuntimeError when it cannot find +# a unique term within the retry budget. Pre-Phase-6 the function looped +# forever (or warned and continued); Phase 6 caps retries with a hard fail. +# --------------------------------------------------------------------------- + +class TestFilterTokensByRightPartExhaustion: + def test_raises_runtimeerror_on_exhaustion(self, equation): + import warnings as _w + + # The deprecated function reads factor.status['unique_for_right_part']; + # patch it onto our test factors (the fixture uses the modern token- + # family schema where this key is absent). + for t in equation.structure: + for f in t.structure: + f.status['unique_for_right_part'] = False + + # Force a duplicate so terms_labels never matches len(structure) + # — guaranteeing the loop never breaks out via success. + equation.structure.append(copy.deepcopy(equation.structure[0])) + equation._invalidate_label_cache() + + target = equation.structure[equation.target_idx] + candidate = equation.structure[0] + + with _w.catch_warnings(): + _w.simplefilter('ignore', DeprecationWarning) + with pytest.raises(RuntimeError, match='filter_tokens_by_right_part'): + candidate.filter_tokens_by_right_part( + target, equation, equation_position=0, max_retries=1) + + +# --------------------------------------------------------------------------- +# 5. TestEquationDefaultMetaparameters +# +# After Phase 2: each Equation gets its OWN deep-copied default metaparameters +# dict, so mutating one cannot leak into another. Pre-Phase-2 this test +# asserted the opposite (shared mutation). The flip is the visible artifact +# that the bug at the old l.391-395 has been fixed. +# --------------------------------------------------------------------------- + +class TestEquationDefaultMetaparameters: + def test_two_equations_have_independent_default_metaparameters(self, basic_pool): + # Default terms_number is 5; passing five basic terms skips the + # random-padding loop entirely (range(5, 5) is empty). + eq1 = Equation(basic_pool, basic_structure=['u'] * 5, + var_to_explain='u') + eq2 = Equation(basic_pool, basic_structure=['u'] * 5, + var_to_explain='u') + # Each sees the documented default value. + assert eq1.metaparameters['sparsity']['value'] == 1.0 + assert eq2.metaparameters['sparsity']['value'] == 1.0 + + eq1.metaparameters['sparsity']['value'] = 999.0 + # Mutation MUST stay local — the dict objects are independent. + assert eq2.metaparameters['sparsity']['value'] == 1.0 + assert eq1.metaparameters is not eq2.metaparameters