452 lines
19 KiB
Python
452 lines
19 KiB
Python
|
|
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 *
|
|||
|
|
|
|||
|
|
# ====================== ТИПЫ ЗАВИСИМОСТЕЙ ======================
|
|||
|
|
|
|||
|
|
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]
|
|||
|
|
|
|||
|
|
# Вычисляем выражение
|
|||
|
|
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
|
|||
|
|
|
|||
|
|
# Отслеживаем зависимости
|
|||
|
|
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)
|
|||
|
|
else:
|
|||
|
|
raise ValueError(f"Не удалось получить значение для {param}")
|
|||
|
|
|
|||
|
|
def __repr__(self):
|
|||
|
|
return f"Harmonic(A={self.amplitude}, f={self.frequency}, φ={self.phase})"
|
|||
|
|
|
|||
|
|
# ====================== ОСНОВНОЙ КЛАСС МОДЕЛИ ======================
|
|||
|
|
|
|||
|
|
class MathematicalModel:
|
|||
|
|
"""
|
|||
|
|
Модель с поддержкой зависимых параметров
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
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.parameters[name] = DependentParameter(
|
|||
|
|
name=name,
|
|||
|
|
expression=expression,
|
|||
|
|
dependencies=dependencies,
|
|||
|
|
color=color,
|
|||
|
|
line_style=line_style,
|
|||
|
|
description=description
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
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:
|
|||
|
|
"""Устанавливает основную формулу"""
|
|||
|
|
self.main_formula = formula
|
|||
|
|
|
|||
|
|
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_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 topological_sort():
|
|||
|
|
visited = set()
|
|||
|
|
order = []
|
|||
|
|
|
|||
|
|
def dfs(param_name):
|
|||
|
|
if param_name in visited:
|
|||
|
|
return
|
|||
|
|
visited.add(param_name)
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
|
|||
|
|
# Вычисляем параметры в правильном порядке
|
|||
|
|
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:
|
|||
|
|
"""Вычисляет сумму всех гармонических колебаний с учетом зависимостей"""
|
|||
|
|
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:
|
|||
|
|
raise ValueError("Основная формула не установлена")
|
|||
|
|
|
|||
|
|
# Вычисляем все параметры
|
|||
|
|
context = self.evaluate_parameters(x)
|
|||
|
|
context.update(kwargs)
|
|||
|
|
|
|||
|
|
# Добавляем сумму гармоник в контекст
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
"""Пример использования с зависимыми параметрами"""
|
|||
|
|
|
|||
|
|
# Создаем модель
|
|||
|
|
model = MathematicalModel()
|
|||
|
|
|
|||
|
|
# Добавляем независимые параметры (базовые)
|
|||
|
|
model.add_independent_parameter(
|
|||
|
|
name="time",
|
|||
|
|
formula=lambda x: x, # Просто время
|
|||
|
|
x_range=(0, 10),
|
|||
|
|
color='gray',
|
|||
|
|
description="Базовый параметр времени"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
model.add_independent_parameter(
|
|||
|
|
name="A0",
|
|||
|
|
formula=lambda x: 2.0 * np.ones_like(x), # Константа
|
|||
|
|
x_range=(0, 10),
|
|||
|
|
color='blue',
|
|||
|
|
description="Базовая амплитуда"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
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)}")
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|