This commit is contained in:
kit8nino 2026-02-18 11:50:20 +03:00
parent a1a898521a
commit ea55212306
2 changed files with 267 additions and 642 deletions

681
main.py
View File

@ -1,451 +1,304 @@
import numpy as np import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from typing import Callable, List, Tuple, Optional, Dict, Any, Union from typing import Dict, List, Callable, Any, Optional
import dataclasses import networkx as nx
from enum import Enum from functools import lru_cache
import inspect import sympy as sp
from visualizer import * from collections import defaultdict
# ====================== ТИПЫ ЗАВИСИМОСТЕЙ ======================
class DependencyType(Enum):
"""Типы зависимостей между параметрами"""
INDEPENDENT = "independent" # Независимый параметр
DEPENDENT = "dependent" # Зависимый параметр
EXPRESSION = "expression" # Выражение, использующее другие параметры
# ====================== КЛАССЫ ДЛЯ ПАРАМЕТРОВ ======================
@dataclasses.dataclass
class Parameter: class Parameter:
"""Базовый класс для параметра""" """Класс для представления параметра с его формулой и зависимостями"""
name: str
param_type: DependencyType
description: str = ""
def evaluate(self, context: Dict[str, Any]) -> np.ndarray: def __init__(self, name: str, formula: Callable, dependencies: List[str] = None,
"""Вычисляет значение параметра в заданном контексте""" description: str = "", units: str = ""):
raise NotImplementedError self.name = name
self.formula = formula # Функция для вычисления параметра
@dataclasses.dataclass self.dependencies = dependencies or [] # Список зависимых параметров
class IndependentParameter(Parameter): self.description = description
"""Независимый параметр (базовый)""" self.units = units
formula: Callable self.cache = {} # Кэш для мемоизации
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 calculate(self, x_values: np.ndarray, param_values: Dict[str, np.ndarray]) -> np.ndarray:
return self.expression(**dep_values) """Вычисление значений параметра для заданных x"""
# Проверка кэша
# ====================== КЛАСС ДЛЯ ГАРМОНИК ====================== cache_key = (tuple(x_values), tuple(sorted(param_values.items())))
if cache_key in self.cache:
class HarmonicOscillation: return self.cache[cache_key]
"""Класс для описания гармонического колебания"""
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
# Отслеживаем зависимости # Вычисление
self.amplitude_depends_on = amplitude_depends_on or [] if self.dependencies:
self.frequency_depends_on = frequency_depends_on or [] # Подготовка аргументов для формулы
self.phase_depends_on = phase_depends_on or [] args = [x_values] + [param_values[dep] for dep in self.dependencies]
result = self.formula(*args)
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)
else: else:
raise ValueError(f"Не удалось получить значение для {param}") result = self.formula(x_values)
# Сохранение в кэш
self.cache[cache_key] = result
return result
def __repr__(self): def clear_cache(self):
return f"Harmonic(A={self.amplitude}, f={self.frequency}, φ={self.phase})" """Очистка кэша параметра"""
self.cache.clear()
# ====================== ОСНОВНОЙ КЛАСС МОДЕЛИ ======================
class MathematicalModel: class FormulaModel:
""" """Основной класс для управления моделью с параметрами"""
Модель с поддержкой зависимых параметров
"""
def __init__(self): def __init__(self):
self.parameters: Dict[str, Parameter] = {} self.parameters: Dict[str, Parameter] = {}
self.harmonics: List[HarmonicOscillation] = []
self.main_formula: Optional[Callable] = None self.main_formula: Optional[Callable] = None
self.evaluation_context: Dict[str, Any] = {} self.dependency_graph = nx.DiGraph()
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.parameters[name] = DependentParameter( def add_parameter(self, parameter: Parameter):
name=name, """Добавление параметра в модель"""
expression=expression, self.parameters[parameter.name] = parameter
dependencies=dependencies,
color=color, # Обновление графа зависимостей
line_style=line_style, self.dependency_graph.add_node(parameter.name)
description=description 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], def set_main_formula(self, formula: Callable, dependencies: List[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:
"""Устанавливает основную формулу"""
self.main_formula = formula self.main_formula = formula
self.main_dependencies = dependencies
def get_parameter_dependencies(self, param_name: str) -> List[str]: def get_calculation_order(self) -> List[str]:
"""Возвращает список зависимостей для параметра""" """Получение порядка вычисления параметров с учетом зависимостей"""
param = self.parameters.get(param_name) try:
if isinstance(param, DependentParameter): return list(nx.topological_sort(self.dependency_graph))
return param.dependencies except nx.NetworkXError:
return [] raise ValueError("Невозможно определить порядок вычисления из-за циклических зависимостей")
def get_all_dependencies(self) -> Dict[str, List[str]]: def calculate_all_parameters(self, x_values: np.ndarray) -> Dict[str, np.ndarray]:
"""Возвращает словарь всех зависимостей""" """Вычисление всех параметров в правильном порядке"""
dependencies = {} param_values = {}
for name, param in self.parameters.items(): calculation_order = self.get_calculation_order()
if isinstance(param, DependentParameter):
dependencies[name] = param.dependencies
return dependencies
def evaluate_parameters(self, x: np.ndarray) -> Dict[str, np.ndarray]:
"""
Вычисляет все параметры с учетом зависимостей.
Использует топологическую сортировку для правильного порядка вычисления.
"""
results = {'x': x}
# Функция для топологической сортировки for param_name in calculation_order:
def topological_sort(): if param_name in self.parameters:
visited = set()
order = []
def dfs(param_name):
if param_name in visited:
return
visited.add(param_name)
param = self.parameters[param_name] param = self.parameters[param_name]
if isinstance(param, DependentParameter): param_values[param_name] = param.calculate(x_values, param_values)
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
# Вычисляем параметры в правильном порядке return 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
def sum_harmonics(self, t: np.ndarray, context: Dict[str, Any]) -> np.ndarray: def calculate_main_formula(self, x_values: np.ndarray) -> 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 и дополнительными параметрами
"""
if self.main_formula is None: if self.main_formula is None:
raise ValueError("Основная формула не установлена") raise ValueError("Основная формула не установлена")
# Вычисляем все параметры param_values = self.calculate_all_parameters(x_values)
context = self.evaluate_parameters(x)
context.update(kwargs)
# Добавляем сумму гармоник в контекст # Подготовка аргументов для основной формулы
context['harmonic_sum'] = lambda t: self.sum_harmonics(t, context) args = [x_values] + [param_values[dep] for dep in self.main_dependencies]
return self.main_formula(*args)
# Получаем аргументы функции
sig = inspect.signature(self.main_formula) def clear_all_cache(self):
"""Очистка всех кэшей"""
# Подготавливаем аргументы для вызова for param in self.parameters.values():
call_args = {} param.clear_cache()
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)
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(): def main():
"""Пример использования с зависимыми параметрами""" """Основная функция для демонстрации работы скрипта"""
# Создание модели
model = create_example_model()
plotter = Plotter(model)
# Создаем модель # Анализ модели
model = MathematicalModel() analyze_model_complexity(model)
# Добавляем независимые параметры (базовые) # Построение графиков
model.add_independent_parameter( plotter.plot_dependency_graph()
name="time", plotter.plot_all_parameters()
formula=lambda x: x, # Просто время plotter.plot_main_formula()
x_range=(0, 10),
color='gray',
description="Базовый параметр времени"
)
model.add_independent_parameter( # Графики отдельных параметров
name="A0", for param_name in ["A", "B", "C"]:
formula=lambda x: 2.0 * np.ones_like(x), # Константа plotter.plot_parameter(param_name)
x_range=(0, 10),
color='blue',
description="Базовая амплитуда"
)
model.add_independent_parameter( # Бенчмарк
name="f0", benchmark_model(model)
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)}")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -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