diff --git a/Л4-В6/Lab_4.pdf b/Л4-В6/Lab_4.pdf new file mode 100644 index 0000000..b2125e5 Binary files /dev/null and b/Л4-В6/Lab_4.pdf differ diff --git a/Л4-В6/Блоксхема.drawio b/Л4-В6/Блоксхема.drawio new file mode 100644 index 0000000..c698806 --- /dev/null +++ b/Л4-В6/Блоксхема.drawio @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Л4-В6/Отчет.docx b/Л4-В6/Отчет.docx new file mode 100644 index 0000000..350abb6 Binary files /dev/null and b/Л4-В6/Отчет.docx differ diff --git a/Л4-В6/Программа/.gitignore b/Л4-В6/Программа/.gitignore new file mode 100644 index 0000000..df989ca --- /dev/null +++ b/Л4-В6/Программа/.gitignore @@ -0,0 +1,2 @@ +.venv +*/__pycache__ \ No newline at end of file diff --git a/Л4-В6/Программа/requirements.txt b/Л4-В6/Программа/requirements.txt new file mode 100644 index 0000000..1d0cbea --- /dev/null +++ b/Л4-В6/Программа/requirements.txt @@ -0,0 +1,5 @@ +sympy +numpy +scipy +pyqt6 +matplotlib diff --git a/Л4-В6/Программа/src/algorithm.py b/Л4-В6/Программа/src/algorithm.py new file mode 100644 index 0000000..fe08c78 --- /dev/null +++ b/Л4-В6/Программа/src/algorithm.py @@ -0,0 +1,60 @@ +import numpy as np +import typing + + +def difference_n( + func: typing.Callable[[float], float], x_array: np.ndarray, start: int, end: int +) -> float: + if len(x_array) < end + 1: + return None + + result = 0 + + for k in range(start, end + 1): + prod = 1 + for j in range(start, end + 1): + if j != k: + prod *= x_array[k] - x_array[j] + result += func(x_array[k]) / prod + + return result + + +def make_forward_interpolate_function( + func: typing.Callable[[float], float], x_array: np.ndarray +) -> typing.Callable[[float], float]: + n = len(x_array) + diffs = [difference_n(func, x_array, 0, k) for k in range(1, n)] + + def forward_interpolate(x: float) -> float: + L = func(x_array[0]) + + for k in range(1, n): + prod = diffs[k - 1] + for i in range(k): + prod *= x - x_array[i] + L += prod + + return L + + return forward_interpolate + + +def make_backward_interpolate_function( + func: typing.Callable[[float], float], x_array: np.ndarray +) -> typing.Callable[[float], float]: + n = len(x_array) + diffs = [difference_n(func, x_array, n - k - 1, n - 1) for k in range(1, n)] + + def backward_interpolate(x: float) -> float: + L = func(x_array[-1]) + + for k in range(n - 1): + prod = diffs[k] + for i in range(n - 1, n - k - 2, -1): + prod *= x - x_array[i] + L += prod + + return L + + return backward_interpolate diff --git a/Л4-В6/Программа/src/main.py b/Л4-В6/Программа/src/main.py new file mode 100644 index 0000000..f80d85b --- /dev/null +++ b/Л4-В6/Программа/src/main.py @@ -0,0 +1,14 @@ +import sys +from PyQt6.QtWidgets import QApplication +from PyQt6.QtGui import QFont + +import window + +if __name__ == "__main__": + app = QApplication(sys.argv) + font = QFont("Cascadia Code", 14) + app.setFont(font) + window = window.MainWindow() + window.show() + + sys.exit(app.exec()) diff --git a/Л4-В6/Программа/src/task.py b/Л4-В6/Программа/src/task.py new file mode 100644 index 0000000..09636c6 --- /dev/null +++ b/Л4-В6/Программа/src/task.py @@ -0,0 +1,10 @@ +func_str = "exp(-b * x) * cos(pi * (x + x ** 2))" +func_args = {"b": 0.15} + + +def f_str() -> str: + return func_str + + +def f_args() -> dict: + return func_args.copy() diff --git a/Л4-В6/Программа/src/widgets.py b/Л4-В6/Программа/src/widgets.py new file mode 100644 index 0000000..010cca4 --- /dev/null +++ b/Л4-В6/Программа/src/widgets.py @@ -0,0 +1,219 @@ +from PyQt6.QtWidgets import ( + QLabel, + QWidget, + QLineEdit, + QHBoxLayout, + QSlider, +) +from PyQt6.QtCore import Qt + +import abc +import typing + + +class DisplayField(QWidget): + label_text: str + + def __init__(self, parent, label_text="", text=""): + super().__init__(parent) + + self._layout = QHBoxLayout(self) + self._layout.setContentsMargins(0, 0, 0, 0) + + self.label_text = label_text + self.text = QLabel(f"{self.label_text}{text}") + self.text.setAlignment(Qt.AlignmentFlag.AlignLeft) + + self._layout.addWidget(self.text) + + def set_visible(self, visible): + self.text.setVisible(visible) + + def set_text(self, text): + self.text.setText(f"{self.label_text}{text}") + + +class InputWidget(QWidget): + value: typing.Any + previous_value: typing.Any + callback: typing.Callable[[typing.Self], bool] | None = None + + def init(self): + self.value = self._get_value_internal() + self.previous_value = self.value + self._set_callback_internal(self._on_change_internal) + + def get_value(self) -> typing.Any: + return self.value + + def get_previous_value(self) -> typing.Any: + return self.previous_value + + def set_value(self, value: typing.Any): + self._set_value_internal(value) + self._set_value_no_sync(value) + + def set_callback(self, callback: typing.Callable[[typing.Self], bool]): + self.callback = callback + + @abc.abstractmethod + def _get_value_internal(self) -> typing.Any: + pass + + @abc.abstractmethod + def _set_value_internal(self, value: typing.Any): + pass + + @abc.abstractmethod + def _set_callback_internal(self, callback: typing.Callable[[], None]): + pass + + def _set_value_no_sync(self, value: typing.Any): + self.previous_value = self.value + self.value = value + + def _on_change_internal(self): + self._set_value_no_sync(self._get_value_internal()) + + if self.callback: + self.callback(self) + + +def make_callback_chain( + callbacks: typing.List[typing.Callable[[InputWidget], bool]] +) -> typing.Callable[[InputWidget], bool]: + def callback(widget: InputWidget): + for callback in callbacks: + if not callback(widget): + break + return True + + return callback + + +def make_min_max_callback( + min: typing.Any, max: typing.Any +) -> typing.Callable[[InputWidget], bool]: + def callback(widget: InputWidget): + value = widget.get_value() + + if type(min)(value) < min: + widget.set_value(min) + if type(max)(value) > max: + widget.set_value(max) + + return True + + return callback + + +def float_check_callback(widget: InputWidget): + try: + internal_value = widget._get_value_internal() + if not (internal_value == "" or internal_value == "-" or internal_value == "."): + _ = float(widget.get_value()) + return True + else: + widget.value = widget.previous_value + return False + except: + widget.set_value(widget.previous_value) + return False + + +def int_check_callback(widget: InputWidget): + try: + internal_value = widget._get_value_internal() + if not (internal_value == "" or internal_value == "-"): + _ = int(widget.get_value()) + return True + else: + widget.value = widget.previous_value + return False + except: + widget.set_value(widget.previous_value) + return False + + +class InputField(InputWidget): + def __init__( + self, + parent, + label_text="", + placeholder_text="", + lineedit_text="", + label_position="left", + ): + super().__init__(parent) + + self._layout = QHBoxLayout(self) + self._layout.setContentsMargins(0, 0, 0, 0) + self.label = QLabel(label_text) + self.lineedit = QLineEdit(lineedit_text) + self.lineedit.setPlaceholderText(placeholder_text) + self.lineedit.setMinimumWidth(50) + + if label_position == "left": + self._layout.addWidget(self.label) + self._layout.addWidget(self.lineedit) + elif label_position == "right": + self._layout.addWidget(self.lineedit) + self._layout.addWidget(self.label) + elif label_position == "none": + self._layout.addWidget(self.lineedit) + + super().init() + super().set_value(lineedit_text) + + def _get_value_internal(self): + return self.lineedit.text() + + def _set_value_internal(self, value): + self.lineedit.setText(str(value)) + + def _set_callback_internal(self, callback): + self.lineedit.textChanged.connect(callback) + + +class InputSlider(InputWidget): + slider: QSlider + + def __init__( + self, + parent, + label_text="", + value=0, + min_value=0, + max_value=100, + step=1, + ticks=False, + ): + super().__init__(parent) + + self._layout = QHBoxLayout(self) + self._layout.setContentsMargins(0, 0, 0, 0) + self.label = QLabel(label_text) + self.slider = QSlider(Qt.Orientation.Horizontal) + if ticks: + self.slider.setTickPosition(QSlider.TickPosition.TicksBelow) + self.slider.setTickInterval(step) + + self.slider.setMinimum(min_value) + self.slider.setMaximum(max_value) + self.slider.setValue(value) + self.slider.setSingleStep(step) + + self._layout.addWidget(self.label) + self._layout.addWidget(self.slider) + + super().init() + super().set_value(value) + + def _get_value_internal(self): + return self.slider.value() + + def _set_value_internal(self, value): + self.slider.setValue(value) + + def _set_callback_internal(self, callback): + self.slider.valueChanged.connect(callback) diff --git a/Л4-В6/Программа/src/window.py b/Л4-В6/Программа/src/window.py new file mode 100644 index 0000000..7d4da79 --- /dev/null +++ b/Л4-В6/Программа/src/window.py @@ -0,0 +1,567 @@ +from PyQt6.QtWidgets import ( + QMainWindow, + QLabel, + QPushButton, + QFormLayout, + QWidget, + QHBoxLayout, +) +from PyQt6 import QtWidgets +import numpy as np +import sympy +from typing import * + +import matplotlib +import matplotlib.pyplot as plt +from matplotlib.figure import Figure +from matplotlib.axes import Axes +from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg + +matplotlib.use("QtAgg") + +import widgets +import task +import algorithm + + +class ParamsField(widgets.InputWidget): + _layout: QFormLayout + fields: Dict[str, widgets.InputWidget] + + callback_internal: Callable + + def __init__(self, parent, fields: List[str]): + super().__init__(parent) + + self._initialize() + self._create(fields) + + super().init() + + def _initialize(self): + self.fields = {} + self._layout = QFormLayout(self) + self._layout.setContentsMargins(0, 0, 0, 0) + + def _create(self, fields: List[str]): + fields = sorted(fields) + + for field in fields: + val = "0.0" + if not self.value.get(field) is None: + val = str(self.value.get(field)) + + self.fields[field] = widgets.InputField( + self, label_text=f" {field}: ", lineedit_text=val + ) + self.fields[field].set_callback( + widgets.make_callback_chain( + [widgets.float_check_callback, self._callback] + ) + ) + self._layout.addRow(self.fields[field]) + + def recreate(self, fields: List[str]): + for field in self.fields: + self.fields[field].deleteLater() + + self.fields = {} + self._create(fields) + self.callback_internal() + + def _get_value_internal(self) -> Dict[str, float]: + value = {} + for field in self.fields: + value[field] = float(self.fields[field].get_value()) + + return value + + def _set_value_internal(self, value: Dict[str, float]): + for field in self.fields: + self.fields[field].set_value(value[field]) + + def _set_callback_internal(self, callback): + self.callback_internal = callback + + def _callback(self, widget: widgets.InputWidget): + self.callback_internal() + return True + + +class PointsField(widgets.InputWidget): + _layout: QFormLayout + points: List[widgets.InputField] + + min: float + max: float + + DIVISIONS = 1_000_000 + + callback_internal: Callable[[None], None] + + def __init__(self, parent, start: int, points: List[float], min: float, max: float): + super().__init__(parent) + + self._initialize() + self._create(start, points, min, max) + + super().init() + + def _initialize(self): + self.points = [] + self.min = 0.0 + self.max = 1.0 + self._layout = QFormLayout(self) + self._layout.setContentsMargins(0, 0, 0, 0) + + def _create(self, start: int, points: List[float], min: float, max: float): + self.min = min + self.max = max + + for i in range(0, len(points)): + value = int((points[i] - min) / (max - min) * self.DIVISIONS) + self.points.append( + widgets.InputSlider( + self, + label_text=f" {i + start}: ", + value=value, + min_value=0, + max_value=self.DIVISIONS, + ) + ) + self.points[i].set_callback( + widgets.make_callback_chain( + [widgets.float_check_callback, self._callback] + ) + ) + self._layout.addRow(self.points[i]) + + def recreate(self, start: int, points: List[float], min: float, max: float): + for point in self.points: + point.deleteLater() + + self.points = [] + self._create(start, points, min, max) + self.callback_internal() + + def _get_value_internal(self) -> List[float]: + value = [] + for point in self.points: + value.append( + float(point.get_value()) / self.DIVISIONS * (self.max - self.min) + + self.min + ) + + return value + + def _set_value_internal(self, value: List[float]): + for i in range(0, len(value)): + self.points[i].set_value( + (value[i] - self.min) / (self.max - self.min) * self.DIVISIONS + ) + + def _set_callback_internal(self, callback): + self.callback_internal = callback + + def _callback(self, widget: widgets.InputWidget): + self.callback_internal() + return True + + +class PlotWidget(QWidget): + figure: Figure + axes: Axes + canvas: FigureCanvasQTAgg + + pre_style: Callable[[Axes], None] + post_style: Callable[[Axes], None] + plots: List[Callable[[Axes], None]] + + def __init__(self, parent): + super().__init__(parent) + + self._layout = QHBoxLayout(self) + + self.figure = plt.figure() + self.axes = self.figure.add_subplot(111) + self.canvas = FigureCanvasQTAgg(self.figure) + self._layout.addWidget(self.canvas) + + self.pre_style = lambda _: None + self.plots = [] + + def set_pre_style(self, style: Callable[[Axes], None]): + self.pre_style = style + + def set_post_style(self, style: Callable[[Axes], None]): + self.post_style = style + + def clear_plots(self): + self.plots = [] + + def add_plot(self, plot: Callable[[Axes], None]): + self.plots.append(plot) + + def redraw(self): + self.axes.clear() + self.pre_style(self.axes) + for plot in self.plots: + plot(self.axes) + self.post_style(self.axes) + + self.canvas.draw() + + +class MainWindow(QMainWindow): + central_widget: QWidget + + function_field: widgets.InputField + function_param_fields: ParamsField + + x_0_field: widgets.InputField + x_n_field: widgets.InputField + n_slider: widgets.InputSlider + + points_field: PointsField + + margin_slider: widgets.InputSlider + + function_plot: PlotWidget + error_plot: PlotWidget + + function: Callable[[float], float] + interpolated_function: Callable[[float], float] + function_params: Dict[str, float] + + x_0: float + x_n: float + n: int + points: List[float] + + margin: float + + parsing_function: bool + updating: bool + + def __init__(self): + super().__init__() + + self.n = 2 + self.margin = 5 + self.x_0 = 0.0 + self.x_n = 1.0 + self.points = [0.5] + self.function = None + self.function_params = {} + self.parsing_function = False + + self.setup_ui() + self.reset() + + def setup_ui(self): + self.setWindowTitle("Лабораторная работа №4") + self.setFixedSize(1400, 800) + + self.central_widget = QWidget(self) + self.setCentralWidget(self.central_widget) + + layout = QHBoxLayout(self) + + left_row = QWidget(self.central_widget) + left_row_layout = QFormLayout(left_row) + right_row = QWidget(self.central_widget) + right_row_layout = QFormLayout(right_row) + + self.function_field = widgets.InputField(self, label_text="Функция: ") + self.function_field.set_callback(self._function_field_changed) + left_row_layout.addWidget(self.function_field) + + self.function_param_fields = ParamsField(self, []) + self.function_param_fields.set_callback(self._function_param_field_changed) + left_row_layout.addWidget(self.function_param_fields) + + self.x_0_field = widgets.InputField( + self, label_text="x_0: ", lineedit_text="0.0" + ) + self.x_0_field.set_callback( + widgets.make_callback_chain( + [widgets.float_check_callback, self._x_0_field_changed] + ) + ) + left_row_layout.addWidget(self.x_0_field) + + self.x_n_field = widgets.InputField( + self, label_text="x_n: ", lineedit_text="1.0" + ) + self.x_n_field.set_callback( + widgets.make_callback_chain( + [widgets.float_check_callback, self._x_n_field_changed] + ) + ) + left_row_layout.addWidget(self.x_n_field) + + self.n_slider = widgets.InputSlider(self, "n: ", 1, 1, 20, 1, True) + self.n_slider.set_callback(self._n_slider_changed) + left_row_layout.addWidget(self.n_slider) + + self.points_field = PointsField(self, 1, [0.5], 0.0, 1.0 - 0.001) + self.points_field.set_callback(self._points_field_changed) + left_row_layout.addWidget(self.points_field) + + self.margin_slider = widgets.InputSlider(self, "отсутп (%): ", 5, 0, 100, 1) + self.margin_slider.set_callback(self._margin_slider_changed) + left_row_layout.addWidget(self.margin_slider) + + reset_button = QPushButton("Сброс", self) + reset_button.clicked.connect(self.reset) + left_row_layout.addWidget(reset_button) + + label1 = QLabel("График функции", self) + right_row_layout.addWidget(label1) + self.function_plot = PlotWidget(self) + self.function_plot.set_pre_style(self.function_plot_pre_style) + self.function_plot.set_post_style(self.function_plot_post_style) + right_row_layout.addWidget(self.function_plot) + + label2 = QLabel("График ошибки", self) + right_row_layout.addWidget(label2) + self.error_plot = PlotWidget(self) + self.error_plot.set_pre_style(self.function_plot_pre_style) + self.error_plot.set_post_style(self.function_plot_post_style) + right_row_layout.addWidget(self.error_plot) + + left_row.setLayout(left_row_layout) + right_row.setLayout(right_row_layout) + + layout.addWidget(left_row) + layout.addWidget(right_row) + + self.central_widget.setLayout(layout) + + def function_plot_pre_style(self, axes: Axes): + axes.set_xlabel("x") + axes.set_ylabel("y") + axes.axhline(0, color="black", linewidth=1) + axes.axvline(0, color="black", linewidth=1) + axes.grid(True) + + def function_plot_post_style(self, axes: Axes): + axes.legend() + + def reset(self): + self.function_field.set_value(task.f_str()) + self.x_0_field.set_value(0.0) + self.x_n_field.set_value(1.0) + self.n_slider.set_value(5) + self.margin_slider.set_value(5) + + QtWidgets.QApplication.processEvents() + + self.function_params = task.f_args() + self.function_param_fields.set_value(self.function_params) + + QtWidgets.QApplication.processEvents() + + self.redraw_plots() + + QtWidgets.QApplication.processEvents() + + def redraw_plots(self): + length = abs(self.x_n - self.x_0) + margin = length * self.margin / 100.0 + + x_min = self.x_0 - margin + x_max = self.x_n + margin + + x_values = np.linspace(x_min, x_max, 200) + y_values = self.function(x_values) + y_interpolated = self.interpolated_function(x_values) + + self.function_plot.clear_plots() + + self.function_plot.add_plot( + lambda axes: axes.set_xlim(x_min - length * 0.05, x_max + length * 0.05) + ) + + self.function_plot.add_plot( + lambda axes: axes.plot(x_values, y_values, label="y = f(x)", color="red") + ) + self.function_plot.add_plot( + lambda axes: axes.plot( + x_values, + y_interpolated, + label="y = L(x)", + color="blue", + linestyle="dashed", + ) + ) + + points = [self.x_0, self.x_n] + points[1:1] = self.points + points = np.array(points) + + # points = np.linspace(self.x_0, self.x_n, self.n + 1) + self.function_plot.add_plot( + lambda axes: axes.scatter( + points, self.function(points), color="green", zorder=10 + ) + ) + + def label_func(axes): + for i in range(len(points)): + axes.text( + points[i], + self.function(points[i]) + 0.05, + str(i), + ha="center", + va="bottom", + color="green", + ) + + self.function_plot.add_plot(label_func) + + self.error_plot.clear_plots() + + self.error_plot.add_plot( + lambda axes: axes.set_xlim(x_min - length * 0.05, x_max + length * 0.05) + ) + + self.error_plot.add_plot( + lambda axes: axes.plot( + x_values, + np.abs(y_values - y_interpolated), + color="blue", + label="|f(x) - L(x)|", + ) + ) + + self.function_plot.redraw() + self.error_plot.redraw() + + def parse_function(self, func_str: str) -> Callable[[float], float]: + func = sympy.sympify(func_str) + params = [] + for name in func.atoms(sympy.Symbol): + params.append(str(name)) + + if len(params) == 0 or params.count("x") < 1: + raise ValueError("No x in function") + + func_lambda = sympy.lambdify(params, func, cse=True) + + if func_lambda.__code__.co_argcount != len(params): + raise ValueError("Bad arguments count") + + params = [p for p in params if p != "x"] + old_params = self.function_params + if set(old_params.keys()) != set(params): + self.recreate_params_field(params) + + def f(x: float) -> float: + return func_lambda(x=x, **self.function_params) + + return f + + def update_function(self): + if self.parsing_function: + return + + try: + self.recalculate_interpolated_function() + self.redraw_plots() + except Exception as e: + print(e) + + def recalculate_interpolated_function(self): + points = [self.x_0, self.x_n] + points[1:1] = self.points + points = np.array(points) + + self.interpolated_function = algorithm.make_forward_interpolate_function( + self.function, points + ) + + # self.interpolated_function = algorithm.make_backward_interpolate_function( + # self.function, points + # ) + + def recreate_params_field(self, params: List[str]): + self.function_param_fields.recreate(params) + + def recreate_points_field(self): + points = np.linspace(self.x_0, self.x_n, self.n + 1) + points = [float(p) for p in points[1:-1]] + self.points_field.recreate( + 1, + points, + self.x_0 + 1 / self.points_field.DIVISIONS, + self.x_n - 1 / self.points_field.DIVISIONS, + ) + + self.points = points + + def _function_field_changed(self, w: widgets.InputWidget) -> bool: + try: + self.parsing_function = True + self.function = self.parse_function(w.get_value()) + self.parsing_function = False + except Exception as e: + print(f"{e}") + self.parsing_function = False + return False + + self.update_function() + + return True + + def _function_param_field_changed(self, w: widgets.InputWidget) -> bool: + self.function_params = self.function_param_fields.get_value() + + self.update_function() + return True + + def _x_0_field_changed(self, w: widgets.InputWidget) -> bool: + value = float(w.get_value()) + if value == self.x_n: + w.value = w.previous_value + return False + + self.x_0 = value + + self.recreate_points_field() + + self.update_function() + return True + + def _x_n_field_changed(self, w: widgets.InputWidget) -> bool: + value = float(w.get_value()) + if value == self.x_0: + w.value = w.previous_value + return False + + self.x_n = value + + self.recreate_points_field() + + self.update_function() + return True + + def _n_slider_changed(self, w: widgets.InputWidget) -> bool: + self.n = int(w.get_value()) + + self.recreate_points_field() + + # self.update_function() + return True + + def _points_field_changed(self, w: widgets.InputWidget) -> bool: + self.points = w.get_value() + + self.update_function() + return True + + def _margin_slider_changed(self, w: widgets.InputWidget) -> bool: + self.margin = float(w.get_value()) + + self.redraw_plots() + return True