diff --git a/main.py b/main.py index 988e93c..3df2ccd 100644 --- a/main.py +++ b/main.py @@ -1,451 +1,304 @@ import numpy as np import matplotlib.pyplot as plt -from typing import Callable, List, Tuple, Optional, Dict, Any, Union -import dataclasses -from enum import Enum -import inspect -from visualizer import * +from typing import Dict, List, Callable, Any, Optional +import networkx as nx +from functools import lru_cache +import sympy as sp +from collections import defaultdict -# ====================== ТИПЫ ЗАВИСИМОСТЕЙ ====================== - -class DependencyType(Enum): - """Типы зависимостей между параметрами""" - INDEPENDENT = "independent" # Независимый параметр - DEPENDENT = "dependent" # Зависимый параметр - EXPRESSION = "expression" # Выражение, использующее другие параметры - -# ====================== КЛАССЫ ДЛЯ ПАРАМЕТРОВ ====================== - -@dataclasses.dataclass class Parameter: - """Базовый класс для параметра""" - name: str - param_type: DependencyType - description: str = "" + """Класс для представления параметра с его формулой и зависимостями""" - def evaluate(self, context: Dict[str, Any]) -> np.ndarray: - """Вычисляет значение параметра в заданном контексте""" - raise NotImplementedError - -@dataclasses.dataclass -class IndependentParameter(Parameter): - """Независимый параметр (базовый)""" - formula: Callable - x_range: Tuple[float, float] - num_points: int = 1000 - color: str = 'blue' - line_style: str = '-' - - def __post_init__(self): - self.param_type = DependencyType.INDEPENDENT - - def evaluate(self, context: Dict[str, Any]) -> np.ndarray: - """Вычисляет значение независимого параметра""" - x = context.get('x', np.linspace(self.x_range[0], self.x_range[1], self.num_points)) - return self.formula(x) - -@dataclasses.dataclass -class DependentParameter(Parameter): - """Зависимый параметр (выражается через другие параметры)""" - expression: Callable # Функция, которая использует другие параметры - dependencies: List[str] # Имена параметров, от которых зависит - color: str = 'green' - line_style: str = '--' - - def __post_init__(self): - self.param_type = DependencyType.DEPENDENT - - def evaluate(self, context: Dict[str, Any]) -> np.ndarray: - """Вычисляет значение зависимого параметра""" - # Собираем значения зависимостей из контекста - dep_values = {} - for dep_name in self.dependencies: - if dep_name not in context: - raise ValueError(f"Зависимость '{dep_name}' не найдена в контексте") - dep_values[dep_name] = context[dep_name] + def __init__(self, name: str, formula: Callable, dependencies: List[str] = None, + description: str = "", units: str = ""): + self.name = name + self.formula = formula # Функция для вычисления параметра + self.dependencies = dependencies or [] # Список зависимых параметров + self.description = description + self.units = units + self.cache = {} # Кэш для мемоизации - # Вычисляем выражение - return self.expression(**dep_values) - -# ====================== КЛАСС ДЛЯ ГАРМОНИК ====================== - -class HarmonicOscillation: - """Класс для описания гармонического колебания""" - def __init__(self, amplitude: Union[float, str], - frequency: Union[float, str], - phase: Union[float, str] = 0, - amplitude_depends_on: Optional[List[str]] = None, - frequency_depends_on: Optional[List[str]] = None, - phase_depends_on: Optional[List[str]] = None): - """ - Параметры гармоники могут быть как числами, так и именами параметров модели - """ - self.amplitude = amplitude - self.frequency = frequency - self.phase = phase + def calculate(self, x_values: np.ndarray, param_values: Dict[str, np.ndarray]) -> np.ndarray: + """Вычисление значений параметра для заданных x""" + # Проверка кэша + cache_key = (tuple(x_values), tuple(sorted(param_values.items()))) + if cache_key in self.cache: + return self.cache[cache_key] - # Отслеживаем зависимости - self.amplitude_depends_on = amplitude_depends_on or [] - self.frequency_depends_on = frequency_depends_on or [] - self.phase_depends_on = phase_depends_on or [] - - def get_dependencies(self) -> List[str]: - """Возвращает все зависимости гармоники""" - return (self.amplitude_depends_on + - self.frequency_depends_on + - self.phase_depends_on) - - def evaluate(self, t: np.ndarray, context: Dict[str, Any]) -> np.ndarray: - """Вычисляет значение гармонического колебания с учетом зависимостей""" - - # Получаем значения амплитуды, частоты и фазы - amp = self._get_value(self.amplitude, context) - freq = self._get_value(self.frequency, context) - phase = self._get_value(self.phase, context) - - return amp * np.sin(2 * np.pi * freq * t + phase) - - def _get_value(self, param: Any, context: Dict[str, Any]) -> float: - """Получает значение параметра (число или из контекста)""" - if isinstance(param, (int, float)): - return param - elif isinstance(param, str) and param in context: - # Если параметр - строка, берем соответствующее значение из контекста - val = context[param] - # Если это массив, берем среднее или первое значение - if isinstance(val, np.ndarray): - return np.mean(val) # или val[0] в зависимости от логики - return float(val) + # Вычисление + if self.dependencies: + # Подготовка аргументов для формулы + args = [x_values] + [param_values[dep] for dep in self.dependencies] + result = self.formula(*args) else: - raise ValueError(f"Не удалось получить значение для {param}") + result = self.formula(x_values) + + # Сохранение в кэш + self.cache[cache_key] = result + return result - def __repr__(self): - return f"Harmonic(A={self.amplitude}, f={self.frequency}, φ={self.phase})" + def clear_cache(self): + """Очистка кэша параметра""" + self.cache.clear() -# ====================== ОСНОВНОЙ КЛАСС МОДЕЛИ ====================== -class MathematicalModel: - """ - Модель с поддержкой зависимых параметров - """ +class FormulaModel: + """Основной класс для управления моделью с параметрами""" def __init__(self): self.parameters: Dict[str, Parameter] = {} - self.harmonics: List[HarmonicOscillation] = [] self.main_formula: Optional[Callable] = None - self.evaluation_context: Dict[str, Any] = {} - - def add_independent_parameter(self, name: str, formula: Callable, - x_range: Tuple[float, float], - num_points: int = 1000, - color: str = 'blue', - line_style: str = '-', - description: str = "") -> None: - """Добавляет независимый параметр""" - self.parameters[name] = IndependentParameter( - name=name, - formula=formula, - x_range=x_range, - num_points=num_points, - color=color, - line_style=line_style, - description=description - ) - - def add_dependent_parameter(self, name: str, expression: Callable, - dependencies: List[str], - color: str = 'green', - line_style: str = '--', - description: str = "") -> None: - """Добавляет зависимый параметр""" - # Проверяем, что все зависимости существуют - for dep in dependencies: - if dep not in self.parameters: - raise ValueError(f"Зависимость '{dep}' не найдена среди параметров") + self.dependency_graph = nx.DiGraph() - self.parameters[name] = DependentParameter( - name=name, - expression=expression, - dependencies=dependencies, - color=color, - line_style=line_style, - description=description - ) + def add_parameter(self, parameter: Parameter): + """Добавление параметра в модель""" + self.parameters[parameter.name] = parameter + + # Обновление графа зависимостей + self.dependency_graph.add_node(parameter.name) + for dep in parameter.dependencies: + self.dependency_graph.add_edge(dep, parameter.name) + + # Проверка на циклические зависимости + if not nx.is_directed_acyclic_graph(self.dependency_graph): + raise ValueError(f"Циклическая зависимость обнаружена при добавлении параметра {parameter.name}") - def add_harmonic(self, amplitude: Union[float, str], - frequency: Union[float, str], - phase: Union[float, str] = 0, - amplitude_depends_on: Optional[List[str]] = None, - frequency_depends_on: Optional[List[str]] = None, - phase_depends_on: Optional[List[str]] = None) -> None: - """Добавляет гармоническое колебание с возможными зависимостями""" - - harmonic = HarmonicOscillation( - amplitude=amplitude, - frequency=frequency, - phase=phase, - amplitude_depends_on=amplitude_depends_on, - frequency_depends_on=frequency_depends_on, - phase_depends_on=phase_depends_on - ) - - # Проверяем, что все зависимости существуют - for dep in harmonic.get_dependencies(): - if dep not in self.parameters: - raise ValueError(f"Зависимость '{dep}' для гармоники не найдена среди параметров") - - self.harmonics.append(harmonic) - - def set_main_formula(self, formula: Callable) -> None: - """Устанавливает основную формулу""" + def set_main_formula(self, formula: Callable, dependencies: List[str]): + """Установка основной формулы модели""" self.main_formula = formula + self.main_dependencies = dependencies - def get_parameter_dependencies(self, param_name: str) -> List[str]: - """Возвращает список зависимостей для параметра""" - param = self.parameters.get(param_name) - if isinstance(param, DependentParameter): - return param.dependencies - return [] + def get_calculation_order(self) -> List[str]: + """Получение порядка вычисления параметров с учетом зависимостей""" + try: + return list(nx.topological_sort(self.dependency_graph)) + except nx.NetworkXError: + raise ValueError("Невозможно определить порядок вычисления из-за циклических зависимостей") - def get_all_dependencies(self) -> Dict[str, List[str]]: - """Возвращает словарь всех зависимостей""" - dependencies = {} - for name, param in self.parameters.items(): - if isinstance(param, DependentParameter): - dependencies[name] = param.dependencies - return dependencies - - def evaluate_parameters(self, x: np.ndarray) -> Dict[str, np.ndarray]: - """ - Вычисляет все параметры с учетом зависимостей. - Использует топологическую сортировку для правильного порядка вычисления. - """ - results = {'x': x} + def calculate_all_parameters(self, x_values: np.ndarray) -> Dict[str, np.ndarray]: + """Вычисление всех параметров в правильном порядке""" + param_values = {} + calculation_order = self.get_calculation_order() - # Функция для топологической сортировки - def topological_sort(): - visited = set() - order = [] - - def dfs(param_name): - if param_name in visited: - return - visited.add(param_name) - + for param_name in calculation_order: + if param_name in self.parameters: param = self.parameters[param_name] - if isinstance(param, DependentParameter): - for dep in param.dependencies: - if dep in self.parameters: - dfs(dep) - - order.append(param_name) - - for name in self.parameters: - if name not in visited: - dfs(name) - - return order + param_values[param_name] = param.calculate(x_values, param_values) - # Вычисляем параметры в правильном порядке - eval_order = topological_sort() - - for param_name in eval_order: - param = self.parameters[param_name] - results[param_name] = param.evaluate(results) - - return results + return param_values - def sum_harmonics(self, t: np.ndarray, context: Dict[str, Any]) -> np.ndarray: - """Вычисляет сумму всех гармонических колебаний с учетом зависимостей""" - if not self.harmonics: - return np.zeros_like(t) - - result = np.zeros_like(t) - for h in self.harmonics: - result += h.evaluate(t, context) - - return result - - def evaluate_main(self, x: np.ndarray, **kwargs) -> np.ndarray: - """ - Вычисляет основную формулу с заданным x и дополнительными параметрами - """ + def calculate_main_formula(self, x_values: np.ndarray) -> np.ndarray: + """Вычисление основной формулы""" if self.main_formula is None: raise ValueError("Основная формула не установлена") - # Вычисляем все параметры - context = self.evaluate_parameters(x) - context.update(kwargs) + param_values = self.calculate_all_parameters(x_values) - # Добавляем сумму гармоник в контекст - context['harmonic_sum'] = lambda t: self.sum_harmonics(t, context) - - # Получаем аргументы функции - sig = inspect.signature(self.main_formula) - - # Подготавливаем аргументы для вызова - call_args = {} - for param_name in sig.parameters: - if param_name in context: - call_args[param_name] = context[param_name] - elif param_name == 'x': - call_args['x'] = x - - return self.main_formula(**call_args) + # Подготовка аргументов для основной формулы + args = [x_values] + [param_values[dep] for dep in self.main_dependencies] + return self.main_formula(*args) + + def clear_all_cache(self): + """Очистка всех кэшей""" + for param in self.parameters.values(): + param.clear_cache() + +class Plotter: + """Класс для построения графиков""" + + def __init__(self, model: FormulaModel): + self.model = model + + def plot_parameter(self, param_name: str, x_range: tuple = (-10, 10), + num_points: int = 1000, title: str = None): + """График отдельного параметра""" + if param_name not in self.model.parameters: + raise ValueError(f"Параметр {param_name} не найден") + + x_values = np.linspace(x_range[0], x_range[1], num_points) + param_values = self.model.calculate_all_parameters(x_values) + y_values = param_values[param_name] + + plt.figure(figsize=(10, 6)) + plt.plot(x_values, y_values, linewidth=2) + plt.grid(True, alpha=0.3) + + param = self.model.parameters[param_name] + plt.title(title or f"Параметр {param_name}: {param.description}") + plt.xlabel("x") + plt.ylabel(f"{param_name} ({param.units})" if param.units else param_name) + + plt.show() + + def plot_all_parameters(self, x_range: tuple = (-10, 10), num_points: int = 1000): + """График всех параметров на одном рисунке""" + x_values = np.linspace(x_range[0], x_range[1], num_points) + param_values = self.model.calculate_all_parameters(x_values) + + plt.figure(figsize=(14, 8)) + + for param_name, y_values in param_values.items(): + plt.plot(x_values, y_values, label=param_name, linewidth=2) + + plt.grid(True, alpha=0.3) + plt.title("Все параметры модели") + plt.xlabel("x") + plt.ylabel("Значения параметров") + plt.legend() + plt.show() + + def plot_main_formula(self, x_range: tuple = (-10, 10), num_points: int = 1000, + title: str = "Основная формула"): + """График основной формулы""" + x_values = np.linspace(x_range[0], x_range[1], num_points) + y_values = self.model.calculate_main_formula(x_values) + + plt.figure(figsize=(10, 6)) + plt.plot(x_values, y_values, 'r-', linewidth=2, label='Основная формула') + plt.grid(True, alpha=0.3) + plt.title(title) + plt.xlabel("x") + plt.ylabel("f(x)") + plt.legend() + plt.show() + + def plot_dependency_graph(self): + """Визуализация графа зависимостей""" + plt.figure(figsize=(12, 8)) + pos = nx.spring_layout(self.model.dependency_graph, k=2, iterations=50) + + nx.draw(self.model.dependency_graph, pos, + with_labels=True, node_color='lightblue', + node_size=2000, font_size=10, font_weight='bold', + arrows=True, arrowsize=20, edge_color='gray') + + plt.title("Граф зависимостей параметров") + plt.show() + + +# Пример использования и заготовки функций + +def create_example_model(): + """Создание примера модели для демонстрации""" + model = FormulaModel() + + # Независимые параметры + param_a = Parameter( + name="A", + formula=lambda x: np.sin(x) + 2, + description="Синусоидальная функция", + units="м/с" + ) + + # Зависимый параметр (зависит от A) + param_b = Parameter( + name="B", + formula=lambda x, a: np.cos(x) * a, + dependencies=["A"], + description="Функция, зависящая от A", + units="м/с²" + ) + + # Параметр, зависящий от B + param_c = Parameter( + name="C", + formula=lambda x, b: np.exp(-x/10) * b, + dependencies=["B"], + description="Экспоненциальная функция от B", + units="Н" + ) + + # Параметр, зависящий от A и C + param_d = Parameter( + name="D", + formula=lambda x, a, c: (a**2 + c) / (1 + x**2), + dependencies=["A", "C"], + description="Комбинированная функция", + units="Дж" + ) + + # Независимый параметр + param_e = Parameter( + name="E", + formula=lambda x: np.log(1 + np.abs(x)), + description="Логарифмическая функция", + units="К" + ) + + # Добавление параметров в модель + for param in [param_a, param_b, param_c, param_d, param_e]: + model.add_parameter(param) + + # Основная формула, использующая несколько параметров + model.set_main_formula( + formula=lambda x, a, c, e: a * c + e**2, + dependencies=["A", "C", "E"] + ) + + return model + + +# Утилитарные функции + +def analyze_model_complexity(model: FormulaModel): + """Анализ сложности модели""" + print("=== Анализ модели ===") + print(f"Количество параметров: {len(model.parameters)}") + + # Анализ зависимостей + max_depth = 0 + for param_name in model.parameters: + try: + depth = nx.shortest_path_length(model.dependency_graph, param_name) + max_depth = max(max_depth, max(depth.values()) if depth else 0) + except: + pass + + print(f"Максимальная глубина зависимостей: {max_depth}") + print(f"Порядок вычисления: {model.get_calculation_order()}") + + return max_depth + + +def benchmark_model(model: FormulaModel, x_range: tuple = (-10, 10), + num_points: int = 10000): + """Бенчмарк производительности модели""" + import time + + x_values = np.linspace(x_range[0], x_range[1], num_points) + + start_time = time.time() + param_values = model.calculate_all_parameters(x_values) + end_time = time.time() + + print(f"Время вычисления всех параметров: {end_time - start_time:.4f} сек") + print(f"Точек данных: {num_points}") + print(f"Скорость: {num_points / (end_time - start_time):.0f} точек/сек") + + +# Основная функция для демонстрации def main(): - """Пример использования с зависимыми параметрами""" + """Основная функция для демонстрации работы скрипта""" + # Создание модели + model = create_example_model() + plotter = Plotter(model) - # Создаем модель - model = MathematicalModel() + # Анализ модели + analyze_model_complexity(model) - # Добавляем независимые параметры (базовые) - model.add_independent_parameter( - name="time", - formula=lambda x: x, # Просто время - x_range=(0, 10), - color='gray', - description="Базовый параметр времени" - ) + # Построение графиков + plotter.plot_dependency_graph() + plotter.plot_all_parameters() + plotter.plot_main_formula() - model.add_independent_parameter( - name="A0", - formula=lambda x: 2.0 * np.ones_like(x), # Константа - x_range=(0, 10), - color='blue', - description="Базовая амплитуда" - ) + # Графики отдельных параметров + for param_name in ["A", "B", "C"]: + plotter.plot_parameter(param_name) - model.add_independent_parameter( - name="f0", - formula=lambda x: 1.0 * np.ones_like(x), # Константа - x_range=(0, 10), - color='red', - description="Базовая частота" - ) - - # Добавляем зависимые параметры (выражаются через другие) - model.add_dependent_parameter( - name="A_effective", - expression=lambda A0, time: A0 * np.exp(-0.1 * time), # Затухающая амплитуда - dependencies=["A0", "time"], - color='green', - description="Эффективная амплитуда (зависит от времени)" - ) - - model.add_dependent_parameter( - name="f_effective", - expression=lambda f0, time: f0 * (1 + 0.05 * np.sin(time)), # Модулированная частота - dependencies=["f0", "time"], - color='orange', - description="Эффективная частота (модулирована)" - ) - - model.add_dependent_parameter( - name="modulation_index", - expression=lambda A_effective, f_effective: A_effective * f_effective / 2, - dependencies=["A_effective", "f_effective"], - color='purple', - description="Индекс модуляции (произведение)" - ) - - # Добавляем гармоники, которые могут зависеть от параметров - model.add_harmonic( - amplitude="A_effective", # Использует параметр A_effective - frequency="f_effective", # Использует параметр f_effective - phase=0, - amplitude_depends_on=["A_effective"], - frequency_depends_on=["f_effective"] - ) - - model.add_harmonic( - amplitude=0.5, - frequency=2.0, - phase=np.pi/4 - ) - - model.add_harmonic( - amplitude="modulation_index", - frequency=3.0, - phase=0, - amplitude_depends_on=["modulation_index"] - ) - - # Устанавливаем основную формулу - def main_formula(x, A_effective, f_effective, modulation_index, harmonic_sum): - """ - Основная формула: комбинация параметров и гармоник - """ - # Параметрическая часть - parametric_part = A_effective * np.sin(2 * np.pi * f_effective * x) - - # Модуляционная часть - modulation_part = modulation_index * np.cos(2 * np.pi * x) - - # Гармоническая часть - harmonic_part = harmonic_sum(x) - - return parametric_part + modulation_part + harmonic_part - - model.set_main_formula(main_formula) - - # Создаем визуализатор - viz = ModelVisualizer(model) - - # Строим граф зависимостей - print("Визуализация графа зависимостей...") - viz.plot_dependency_graph() - - # Строим графики параметров с зависимостями - print("\nГрафики параметров с зависимостями...") - viz.plot_parameter("A_effective", show_dependencies=True) - viz.plot_parameter("f_effective", show_dependencies=True) - viz.plot_parameter_with_dependencies("modulation_index") - - # Строим графики всех параметров - print("\nВсе параметры...") - fig, axes = plt.subplots(2, 3, figsize=(15, 10)) - axes = axes.flatten() - - x_test = np.linspace(0, 10, 1000) - context = model.evaluate_parameters(x_test) - - for i, (name, param) in enumerate(model.parameters.items()): - if i < len(axes): - axes[i].plot(x_test, context[name], color=param.color, - linestyle=param.line_style, linewidth=2) - axes[i].grid(True, alpha=0.3) - axes[i].set_title(f"{name}\n{param.description}") - axes[i].set_xlabel("x") - - # Скрываем лишний подграфик - for j in range(len(model.parameters), len(axes)): - axes[j].set_visible(False) - - plt.tight_layout() - - # Строим графики гармоник с зависимостями - print("\nГармонические колебания...") - viz.plot_harmonics(t_range=(0, 5), x_for_dependencies=x_test) - - # Строим график основной формулы - print("\nОсновная формула...") - fig, ax = plt.subplots(figsize=(12, 6)) - y_main = model.evaluate_main(x_test) - ax.plot(x_test, y_main, 'b-', linewidth=2) - ax.grid(True, alpha=0.3) - ax.set_title("Результат работы основной формулы", fontsize=14) - ax.set_xlabel("x", fontsize=12) - ax.set_ylabel("F(x)", fontsize=12) - - # Показываем все графики - plt.show() - - # Выводим информацию о зависимостях - print("\nСтруктура зависимостей:") - deps = model.get_all_dependencies() - for param, dependencies in deps.items(): - print(f" {param} зависит от: {', '.join(dependencies)}") + # Бенчмарк + benchmark_model(model) + if __name__ == "__main__": main() diff --git a/visualizer.py b/visualizer.py deleted file mode 100644 index f22a3df..0000000 --- a/visualizer.py +++ /dev/null @@ -1,228 +0,0 @@ -class ModelVisualizer: - """Класс для визуализации модели с зависимостями""" - - def __init__(self, model: MathematicalModel): - self.model = model - self.figures = {} - - def plot_parameter(self, param_name: str, - x_range: Optional[Tuple[float, float]] = None, - figsize: Tuple[int, int] = (10, 6), - title: Optional[str] = None, - show_dependencies: bool = True) -> plt.Figure: - """ - Строит график для параметра с учетом его зависимостей - """ - if param_name not in self.model.parameters: - raise ValueError(f"Параметр '{param_name}' не найден") - - param = self.model.parameters[param_name] - - # Определяем диапазон x - if x_range is None: - if isinstance(param, IndependentParameter): - x_range = param.x_range - else: - # Для зависимых параметров нужно определить разумный диапазон - x_range = (0, 10) # По умолчанию - - x = np.linspace(x_range[0], x_range[1], 1000) - - # Вычисляем значение параметра - context = self.model.evaluate_parameters(x) - y = context[param_name] - - # Создаем фигуру - fig, ax = plt.subplots(figsize=figsize) - - # Строим основной график - ax.plot(x, y, color=param.color, linestyle=param.line_style, - linewidth=2, label=param_name) - - # Если нужно показать зависимости - if show_dependencies and isinstance(param, DependentParameter): - for dep_name in param.dependencies: - if dep_name in context: - dep_y = context[dep_name] - # Нормализуем для отображения на том же графике - dep_y_normalized = (dep_y - np.min(dep_y)) / (np.max(dep_y) - np.min(dep_y) + 1e-10) - ax.plot(x, dep_y_normalized, '--', alpha=0.5, - label=f"{dep_name} (норм.)") - - ax.grid(True, alpha=0.3) - ax.set_title(title or f"График параметра: {param_name}\n{param.description}", fontsize=14) - ax.set_xlabel("x", fontsize=12) - ax.set_ylabel("Значение", fontsize=12) - ax.legend(loc='best') - - self.figures[f"param_{param_name}"] = fig - return fig - - def plot_parameter_with_dependencies(self, param_name: str, - figsize: Tuple[int, int] = (14, 10)) -> plt.Figure: - """ - Строит подробный график параметра и всех его зависимостей - """ - if param_name not in self.model.parameters: - raise ValueError(f"Параметр '{param_name}' не найден") - - param = self.model.parameters[param_name] - - # Собираем все зависимости (рекурсивно) - def get_all_deps(name, deps_set): - deps = self.model.get_parameter_dependencies(name) - for dep in deps: - if dep not in deps_set: - deps_set.add(dep) - get_all_deps(dep, deps_set) - return deps_set - - all_deps = list(get_all_deps(param_name, set())) - - # Определяем диапазон x - x_range = (0, 10) - if isinstance(param, IndependentParameter): - x_range = param.x_range - elif all_deps: - # Пытаемся найти независимый параметр среди зависимостей - for dep in all_deps: - p = self.model.parameters[dep] - if isinstance(p, IndependentParameter): - x_range = p.x_range - break - - x = np.linspace(x_range[0], x_range[1], 1000) - context = self.model.evaluate_parameters(x) - - # Создаем подграфики - n_plots = len(all_deps) + 1 - cols = min(3, n_plots) - rows = (n_plots + cols - 1) // cols - - fig, axes = plt.subplots(rows, cols, figsize=figsize) - axes = axes.flatten() if n_plots > 1 else [axes] - - # График целевого параметра - axes[0].plot(x, context[param_name], color=param.color, - linestyle=param.line_style, linewidth=2.5) - axes[0].grid(True, alpha=0.3) - axes[0].set_title(f"{param_name} (целевой)", fontsize=12) - axes[0].set_xlabel("x") - - # Графики зависимостей - for i, dep_name in enumerate(all_deps, 1): - if i < len(axes): - dep_param = self.model.parameters[dep_name] - axes[i].plot(x, context[dep_name], color=dep_param.color, - linestyle=dep_param.line_style, linewidth=2) - axes[i].grid(True, alpha=0.3) - axes[i].set_title(f"{dep_name}\n{getattr(dep_param, 'description', '')}", fontsize=10) - axes[i].set_xlabel("x") - - # Скрываем лишние подграфики - for j in range(len(all_deps) + 1, len(axes)): - axes[j].set_visible(False) - - plt.tight_layout() - self.figures[f"param_{param_name}_with_deps"] = fig - return fig - - def plot_harmonics(self, t_range: Tuple[float, float] = (0, 10), - figsize: Tuple[int, int] = (12, 8), - x_for_dependencies: Optional[np.ndarray] = None) -> plt.Figure: - """ - Строит графики гармоник с учетом возможных зависимостей от параметров - """ - if not self.model.harmonics: - raise ValueError("Нет гармонических колебаний") - - t = np.linspace(t_range[0], t_range[1], 1000) - - # Подготавливаем контекст для зависимых гармоник - if x_for_dependencies is not None: - context = self.model.evaluate_parameters(x_for_dependencies) - else: - # Если не задан x, используем значения по умолчанию - context = {} - for name, param in self.model.parameters.items(): - if isinstance(param, IndependentParameter): - x_default = np.linspace(param.x_range[0], param.x_range[1], 1) - context[name] = param.evaluate({'x': x_default})[0] - - n_plots = len(self.model.harmonics) + 1 - fig, axes = plt.subplots(n_plots, 1, figsize=(figsize[0], figsize[1] * n_plots/3)) - - # Индивидуальные гармоники - for i, h in enumerate(self.model.harmonics): - harmonic_values = h.evaluate(t, context) - axes[i].plot(t, harmonic_values, linewidth=1.5) - axes[i].grid(True, alpha=0.3) - - # Показываем зависимости в заголовке - deps = h.get_dependencies() - deps_str = f" (зависит от: {', '.join(deps)})" if deps else "" - axes[i].set_title(f"Гармоника {i+1}: {h}{deps_str}") - axes[i].set_ylabel("Амплитуда") - - # Сумма гармоник - total = np.zeros_like(t) - for h in self.model.harmonics: - total += h.evaluate(t, context) - - axes[-1].plot(t, total, 'r-', linewidth=2) - axes[-1].grid(True, alpha=0.3) - axes[-1].set_title("Сумма всех гармоник") - axes[-1].set_xlabel("Время t") - axes[-1].set_ylabel("Амплитуда") - - plt.tight_layout() - self.figures["harmonics"] = fig - return fig - - def plot_dependency_graph(self, figsize: Tuple[int, int] = (12, 8)) -> plt.Figure: - """ - Визуализирует граф зависимостей между параметрами - """ - try: - import networkx as nx - except ImportError: - print("Для визуализации графа зависимостей установите networkx: pip install networkx") - return None - - G = nx.DiGraph() - - # Добавляем узлы и ребра - for name, param in self.model.parameters.items(): - node_attrs = { - 'color': 'lightblue' if isinstance(param, IndependentParameter) else 'lightgreen' - } - G.add_node(name, **node_attrs) - - if isinstance(param, DependentParameter): - for dep in param.dependencies: - G.add_edge(dep, name) - - # Добавляем гармоники как узлы, если у них есть зависимости - for i, h in enumerate(self.model.harmonics): - deps = h.get_dependencies() - if deps: - harmonic_name = f"Harmonic_{i+1}" - G.add_node(harmonic_name, color='lightcoral') - for dep in deps: - G.add_edge(dep, harmonic_name) - - fig, ax = plt.subplots(figsize=figsize) - - # Раскладка графа - pos = nx.spring_layout(G, k=2, iterations=50) - - # Рисуем граф - node_colors = [G.nodes[node].get('color', 'lightgray') for node in G.nodes] - nx.draw(G, pos, with_labels=True, node_color=node_colors, - node_size=2000, font_size=10, font_weight='bold', - arrows=True, arrowsize=20, ax=ax) - - ax.set_title("Граф зависимостей параметров", fontsize=16) - - self.figures["dependency_graph"] = fig - return fig