Сгенерировать ограниченный путь, используя полином кубической или пятой степени

Мне нужно написать программу для создания пути с использованием полиномов кубической/пятой степени.

Я написал следующий код для создания пути для трехмерного пространства. Он строит путь (используя кубический полином) с ограничениями на начальную точку, целевую точку, начальную скорость и целевую скорость.

import numpy as np
import matplotlib.pyplot as plt


def cubic_trajectory(x0, xf, v0, vf, T):
    # Calculate coefficients for cubic polynomial
    a0 = x0
    a1 = v0
    a2 = (3 * (xf - x0) - (2 * v0 + vf) * T) / T ** 2
    a3 = (2 * (x0 - xf) + (v0 + vf) * T) / T ** 3
    return a0, a1, a2, a3


def generate_trajectory(a0, a1, a2, a3, T, num_points=100):
    t = np.linspace(0, T, num_points)
    x_t = a0 + a1 * t + a2 * t ** 2 + a3 * t ** 3
    return t, x_t


def plot_trajectories_3d(trajectories):
    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')

    for traj in trajectories:
        t, x_t, y_t, z_t = traj
        ax.plot(x_t, y_t, z_t,
                label=f'Trajectory from ({x_t[0]}, {y_t[0]}, {z_t[0]}) to ({x_t[-1]}, {y_t[-1]}, {z_t[-1]})')

    ax.set_xlabel('X Position')
    ax.set_ylabel('Y Position')
    ax.set_zlabel('Z Position')
    ax.set_title('3D Cubic Polynomial Trajectories')
    ax.legend()
    plt.show()


# Define multiple start and end conditions for 3D
conditions_3d = [
    {'x0': (-0.5, 0.0, 0.5), 'xf': (0.5, 0.0, 1.0), 'v0': (0, 0, 0), 'vf': (0, 10, 0), 'T': 5},
    #{'x0': (5, 5, 5), 'xf': (15, 15, 15), 'v0': (1, 1, 1), 'vf': (-1, -1, -1), 'T': 6},
    #{'x0': (-3, -3, -3), 'xf': (7, 7, 7), 'v0': (0, 0, 0), 'vf': (2, 0, 0), 'T': 4},
]

trajectories_3d = []
for cond in conditions_3d:
    a0_x, a1_x, a2_x, a3_x = cubic_trajectory(cond['x0'][0], cond['xf'][0], cond['v0'][0], cond['vf'][0], cond['T'])
    a0_y, a1_y, a2_y, a3_y = cubic_trajectory(cond['x0'][1], cond['xf'][1], cond['v0'][1], cond['vf'][1], cond['T'])
    a0_z, a1_z, a2_z, a3_z = cubic_trajectory(cond['x0'][2], cond['xf'][2], cond['v0'][2], cond['vf'][2], cond['T'])

    t, x_t = generate_trajectory(a0_x, a1_x, a2_x, a3_x, cond['T'])
    _, y_t = generate_trajectory(a0_y, a1_y, a2_y, a3_y, cond['T'])
    _, z_t = generate_trajectory(a0_z, a1_z, a2_z, a3_z, cond['T'])

    trajectories_3d.append((t, x_t, y_t, z_t))

plot_trajectories_3d(trajectories_3d)

Теперь мне нужно расширить этот код так, чтобы сгенерированный путь находился внутри определенного региона. Например, в синей области на изображении ниже.

ВОПРОС:

  • Как добавить ограничения к полиному кубической или пятой степени, чтобы сгенерированная линия находилась внутри региона. Например, в синей области ниже:

Например, если я создаю линию от (3,4) до (8,5), выходная линия должна выглядеть как красная линия на изображении ниже (означает, что она не проходит через белую часть):

Спасибо

В вопросе нет ничего о программировании, нет ни одной строчки кода, попробуйте обмен математическим стеком вместо этого переполнение стека предназначено только для вопросов по программированию.

Ahmed AEK 18.07.2024 10:01

@AhmedAEK Спасибо за предложение. Я добавил код того, что я сделал до сих пор. Я обычно избегаю добавления кода на начальном этапе, чтобы получить другое мнение людей о проблеме. Если я добавлю код изначально, то ответы будут вращаться вокруг метода, который я сейчас использую, и я могу упустить другие методы, которые имеют в виду люди.

Pratham 18.07.2024 11:33

Ваш вопрос не получает просмотров, вам нужно пометить язык программирования конкретным вопросом по программированию, вопрос слишком широк, даже если он получил просмотры, на него вряд ли ответят как есть, я думаю, вам следует вместо этого перейти на maxchange. Мое мнение о вашей проблеме состоит в том, чтобы итеративно разделить линию на несколько сегментов и оптимизировать контрольные точки до тех пор, пока все точки не окажутся в пределах вашего ограничения, используя какой-либо метод штрафа.

Ahmed AEK 18.07.2024 11:56

Я задал вопрос на Math StackExchange, но не получил никакого решения.

Pratham 19.07.2024 03:36

Хорошая работа по улучшению вопроса!

eric 19.07.2024 03:47

@Reinderien Да, начальная и конечная точки, а также все промежуточные точки сгенерированного пути должны находиться в синей области. Не имеет значения, где проходит путь до начальной точки и после конечной точки.

Pratham 19.07.2024 07:24

@Reinderien Изображение, которое я добавил в этот пост, — 2D, но на самом деле я хочу, чтобы оно было 3D (например, путь в коде). У меня не получилось нарисовать 3D рисунок. Его 3D-изображение не имело бы особого смысла в 2D. Просто для понимания предположим, что есть 2 СФЕРЫ одна внутри другой. Все точки сгенерированного пути должны находиться в пределах объема большой сферы МИНУС(-) объема маленькой сферы.

Pratham 19.07.2024 07:32

Что такое v, T? Почему они определяются рядом с x? Кажется, что ваша начальная и конечная точки полностью определяются x, а v и T следует оставить для оптимизации.

Reinderien 19.07.2024 13:58

@Reinderienwe T — независимая переменная, а x, y и z — зависимые переменные. Т означает время. v — скорость, которую я хотел бы иметь в начале и конце пути. мы вычисляем коэффициенты для каждой оси отдельно (a0_x, a1_x, a2_x, a3_x для x, a0_y, a1_y, a2_y, a3_y для y и a0_z, a1_z, a2_z, a3_z для z), а затем траекторию для каждой оси отдельно (x_t для x, y_t для y и z_t для z). Позже мы объединяем эти траектории, чтобы получить 3D-траекторию. Дайте мне знать, если что-то будет неясно.

Pratham 19.07.2024 16:52

Если T — независимая переменная, как она может осмысленно принимать одно значение, равное 5? Это общее время?

Reinderien 19.07.2024 20:09

@Reinderien да, это общее время, необходимое для достижения цели от начала до цели.

Pratham 20.07.2024 03:14

Ваш cubic_trajectory — это фиксированный метод, учитывая скорости и положения конечных точек, для генерации полинома для одного измерения. Это просто для создания исходного полинома? Оптимизатору нужно что-то изменить — что он может изменить?

Reinderien 20.07.2024 14:57

@Reinderien Да, мы вызовем cubic_trajectory 3 раза (один для x, затем y, следующий z), а затем объединим их, чтобы получить трехмерный график. Необходимо было применить два ограничения: первое — задать определенную скорость в начале и в конце. Другой вариант должен был иметь траекторию внутри определенной области (например, области между двумя сферами). Я могу применить ограничение на скорость, но понятия не имею, как применить ограничение на наличие точек траектории в определенной области.

Pratham 20.07.2024 15:16

Давайте продолжим обсуждение в чате.

Reinderien 20.07.2024 15:17

@Reinderien Что касается вашего вопроса об оптимизаторе, я думаю, что мы могли бы изменить только эти 4 коэффициента таким образом, чтобы были удовлетворены оба ограничения (одно для скорости, а другое для конкретной области).

Pratham 20.07.2024 15:17
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
15
58
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Определенно возможно создать ограниченный путь, но это может быть невозможно реализовать как путь роботизированной руки, если вы также не добавите ограничения на ускорение.

Вам также необходимо выбрать цель, выходящую за рамки просто «реального пути». Например, после первого прохода, который устанавливает возможный путь, вы можете выполнить второй проход, который ужесточает ограничения на стоимость радиуса и представляет полную энергию как стоимость. Полную энергию можно вывести из интеграла абсолютного ускорения, которое, в свою очередь, можно рассчитать на основе второго вызова gradient.

import dataclasses
import functools
import typing

import numpy as np
import matplotlib.pyplot as plt
import scipy.optimize
from numpy.polynomial import Polynomial

if typing.TYPE_CHECKING:
    from mpl_toolkits.mplot3d import Axes3D
    from mpl_toolkits.mplot3d.art3d import Path3DCollection


class Path(typing.NamedTuple):
    space: 'PathSpace'
    polys: tuple[Polynomial, ...]
    position: np.ndarray

    @classmethod
    def from_array(cls, space: 'PathSpace', params: np.ndarray) -> 'typing.Self':
        by_dim = params.reshape((len(space.p0), -1))
        polys = tuple(
            Polynomial(symbol='t', coef=coef) for coef in by_dim
        )
        return cls(
            space=space, polys=polys,
            position=np.array([
                poly(space.t) for poly in polys
            ]),
        )

    def velocity(self) -> np.ndarray:
        # Numerical, less-accurate alternative:
        # return np.gradient(self.position, self.space.dt, axis=1)

        series = [
            poly.deriv()(self.space.t)
            for poly in self.polys
        ]
        return np.array(series)

    def position_endpoints(self) -> np.ndarray:
        return self.position[:, [0, -1]]

    def velocity_endpoints(self) -> np.ndarray:
        return self.velocity()[:, [0, -1]]

    def endpoints(self) -> np.ndarray:
        return np.concatenate((self.position_endpoints().T, self.velocity_endpoints().T))

    def lower_radii(self) -> np.ndarray:
        return np.linalg.norm(self.position.T - self.space.inner_sphere_centre, axis=1)

    def upper_radii(self) -> np.ndarray:
        return np.linalg.norm(self.position.T - self.space.outer_sphere_centre, axis=1)

    def energy(self) -> float:
        accel_polys = [
            poly.deriv(2)
            for poly in self.polys
        ]
        abs_accel = np.abs([
            poly(self.space.t)
            for poly in accel_polys
        ])
        return abs_accel.sum(axis=1)

    def dump(self) -> None:
        print('Polynomials:')
        for sym, poly in zip('xyz', self.polys):
            print('    ', sym, '=', poly)
        print('Position endpoints approximating', self.space.p0, self.space.p1)
        print(self.position_endpoints().round(6))
        print('Velocity endpoints approximating', self.space.v0, self.space.v1)
        print(self.velocity_endpoints().round(6))
        print(
            f'Worst-case radii: '
            f'{self.space.inner_sphere_radius} <= {self.lower_radii().min():.2f}, '
            f'{self.upper_radii().max():.2f} <= {self.space.outer_sphere_radius}'
        )

    def plot(self, ax: 'Axes3D', title: str) -> None:
        ax.plot3D(*self.position, label=title)


@dataclasses.dataclass(frozen=True)
class PathSpace:
    p0: tuple[float, float, float]  # Start position
    p1: tuple[float, float, float]  # End position
    v0: tuple[float, float, float]  # Start velocity
    v1: tuple[float, float, float]  # End velocity
    inner_sphere_centre: tuple[float, float, float]  # Lower bound centroid
    outer_sphere_centre: tuple[float, float, float]  # Upper bound centroid
    inner_sphere_radius: float  # Lower bound radius
    outer_sphere_radius: float  # Upper bound radius
    duration: float  # Total time
    resolution: int = 50  # Number of points in the generated path
    order: int = 5  # of the path polynomials

    @functools.cached_property
    def t(self) -> np.ndarray:
        return np.linspace(start=0, stop=self.duration, num=self.resolution + 1)

    @functools.cached_property
    def dt(self) -> float:
        return self.duration/self.resolution

    def initial_polys(self) -> np.ndarray:
        coefs = np.zeros((len(self.p0), 1 + self.order))
        coefs[:, 0] = self.p0
        coefs[:, 1] = (np.array(self.p1) - self.p0)/self.duration
        return coefs

    def solve(self) -> tuple[Path, Path]:
        flat_endpoints = np.concatenate((self.p0, self.p1, self.v0, self.v1))
        endpoint_constraint = scipy.optimize.NonlinearConstraint(
            fun=self.endpoints, lb=flat_endpoints, ub=flat_endpoints,
        )

        feasible_result = scipy.optimize.minimize(
            fun=self.lstsq_error,
            x0=self.initial_polys().ravel(),
            constraints=endpoint_constraint,
        )
        if not feasible_result.success:
            raise ValueError(feasible_result.message)

        energy_result = scipy.optimize.minimize(
            fun=self.energy_cost,
            x0=feasible_result.x,
            constraints=(
                endpoint_constraint,
                scipy.optimize.NonlinearConstraint(
                    fun=self.lower_radii, lb=self.inner_sphere_radius, ub=float('inf'),
                ),
                scipy.optimize.NonlinearConstraint(
                    fun=self.upper_radii, lb=-float('inf'), ub=self.outer_sphere_radius,
                ),
            ),
        )

        return (
            Path.from_array(space=self, params=feasible_result.x),
            Path.from_array(space=self, params=energy_result.x),
        )

    def lstsq_error(self, params: np.ndarray) -> float:
        path = Path.from_array(space=self, params=params)
        lower_errors = (
            self.inner_sphere_radius - path.lower_radii()
        ).clip(min=0)
        upper_errors = (
            path.upper_radii() - self.outer_sphere_radius
        ).clip(min=0)
        errors = np.concatenate((lower_errors, upper_errors))

        # convert error vector to least-squares scalar
        return errors.dot(errors)

    def endpoints(self, params: np.ndarray) -> np.ndarray:
        path = Path.from_array(space=self, params=params)
        return path.endpoints().ravel()

    def energy_cost(self, params: np.ndarray) -> float:
        path = Path.from_array(space=self, params=params)
        energy = path.energy()
        return energy.sum()

    def lower_radii(self, params: np.ndarray) -> np.ndarray:
        return Path.from_array(space=self, params=params).lower_radii()

    def upper_radii(self, params: np.ndarray) -> np.ndarray:
        return Path.from_array(space=self, params=params).upper_radii()

    def plot(self, ax: 'Axes3D') -> None:
        ax.scatter3D(*self.p0, label='p0')
        ax.scatter3D(*self.p1, label='p1')

        sphere(
            ax=ax, u_count=20j, j_count=10j,  label='inner bound',
            radius=self.inner_sphere_radius, centre=self.inner_sphere_centre,
        )
        sphere(
            ax=ax, u_count=40j, j_count=20j, label='outer bound',
            radius=self.outer_sphere_radius, centre=self.outer_sphere_centre, alpha=0.3,
        )

        ax.set_xlabel('x')
        ax.set_ylabel('y')
        ax.set_zlabel('z')


def sphere(
    ax: 'Axes3D',
    u_count: complex,
    j_count: complex,
    radius: float,
    centre: tuple[float, float, float],
    label: str,
    alpha: float = 1,
) -> 'Path3DCollection':
    u, v = np.mgrid[0:2*np.pi:u_count, 0:np.pi:j_count]
    x = np.cos(u) * np.sin(v)
    y = np.sin(u) * np.sin(v)
    z = np.cos(v)
    xi = radius*x + centre[0]
    yi = radius*y + centre[1]
    zi = radius*z + centre[2]
    return ax.scatter3D(xi, yi, zi, s=1, alpha=alpha, label=label)


def main() -> None:
    # These do NOT reflect the reality of a 1.2m four-link robotic arm.
    all_spaces = (
        PathSpace(
            # v1=(0, 10, 0) is not practical
            p0=(-0.5, 0., 0.5), v0=(0., 0., 0.),
            p1=( 0.5, 0., 1. ), v1=(0., 5., 0.),
            duration=5.,
            inner_sphere_centre=(0.1, -0.1, 0.2), inner_sphere_radius=0.6,
            outer_sphere_centre=(0. ,  0. , 1. ), outer_sphere_radius=1. ,
        ),
        PathSpace(
            p0=( 5.,  5.,  5.), v0=( 1.,  1.,  1.),
            p1=(15., 15., 15.), v1=(-1., -1., -1.),
            duration=6.,
            inner_sphere_centre=(0.1, -0.1, 0.2), inner_sphere_radius= 7.,
            outer_sphere_centre=(0. ,  0. , 1. ), outer_sphere_radius=28.,
        ),
        PathSpace(
            p0=(-3., -3., -3.), v0=(0., 0., 0.),
            p1=( 7.,  7.,  7.), v1=(2., 0., 0.),
            duration=4.,
            inner_sphere_centre=(0.1, -0.1, 0.2), inner_sphere_radius= 3.5,
            outer_sphere_centre=(0. ,  0. , 1. ), outer_sphere_radius=15. ,
        ),
    )

    for space in all_spaces:
        feasible_path, energy_path = space.solve()
        print('Feasible path:')
        feasible_path.dump()
        print('Energy-efficient path:')
        energy_path.dump()
        print()

        fig, ax = plt.subplots(subplot_kw = {'projection': '3d'})
        space.plot(ax)
        feasible_path.plot(ax, title='Feasible')
        energy_path.plot(ax, title='Efficient')
        ax.legend()
    plt.show()


if __name__ == '__main__':
    main()
Feasible path:
Polynomials:
     x = -0.5 + (1.62772267e-23) t + 0.3503209 t**2 - 0.28280783 t**3 +
0.07908462 t**4 - 0.00698718 t**5
     y = 0.0 + (1.20507891e-23) t - 2.63144819 t**2 + 2.45195622 t**3 -
0.7050087 t**4 + 0.06397508 t**5
     z = 0.5 + (1.6085689e-18) t + 0.16248468 t**2 - 0.0582134 t**3 +
0.0077872 t**4 - 0.00036878 t**5
Position endpoints approximating (-0.5, 0.0, 0.5) (0.5, 0.0, 1.0)
[[-0.5  0.5]
 [ 0.  -0. ]
 [ 0.5  1. ]]
Velocity endpoints approximating (0.0, 0.0, 0.0) (0.0, 5.0, 0.0)
[[ 0. -0.]
 [ 0.  5.]
 [ 0.  0.]]
Worst-case radii: 0.6 <= 0.66, 1.00 <= 1.0
Energy-efficient path:
Polynomials:
     x = -0.5 + (5.87576423e-25) t + 0.39036819 t**2 - 0.29438286 t**3 +
0.07890896 t**4 - 0.00680942 t**5
     y = 0.0 - (1.23326358e-25) t - 2.69354583 t**2 + 2.50058162 t**3 -
0.71700715 t**4 + 0.06492653 t**5
     z = 0.5 + (4.02918561e-22) t + 0.17961151 t**2 - 0.07440046 t**3 +
0.0122068 t**4 - 0.00074223 t**5
Position endpoints approximating (-0.5, 0.0, 0.5) (0.5, 0.0, 1.0)
[[-0.5  0.5]
 [ 0.  -0. ]
 [ 0.5  1. ]]
Velocity endpoints approximating (0.0, 0.0, 0.0) (0.0, 5.0, 0.0)
[[ 0.  0.]
 [-0.  5.]
 [ 0.  0.]]
Worst-case radii: 0.6 <= 0.66, 1.00 <= 1.0

Feasible path:
Polynomials:
     x = 5.0 + 1.0 t + 0.00186909 t**2 + 0.0074451 t**3 + 0.0220539 t**4 -
0.00337671 t**5
     y = 5.0 + 1.0 t + 0.00186909 t**2 + 0.0074451 t**3 + 0.0220539 t**4 -
0.00337671 t**5
     z = 5.0 + 1.0 t + 0.00186909 t**2 + 0.0074451 t**3 + 0.0220539 t**4 -
0.00337671 t**5
Position endpoints approximating (5.0, 5.0, 5.0) (15.0, 15.0, 15.0)
[[ 5. 15.]
 [ 5. 15.]
 [ 5. 15.]]
Velocity endpoints approximating (1.0, 1.0, 1.0) (-1.0, -1.0, -1.0)
[[ 1. -1.]
 [ 1. -1.]
 [ 1. -1.]]
Worst-case radii: 7.0 <= 8.55, 25.61 <= 28.0
Energy-efficient path:
Polynomials:
     x = 5.0 + 1.0 t + 0.89433314 t**2 - 0.36750624 t**3 + 0.07266568 t**4 -
0.00552847 t**5
     y = 5.0 + 1.0 t + 0.89429503 t**2 - 0.36746431 t**3 + 0.07265488 t**4 -
0.00552766 t**5
     z = 5.0 + 1.0 t + 0.89426736 t**2 - 0.3674362 t**3 + 0.07264781 t**4 -
0.00552713 t**5
Position endpoints approximating (5.0, 5.0, 5.0) (15.0, 15.0, 15.0)
[[ 5. 15.]
 [ 5. 15.]
 [ 5. 15.]]
Velocity endpoints approximating (1.0, 1.0, 1.0) (-1.0, -1.0, -1.0)
[[ 1. -1.]
 [ 1. -1.]
 [ 1. -1.]]
Worst-case radii: 7.0 <= 8.55, 25.65 <= 28.0

Feasible path:
Polynomials:
     x = -3.0 + (4.4408921e-16) t + 0.53461923 t**2 + 1.44358752 t**3 -
0.65797237 t**4 + 0.07568107 t**5
     y = -3.0 + (4.4408921e-16) t + 3.96838631 t**2 + 2.24463206 t**3 -
1.67107596 t**4 + 0.22523908 t**5
     z = -3.0 + 0.0 t - 1.07776477 t**2 + 1.07596496 t**3 - 0.14058908 t**4 -
0.00549484 t**5
Position endpoints approximating (-3.0, -3.0, -3.0) (7.0, 7.0, 7.0)
[[-3.  7.]
 [-3.  7.]
 [-3.  7.]]
Velocity endpoints approximating (0.0, 0.0, 0.0) (2.0, 0.0, 0.0)
[[0. 2.]
 [0. 0.]
 [0. 0.]]
Worst-case radii: 3.5 <= 4.05, 14.62 <= 15.0
Energy-efficient path:
Polynomials:
     x = -3.0 - (5.55905085e-22) t + 2.69660108 t**2 - 1.23659809 t**3 +
0.27674884 t**4 - 0.0242686 t**5
     y = -3.0 + (7.33606319e-22) t + 5.00901898 t**2 - 2.89516277 t**3 +
0.70370283 t**4 - 0.06347833 t**5
     z = -3.0 - (2.98328286e-20) t - 1.40354787 t**2 + 1.2119847 t**3 -
0.14751462 t**4 - 0.00717433 t**5
Position endpoints approximating (-3.0, -3.0, -3.0) (7.0, 7.0, 7.0)
[[-3.  7.]
 [-3.  7.]
 [-3.  7.]]
Velocity endpoints approximating (0.0, 0.0, 0.0) (2.0, 0.0, 0.0)
[[-0.  2.]
 [ 0. -0.]
 [-0. -0.]]
Worst-case radii: 3.5 <= 3.50, 11.58 <= 15.0

И «Возможный», и «Эффективный» путь следуют ограничению скорости? Эффективный путь — это оптимизированная версия для экономии энергии? Код работает хорошо, но немного медленно, возможно, из-за построения графиков и второго прохода оптимизации. Программирование на C++ может помочь. Рассмотрю это со своей стороны. Спасибо.

Pratham 21.07.2024 08:04

Да и да. Попробуйте удалить график, чтобы оценить скорость.

Reinderien 21.07.2024 14:50

Другие вопросы по теме