Вместо введения


Системы автоматического управления (САУ) предназначены для автоматического изменения одного или нескольких параметров объекта управления с целью установления требуемого режима его работы. САУ обеспечивает поддержание постоянства заданных значений регулируемых параметров или их изменение по заданному закону либо оптимизирует определенные критерии качества управления. Например, к таким системам относятся:


  • системы стабилизации,
  • системы программного управления,
  • следящие системы

Это достаточно широкий класс систем, которые можно найти где угодно. Но какое это отношение имеет к Unity3D и вероятно к играм в частности? В принципе прямое: в любой игре так или иначе использующей симуляцию как элемент геймплея реализуются САУ, к таким играм относятся, например, Kerbal Space Programm, Digital Combat Simulator (бывший Lock On), Strike Suit Zero и т.д. (кто знает еще примеры — пишите в комментариях). В принципе любая игра, моделирующая реальные физические процессы, в том числе и просто кинематику с динамикой движения, может реализовывать те или иные САУ — этот подход проще, естественнее, а у разработчика уже есть есть набор готовых инструментов, предоставленных всякими Вышнеградскими, Ляпуновыми, Калманами, Чебышевами и прочими Коломогоровами, поэтому можно обойтись без изобретения велосипеда, т.к. его уже изобрели, да так, что получилась отдельная наука: Теория автоматического управления. Главное тут не переусердствовать. Одна тут только проблема: рассказывают про ТАУ не везде, не всем, зачастую мало и не очень понятно.


Немножко теории


Классическая система автоматического управления представленная на следующем рисунке:


image


Ключевым элементом любой САУ является регулятор представляющий из себя устройство, которое следит за состоянием объекта управления и обеспечивает требуемый закон управления. Процесс управления включает в себя: вычисление ошибки управления или сигнала рассогласования e(t) как разницы между желаемой уставкой (set point или SP) и текущей величиной процесса (process vale или PV), после чего регулятор вырабатывает управляющие сигналы (manipulated value или MV).


Одной из разновидностью регуляторов является пропорционально-интегрально-дифференцирующий (ПИД) регулятор, который формирует управляющий сигнал, являющийся суммой трёх слагаемых: пропорционального, интегрального и дифференциального.


image


Где, $e(t)$ ошибка рассогласования, а также, $ P = K_p \cdot e(t)$ — пропорциональная, $ I = K_i \cdot \int_0^t e(\tau)d\tau$ — интегральная, $D = K_d \cdot \frac{de(t)}{dt}$ — дифференциальная составляющие (термы) закона управления, который в итоговом виде описывается следующими формулами


$ e(t) = SP(t) - PV(t), $


$ MV(t) = \underbrace{K_p \cdot e(t)}_{P} + \underbrace{K_i \cdot \int_0^t e(\tau)d\tau}_{I} + \underbrace{K_d \cdot \frac{de(t)}{dt}}_{D}, $


Пропорциональная составляющая P — отвечает за т.н. пропорциональное управление, смысл которого в том, что выходной сигнал регулятора, противодействует отклонению регулируемой величины (ошибки рассогласования или еще это называют невязкой) от заданного значения. Чем больше ошибка рассогласования, тем больше командное отклонение регулятора. Это самый простой и очевидный закон управления. Недостаток пропорционального закона управления заключается в том, что регулятор никогда не стабилизируется в заданном значении, а увеличение коэффициента пропорциональности всегда приводит к автоколебаниям. Именно поэтому в довесок к пропорциональному закону управления приходиться использовать интегральный и дифференциальный.


Интегральная составляющая I накапливает (интегрирует) ошибку регулирования, что позволяет ПИД-регулятору устранять статическую ошибку (установившуюся ошибку, остаточное рассогласование). Или другими словами: интегральное звено всегда вносит некоторое смещение и если система подвержена некоторыми постоянным ошибкам, то оно их компенсирует (за счет своего смещения). А вот если же этих ошибок нет или они пренебрежительно малы, то эффект будет обратным — интегральная составляющая сама будет вносить ошибку смещения. Именно по этой причине её не используют, например, в задачах сверхточного позиционирования. Ключевым недостатком интегрального закона управления является эффект насыщения интегратора (Integrator windup).


Дифференциальная составляющая D пропорциональна темпу изменения отклонения регулируемой величины и предназначена для противодействия отклонениям от целевого значения, которые прогнозируются в будущем. Примечательно то, что дифференциальная компонента устраняет затухающие колебания. Дифференциальное регулирование особенно эффективно для процессов, которые имеют большие запаздывания. Недостатком дифференциального закона управления является его неустойчивость к воздействую шумов (Differentiation noise).


Таким образом, в зависимости от ситуации могут применятся П-, ПД-, ПИ- и ПИД-регуляторы, но основным законом управления в основном является пропорциональный (хотя в некоторых специфических задачах и могут использоваться исключительно только звенья дифференциаторов и интеграторов).


Казалось бы, вопрос реализации ПИД-регуляторов уже давно избит и здесь на Хабре есть парочка неплохих статей на эту тему в том числе и на Unity3D, также есть неплохая статья PID Without a PhD (перевод) и цикл статей в журнале "Современные технологии автоматизации" в двух частях: первая и вторая. Также к вашим услугам статья на Википедии (наиболее полную читайте в английском варианте). А на форумах коммьюнити Unity3D нет-нет, да и всплывет PID controller как и на gamedev.stackexchange


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


Попытка номер раз


В качестве примера попытаемся реализовать схему регулирования на примере управления поворотом в простенькой космической 2D-аркаде, по шагам, начиная с самого начала (не забыли, что это туториал?).


Почему не 3D? Потому что реализация не измениться, за исключением того, что придется воротить ПИД-регулятор для контроля тангажа, рысканья и крена. Хотя вопрос корректного применения ПИД-регулирования вместе с кватернионами действительно интересный, возможно в будущем его и освящу, но даже в NASA предпочитают углы Эйлера вместо кватернионов, так что обойдемся простенькой моделью на двухмерной плоскости.


Для начала создадим сам объект игровой объект космического корабля, который будет состоять из собственно самого объекта корабля на верхнем уровне иерархии, прикрепим к нему дочерний объект Engine (чисто спецэффектов ради). Вот как это выглядит у меня:


image


А на сам объект космического корабля накидаем в инспекторе всяческих компонент. Забегая вперед, приведу скрин того, как он будет выглядеть в конце:


image
Но это потом, а пока в нем еще нет никаких скриптов, только стандартный джентльменский набор: Sprite Render, RigidBody2D, Polygon Collider, Audio Source (зачем?).


Собственно физика у нас сейчас самое главное и управление будет осуществляться исключительно через неё, в противном случае, применение ПИД-регулятора потеряло бы смысл. Масса нашего космического корабля оставим также в 1 кг, а все коэффициенты трения и гравитации равны нулю — в космосе же.


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


BaseBody.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

namespace Assets.Scripts.SpaceShooter.Bodies
{
    [RequireComponent(typeof(SpriteRenderer))]
    [RequireComponent(typeof(AudioSource))]
    [RequireComponent(typeof(Rigidbody2D))]
    [RequireComponent(typeof(Collider2D))]

    public class BaseBody : MonoBehaviour
    {
        readonly float _deafultTimeDelay = 0.05f;

[HideInInspector]
        public static List<BaseBody> _bodies = new List<BaseBody>();

        #region RigidBody

        [HideInInspector]
        public Rigidbody2D _rb2d;

        [HideInInspector]
        public Collider2D[] _c2d;

        #endregion

        #region References

        [HideInInspector]
        public Transform _myTransform;

        [HideInInspector]
        public GameObject _myObject;

        /// <summary>
        /// Объект, который появляется при уничтожении
        /// </summary>
        public GameObject _explodePrefab;

        #endregion

        #region  Audio

        public AudioSource _audioSource;

        /// <summary>
        /// Звуки, которые проигрываются при получении повреждения
        /// </summary>
        public AudioClip[] _hitSounds;

        /// <summary>
        /// Звуки, которые проигрываются при появлении объекта
        /// </summary>
        public AudioClip[] _awakeSounds;

        /// <summary>
        /// Звуки, которые воспроизводятся перед смертью
        /// </summary>
        public AudioClip[] _deadSounds;

        #endregion

        #region External Force Variables
        /// <summary>
        /// Внешние силы воздйствующие на объект
        /// </summary>
        [HideInInspector]
        public Vector2 _ExternalForces = new Vector2();

        /// <summary>
        /// Текущий вектор скорости
        /// </summary>
        [HideInInspector]
        public Vector2 _V = new Vector2();

        /// <summary>
        /// Текущий вектор силы гравитации
        /// </summary>
        [HideInInspector]
        public Vector2 _G = new Vector2();
        #endregion

        public virtual void Awake()
        {
            Init();
        }

        public virtual void Start()
        {

        }

        public virtual void Init()
        {
            _myTransform = this.transform;
            _myObject = gameObject;

            _rb2d = GetComponent<Rigidbody2D>();
            _c2d = GetComponentsInChildren<Collider2D>();
            _audioSource = GetComponent<AudioSource>();

            PlayRandomSound(_awakeSounds);

            BaseBody bb = GetComponent<BaseBody>();
            _bodies.Add(bb);
        }

        /// <summary>
        /// Уничтожение персонажа
        /// </summary>
        public virtual void Destroy()
        {
            _bodies.Remove(this);
            for (int i = 0; i < _c2d.Length; i++)
            {
                _c2d[i].enabled = false;
            }
            float _t = PlayRandomSound(_deadSounds);
            StartCoroutine(WaitAndDestroy(_t));
        }

        /// <summary>
        /// Ждем некоторое время перед уничтожением
        /// </summary>
        /// <param name="waitTime">Время ожидания</param>
        /// <returns></returns>
        public IEnumerator WaitAndDestroy(float waitTime)
        {
            yield return new WaitForSeconds(waitTime);

            if (_explodePrefab)
            {
                Instantiate(_explodePrefab, transform.position, Quaternion.identity);
            }

            Destroy(gameObject, _deafultTimeDelay);
        }

        /// <summary>
        /// Проигрывание случайного звука
        /// </summary>
        /// <param name="audioClip">Массив звуков</param>
        /// <returns>Длительность проигрываемого звука</returns>
        public float PlayRandomSound(AudioClip[] audioClip)
        {
            float _t = 0;
            if (audioClip.Length > 0)
            {
                int _i = UnityEngine.Random.Range(0, audioClip.Length - 1);
                AudioClip _audioClip = audioClip[_i];
                _t = _audioClip.length;
                _audioSource.PlayOneShot(_audioClip);
            }
            return _t;
        }

        /// <summary>
        /// Получение урона
        /// </summary>
        /// <param name="damage">Уровень урона</param>
        public virtual void Damage(float damage)
        {
            PlayRandomSound(_hitSounds);
        }

    }
}

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


SpaceShip.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

namespace Assets.Scripts.SpaceShooter.Bodies
{
    public class Ship : BaseBody
    {
        public Vector2 _movement = new Vector2();
        public Vector2 _target = new Vector2();
        public float _rotation = 0f;

        public void FixedUpdate()
        {
            float torque = ControlRotate(_rotation);
            Vector2 force = ControlForce(_movement);

            _rb2d.AddTorque(torque);
            _rb2d.AddRelativeForce(force);
        }

        public float ControlRotate(Vector2 rotate)
        {
            float result = 0f;

            return result;
        }

        public Vector2 ControlForce(Vector2 movement)
        {
            Vector2 result = new Vector2();

            return result;

        }
    }
}

Пока в нем нет ничего интересно, на текущий момент это просто класс-заглушка.


Также опишем базовый(абстрактный) класс для всех контроллеров ввода BaseInputController:


BaseInputController.cs
using UnityEngine;
using Assets.Scripts.SpaceShooter.Bodies;

namespace Assets.Scripts.SpaceShooter.InputController
{
    public enum eSpriteRotation
    {
        Rigth = 0,
        Up = -90,
        Left = -180,
        Down = -270
    }

    public abstract class BaseInputController : MonoBehaviour
    {
        public GameObject _agentObject;
        public Ship _agentBody; // Ссылка на компонент логики корабля
        public eSpriteRotation _spriteOrientation = eSpriteRotation.Up; //Это связано с нестандартной 
                                                                           // ориентации спрайта "вверх" вместо "вправо"

        public abstract void ControlRotate(float dt);
        public abstract void ControlForce(float dt);

        public virtual void Start()
        {
            _agentObject = gameObject;
            _agentBody = gameObject.GetComponent<Ship>();
        }

        public virtual void FixedUpdate()
        {
            float dt = Time.fixedDeltaTime;
            ControlRotate(dt);
            ControlForce(dt);
        }

        public virtual void Update()
        {
            //TO DO
        }
    }
}

И наконец, класс контроллера игрока PlayerFigtherInput:


PlayerInput.cs
using UnityEngine;
using Assets.Scripts.SpaceShooter.Bodies;

namespace Assets.Scripts.SpaceShooter.InputController
{
    public class PlayerFigtherInput : BaseInputController
    {
        public override void ControlRotate(float dt)
        {
            // Определяем позицию мыши относительно игрока
            Vector3 worldPos = Input.mousePosition;
            worldPos = Camera.main.ScreenToWorldPoint(worldPos);

            // Сохраняем координаты указателя мыши
            float dx = -this.transform.position.x + worldPos.x;
            float dy = -this.transform.position.y + worldPos.y;

            //Передаем направление
            Vector2 target = new Vector2(dx, dy);
            _agentBody._target = target;

            //Вычисляем поворот в соответствии с нажатием клавиш
            float targetAngle = Mathf.Atan2(dy, dx) * Mathf.Rad2Deg;
            _agentBody._targetAngle = targetAngle + (float)_spriteOrientation;
        }

        public override void ControlForce(float dt)
        {
            //Передаем movement
            _agentBody._movement = Input.GetAxis("Vertical") * Vector2.up 
                + Input.GetAxis("Horizontal") * Vector2.right;
        }
    }
}

Вроде бы закончили, теперь наконец можно перейти к тому, ради чего все это затевалось, т.е. ПИД-регуляторам (не забыли надеюсь?). Его реализация кажется простой до безобразия:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Assets.Scripts.Regulator
{
    [System.Serializable] // Этот атрибут необходим для того что бы поля регулятора 
                                   // отображались в инспекторе и сериализовывались
    public class SimplePID
    {
        public float Kp, Ki, Kd;

        private float lastError;
        private float P, I, D;

        public SimplePID()
        {
            Kp = 1f;
            Ki = 0;
            Kd = 0.2f;
        }

        public SimplePID(float pFactor, float iFactor, float dFactor)
        {
            this.Kp = pFactor;
            this.Ki = iFactor;
            this.Kd = dFactor;
        }

        public float Update(float error, float dt)
        {
            P = error;
            I += error * dt;
            D = (error - lastError) / dt;
            lastError = error;

            float CO = P * Kp + I * Ki + D * Kd;

            return CO;
        }
    }
}

Значения коэффициентов по умолчанию возьмем с потолка: это будет тривиальный единичный коэффициент пропорционального закона управления Kp = 1, небольшое значение коэффициента для дифференциального закона управления Kd = 0.2, который должен устранить ожидаемые колебания и нулевое значение для Ki, которое выбрано потому, что в нашей программной модели нет никаких статичных ошибок (но вы всегда можете их внести, а потом героически побороться с помощью интегратора).


Теперь вернемся к нашему классу SpaceShip и попробуем заюзать наше творение в качестве регулятора поворота космического корабля в методе ControlRotate:


 public float ControlRotate(Vector2 rotate)
 {
      float MV = 0f;
      float dt = Time.fixedDeltaTime;

      //Вычисляем ошибку
      float angleError = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle);

      //Получаем корректирующее ускорение
      MV = _angleController.Update(angleError, dt);

      return MV;
 }

ПИД-регулятор будет осуществлять точное угловое позиционировая космического корабля только за счет крутящего момента. Все честно, физика и САУ, почти как в реальной жизни.


И без этих ваших Quaternion.Lerp
 if (!_rb2d.freezeRotation)
     rb2d.freezeRotation = true;

 float deltaAngle = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle);
 float T = dt *  Mathf.Abs( _rotationSpeed / deltaAngle);

 // Трансформируем угол в вектор
Quaternion rot = Quaternion.Lerp(
                _myTransform.rotation,
                Quaternion.Euler(new Vector3(0, 0, targetAngle)),
                T);

 // Изменяем поворот объекта
 _myTransform.rotation = rot;

Получившейся исходный код Ship.cs под спойлером
using UnityEngine;
using Assets.Scripts.Regulator;

namespace Assets.Scripts.SpaceShooter.Bodies
{
    public class Ship : BaseBody
    {
        public GameObject _flame;

        public Vector2 _movement = new Vector2();
        public Vector2 _target = new Vector2();

        public float _targetAngle = 0f;
        public float _angle = 0f;

        [Header("PID")]
        public SimplePID _angleController = new SimplePID();

        public void FixedUpdate()
        {
            float torque = ControlRotate(_targetAngle);
            Vector2 force = ControlForce(_movement);

            _rb2d.AddTorque(torque);
            _rb2d.AddRelativeForce(force);
        }

        public float ControlRotate(float rotate)
        {
            float MV = 0f;
            float dt = Time.fixedDeltaTime;

            _angle = _myTransform.eulerAngles.z;

            //Вычисляем ошибку
            float angleError = Mathf.DeltaAngle(_angle, rotate);

            //Получаем корректирующее ускорение
            MV = _angleController.Update(angleError, dt);

            return MV;
        }

        public Vector2 ControlForce(Vector2 movement)
        {
            Vector2 MV = new Vector2();

            //Кусок кода спецэффекта работающего двигателя ради
            if (movement != Vector2.zero)
            {
                if (_flame != null)
                {
                    _flame.SetActive(true);
                }
            }
            else
            {
                if (_flame != null)
                {
                    _flame.SetActive(false);
                }
            }

            MV = movement;
            return MV;
        }
    }
}

Все? Расходимся по домам?



WTF! Что происходит? Почему корабль поворачивается как-то странно? И почему он так резко отскакивает от других объектов? Неужели этот глупый ПИД-регулятор не работает?


Без паники! Давайте попробуем разобраться что происходит.


В момент получения нового значения SP, происходит резкий (ступенчатый) скачок рассогласования ошибки, которая, как мы помним, вычисляется вот так: $e(t) = SP(t) - PV(t), $ соответственно происходит резкий скачок производной ошибки $\frac{de(t)}{dt}$, которую мы вычисляем в этой строчке кода:


D = (error - lastError) / dt;

Можно, конечно, попробовать другие схемы дифференцирования, например, трехточечную, или пятиточечную, или… но все равно это не поможет. Ну вот не любят производные резких скачков — в таких точках функция не является дифференцируемой. Однако поэкспериментировать с разными схемами дифференцирования и интегрирования стоит, но потом и не в этой статье.


Думаю что настал момент построить графики переходного процесса: ступенчатое воздействие от S(t) = 0 в SP(t) = 90 градусов для тела массой в 1 кг, длинной плеча силы в 1 метр и шагом сетки дифференцирования 0.02 с — прям как в нашем примере на Unity3D (на самом деле не совсем, при построении этих графиков не учитывалось, что момент инерции зависит от геометрии твердого тела, поэтому переходный процесс будет немножко другой, но все же достаточно похожий для демонстрации). Все величены на грифике приведены в абсолютных значениях:
image
Хм, что здесь происходит? Куда улетел отклик ПИД-регулятора?


Поздравляю, мы только что столкнулись с таким явлением как "удар" (kick). Очевидно, что в момент времени, когда процесс еще PV = 0, а уставка уже SP = 90, то при численном дифференцировании получим значение производной порядка 4500, которое умножится на Kd=0.2 и сложится с пропорциональным теромом, так что на выходе мы получим значение углового ускорения 990, а это уже форменное надругательство над физической моделью Unity3D (угловые скорости будут достигать 18000 град/с… я думаю это предельное значение угловой скорости для RigidBody2D).


  • Может стоит подобрать коэффициенты ручками, так чтобы скачок был не таким сильным?
  • Нет! Самое лучше чего мы таким образом сможем добиться — небольшая амплитуда скачка производной, однако сам скачок как был так и останется, при этом можно докрутиться до полной неэффективности дифференциальной составляющей.

Впрочем можете поэкспериментировать.


Попытка номер два. Сатурация


Логично, что привод (в нашем случае виртуальные маневровые двигатели SpaceShip), не может отрабатывать сколько угодно большие значения которые может выдать наш безумный регулятор. Так что первое что мы сделаем — сатурируем выход регулятора:


public float ControlRotate(Vector2 rotate, float thrust)
{
    float CO = 0f;
    float MV = 0f;
    float dt = Time.fixedDeltaTime;

    //Вычисляем ошибку
    float angleError = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle);

    //Получаем корректирующее ускорение
    CO = _angleController.Update(angleError, dt);

    //Сатурируем
    MV = CO;
    if (MV > thrust) MV = thrust;
    if (MV< -thrust) MV = -thrust;

    return MV;
}

А очередной раз переписанный класс Ship полностью выглядит так
namespace Assets.Scripts.SpaceShooter.Bodies
{
    public class Ship : BaseBody
    {
        public GameObject _flame;

        public Vector2 _movement = new Vector2();
        public Vector2 _target = new Vector2();

        public float _targetAngle = 0f;
        public float _angle = 0f;

        public float _thrust = 1f;

        [Header("PID")]
        public SimplePID _angleController = new SimplePID(0.1f,0f,0.05f);

        public void FixedUpdate()
        {
            _torque = ControlRotate(_targetAngle, _thrust);
            _force = ControlForce(_movement);

            _rb2d.AddTorque(_torque);
            _rb2d.AddRelativeForce(_force);
        }

        public float ControlRotate(float targetAngle, float thrust)
        {
            float CO = 0f;
            float MV = 0f;
            float dt = Time.fixedDeltaTime;

            //Вычисляем ошибку
            float angleError = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle);

            //Получаем корректирующее ускорение
            CO = _angleController.Update(angleError, dt);

            //Сатурируем
            MV = CO;
            if (MV > thrust) MV = thrust;
            if (MV< -thrust) MV = -thrust;

            return MV;
        }

        public Vector2 ControlForce(Vector2 movement)
        {
            Vector2 MV = new Vector2();

            if (movement != Vector2.zero)
            {
                if (_flame != null)
                {
                    _flame.SetActive(true);
                }
            }
            else
            {
                if (_flame != null)
                {
                    _flame.SetActive(false);
                }
            }

            MV = movement * _thrust;

            return MV;
        }

        public void Update()
        {

        }        
    }
}

Итоговая схема нашего САУ тогда станет уже вот такой
image


При этом уже становится понятно, что выход контроллера CO(t) немного не одно и тоже, что управляемая величина процесса MV(t).


Собственно с этого места можно уже добавлять новую игровую сущность — привод, через которую и будет осуществляться управление процессом, логика работы которой может быть более сложной, чем просто Mathf.Clamp(), например, можно ввести дискретизацию значений (дабы не перегружать игровую физику величинами идущими шестыми после запятой), мертвую зону (опять таки не имеет смысл перегружать физику сверхмалыми реакциями), ввести задержку в упраление и нелинейность (например, сигмоиду) привода, после чего посмотреть, что из этого получится.


Запустив игру, мы обнаружим, что космический корабль стал наконец управляемым:



Если построить графики, то можно увидеть, что реакция контроллера стала уже вот такой:
image
Здесь уже используются нормированные величены, углы поделены на значение SP, а выход контроллера отнормирован относительно максимального значения на котором уже происходит сатурация.


Теперь на графике видно наличие ошибки перерегулирования (overshooting) и затухающие колебания. Уменьшая Kp и увеличивая Kd можно добиться уменьшения колебаний, но зато увеличится время реакции контроллера (скорость поворота корабля). И наоборот, увеличивая Kp и уменьшая Kd — можно добиться увеличения скорости реакции контроллера, но появятся паразитные колебания, которые при определенных (критических) значениях, перестанут быть затухающими.


Ниже приведена известна таблица влияния увеличения параметров ПИД-регулятора (как уменьшить шрифт, а то таблица безе переносов не лезет?):


Терм Время нарастания Ошибка перерегулирования Время переходного процесса Установившаяся ошибка Стабильность
Kp Уменьшение Увеличение Небольшие изменения Уменьшается Деградирует
Ki Уменьшается Увеличивается Увеличивается Компенсируется если есть Деградирует
Kd Небольшие изменения Уменьшается Уменьшается Повышается если Kd небольшая

А общий алгоритм ручной настройки ПИД-регулятора следующий:


  1. Подбираем пропорциональный коэффициенты при отключенных дифференциальных и интегральных звеньях до тех пор пока не начнутся автоколебания.
  2. Постепенно увеличивая дифференциальную составляющую избавляемся от автоколебаний
  3. Если наблюдается остаточная ошибка регулирования (смещение), то устраняем её за счет интегральной составляющей.

Каких-то общих значений параметров ПИД-регулятора нет: конкретные значения зависят исключительно от параметров процесса (его передаточной характеристики): ПИД-регулятор отлично работающий с одним объектом управления окажется неработоспособным с другим. Более того, коэффициенты при пропорциональной, интегральной и дифференциальной составляющих еще и взаимозависимы.


В общем не будем о грустном, дальше нас ждет самое интересное...


Попытка номер три. Еще раз производные


Приделав костыль в виде ограничения значений выхода контроллера мы так и не решили самую главную проблему нашего регулятора — дифференциальная составляющая плохо себя чувствует при ступенчатом изменении ошибки на входе регуляторе. На самом деле есть множество других костылей, например, в момент скачкообразного изменения SP "отключать" дифференциальную составляющую или же поставить фильтры нижних частот между SP(t) и операцией $SP(t)-PV(t)$ за счет которого будет происходить плавное нарастание ошибки, а можно совсем развернуться и впендюрить самый настоящий фильтр Калмана для сглаживания входных данных. В общем костылей много, и добавить наблюдателя конечно хотелось бы, но не в этот раз.


Поэтому снова вернемся к производной ошибки рассогласования и внимательно на неё посмотрим:


$ \frac{de(t)}{dt} = \frac{d(SP(t)-PV(t))}{dt} = \frac{dSP(t)}{dt} - \frac{dPV(t)}{dt}, $


Ничего не заметили? Если хорошенько присмотреться, то можно обнаружить, что вообще-то SP(t), не меняется во времени (за исключением моментов ступенчатого изменения, когда регулятор получает новую команду), т.е. её производная равна нулю:


$ \frac{dSP(t)}{dt} = 0, $


тогда


$ \frac{de(t)}{dt} = - \frac{dPV(t)}{dt}, $


Иными словами, вместо производной ошибки, которая дифференцируема не везде мы можем использовать производную от процесса, который в мире классической механики как правило непрерывен и дифференцируем везде, а схема нашей САУ уже приобретет следующий вид:
image


$ e(t) = SP(t) - PV(t), $


$ CO(t) = \underbrace{K_p \cdot e(t)}_{P} + \underbrace{K_i \cdot \int_0^t e(\tau)d\tau}_{I} - \underbrace{K_d \cdot \frac{dPV(t)}{dt}}_{D}, $


Модифицируем код регулятора:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Assets.Scripts.Regulator
{
    [System.Serializable]
    public class SimplePID
    {
        public float Kp, Ki, Kd;
        private float P, I, D;

        private float lastPV = 0f;   

        public SimplePID()
        {
            Kp = 1f;
            Ki = 0f;
            Kd = 0.2f;
        }

        public SimplePID(float pFactor, float iFactor, float dFactor)
        {
            this.Kp = pFactor;
            this.Ki = iFactor;
            this.Kd = dFactor;
        }

        public float Update(float error, float PV, float dt)
        {
            P = error;
            I += error * dt;
            D = -(PV - lastPV) / dt;

            lastPV = PV;

            float CO = Kp * P + Ki * I + Kd * D;

            return CO;
        }
    }
}

И немного изменим метод ControlRotate:


public float ControlRotate(Vector2 rotate, float thrust)
{
     float CO = 0f;
     float MV = 0f;
     float dt = Time.fixedDeltaTime;

     //Вычисляем ошибку
     float angleError = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle);

     //Получаем корректирующее ускорение
     CO = _angleController.Update(angleError, _myTransform.eulerAngles.z, dt);

     //Сатурируем
     MV = CO;
     if (CO > thrust) MV = thrust;
     if (CO < -thrust) MV = -thrust;

     return MV;
}

И-и-и-и… если запустить игру, то обнаружиться, что на самом деле ничего ничего не изменилось с последней попытки, что и требовалось доказать. Однако, если убрать сатурацию, то график реакции регулятора будет выглядеть вот так:
image
Скачок CO(t) по прежнему присутствует, однако он уже не такой большой как был в самом начале, а самое главное — он стал предсказуемым, т.к. обеспечивается исключительно пропорциональной составляющей, и ограничен максимально возможной ошибкой рассогласования и пропорциональным коэффициентом ПИД-регулятора (а это уже намекает на то, что Kp имеет смысл выбрать все же меньше единицы, например, 1/90f), но не зависит от шага сетки дифференцирования (т.е. dt). В общем, я настоятельно рекомендую использовать именно производную процесса, а не ошибки.


Думаю теперь никого не удивит, но таким же макаром можно заменить $K_p \cdot e(t)$ на $-K_p \cdot PV(t)$, однако останавливаться на этом мы не будем, можете сами поэкспериментировать и рассказать в комментариях, что из этого получилось (самому интересно)


Попытка номер четыре. Альтернативные реализации ПИД-регулятор


Помимо описанного выше идеального представления ПИД-регулятора, на практике часто применяется стандартная форма, без коэффициентов Ki и Kd, вместо которых используются временные постоянные.


Такой подход связан с тем, что ряд методик настройки ПИД-регулятора основан на частотных характеристиках ПИД-регулятора и процесса. Собственно вся ТАУ и крутится вокруг частотных характеристик процессов, поэтому для желающих углубиться, и, внезапно, столкнувшихся с альтернативной номенклатурой, приведу пример т.н. стандартной формы ПИД-регулятора:


$ e(t) = SP(t) - PV(t), $


$ CO(t) =CO_{bias} + K_p \cdot \Bigl(e(t) + \frac{1}{T_i} \cdot \int_0^t e(\tau)d\tau - T_d \cdot \frac{dPV(t)}{dt} \Bigl), $


где, $T_d= \frac{K_d}{K_p}$ — постоянная дифференцирования, влияющая на прогнозирование состояния системы регулятором,
$T_i = \frac{K_p}{K_i}$ — постоянная интегрирования, влияющая на интервал усреднения ошибки интегральным звеном.


Основные принципы настройки ПИД-регулятора в стандартной форме аналогичны идеализированному ПИД-регулятору:


  • увеличение пропорционального коэффициента увеличивает быстродействие и снижает запас устойчивости;
  • с уменьшением интегральной составляющей ошибка регулирования с течением времени уменьшается быстрее;
  • уменьшение постоянной интегрирования уменьшает запас устойчивости;
  • увеличение дифференциальной составляющей увеличивает запас устойчивости и быстродействие

Исходный код стандартной формы, вы можете найти под спойлером
namespace Assets.Scripts.Regulator
{
    [System.Serializable]    
    public class StandartPID
    {
        public float Kp, Ti, Td;
        public float error, CO;
        public float P, I, D;

        private float lastPV = 0f;

        public StandartPID()
        {
            Kp = 0.1f;
            Ti = 10000f;
            Td = 0.5f;
            bias = 0f;
        }

        public StandartPID(float Kp, float Ti, float Td)
        {
            this.Kp = Kp;
            this.Ti = Ti;
            this.Td = Td;
        }

        public float Update(float error, float PV, float dt)
        {
            this.error = error;
            P = error;
            I += (1 / Ti) * error * dt;
            D = -Td * (PV - lastPV) / dt;

            CO = Kp * (P + I + D);
            lastPV = PV;

            return CO;
        }
    }
}

В качестве значений по умолчанию, выбраны Kp = 0.01, Ti = 10000, Td = 0.5 — при таких значениях корабль поворачивается достаточно быстро и обладает некоторым запасом устойчивости.


Помимо такой формы ПИД-регулятора, часто используется т.н. реккурентная форма:


$ CO(t_k)=CO(t_{k-1})+K_p\left[\left(1+\dfrac{\Delta t}{T_i}+\dfrac{T_d}{\Delta t}\right)e(t_k)+\left(-1-\dfrac{2T_d}{\Delta t}\right)e(t_{k-1})+\dfrac{T_d}{\Delta t}e(t_{k-2})\right] $


Не будем на ней останавливаться, т.к. она актуальна прежде всего для хардверных программистов, работающих с FPGA и микроконтроллерами, где такая реализация значительно удобнее и эффективнее. В нашем же случае — давайте что-нибудь сваям на Unity3D — это просто еще одна реализация ПИД-контроллера, которая ни чем не лучше других и даже менее понятная, так что еще раз дружно порадуемся как хорошо программировать в уютненьком C#, а не в жутком и страшном VHDL, например.


Вместо заключения. Куда бы еще присобачить ПИД-регулятор


Теперь попробуем немного усложнить управление корабля используя двухконтурное управление: один ПИД-регулятор, уже знакомый нам _angleController, отвечает по прежнему за угловое позиционирование, а вот второй — новый, _angularVelocityController — контролирует скорость поворота:


public float ControlRotate(float targetAngle, float thrust)
{
    float CO = 0f;
    float MV = 0f;
    float dt = Time.fixedDeltaTime;

    _angle = _myTransform.eulerAngles.z;

    //Контроллер угла поворота
    float angleError = Mathf.DeltaAngle(_angle, targetAngle);
    float torqueCorrectionForAngle = 
    _angleController.Update(angleError, _angle, dt);

    //Контроллер стабилизации скорости
    float angularVelocityError = -_rb2d.angularVelocity;
    float torqueCorrectionForAngularVelocity = 
        _angularVelocityController.Update(angularVelocityError, -angularVelocityError, dt);

    //Суммарный выход контроллера
    CO = torqueCorrectionForAngle + torqueCorrectionForAngularVelocity;

    //Дискретизируем с шагом 100            
    CO = Mathf.Round(100f * CO) / 100f;

    //Сатурируем
    MV = CO;
    if (CO > thrust) MV = thrust;
    if (CO < -thrust) MV = -thrust;

    return MV;
}

Назначение второго регулятора — гашение избыточных угловых скоростей, за счет изменения крутящего момента — это сродни наличию углового трения, которое мы отключили еще при создании игрового объекта. Такая схема управления [возможно] позволит получить более стабильное поведение корабля, и даже обойтись только пропорциональными коэффициентами управления — второй регулятор будет гасить все колебания, выполняя функцию, аналогичную дифференциальной составляющей первого регулятора.


Помимо этого, добавим новый класс ввода игрока — PlayerInputCorvette, в котором повороты буду осуществляться уже за счет нажатия клавиш "вправо-влево", а целеуказание с помощью мыши мы оставим для чего-нибудь более полезного, например, для управления турелью. Заодно у нас теперь появился такой параметр как _turnRate — отвечающий за скорость/отзывчивость поворота (не понятно только куда его поместить лучше в InputCOntroller или все же Ship).


public class PlayerCorvetteInput : BaseInputController
{
     public float _turnSpeed = 90f;

     public override void ControlRotate()
     {
         // Находим указатель мыши
         Vector3 worldPos = Input.mousePosition;
         worldPos = Camera.main.ScreenToWorldPoint(worldPos);

         // Сохраняем относительные координаты указателя мыши
         float dx = -this.transform.position.x + worldPos.x;
         float dy = -this.transform.position.y + worldPos.y;

         //Передаем направление указателя мыши
         Vector2 target = new Vector2(dx, dy);
         _agentBody._target = target;

         //Вычисляем поворот в соответствии с нажатием клавиш
         _agentBody._rotation -= Input.GetAxis("Horizontal") * _turnSpeed * Time.deltaTime;
    }

    public override void ControlForce()
    {            
         //Передаем movement
         _agentBody._movement = Input.GetAxis("Vertical") * Vector2.up;
    }
}

Также для наглядности накидаем на коленках скрипт для отображения отладочной информации
namespace Assets.Scripts.SpaceShooter.UI
{
    [RequireComponent(typeof(Ship))]
    [RequireComponent(typeof(BaseInputController))]
    public class Debugger : MonoBehaviour
    {
        Ship _ship;
        BaseInputController _controller;
        List<SimplePID> _pids = new List<SimplePID>();
        List<string> _names = new List<string>();

        Vector2 _orientation = new Vector2();

        // Use this for initialization
        void Start()
        {
            _ship = GetComponent<Ship>();
            _controller = GetComponent<BaseInputController>();

            _pids.Add(_ship._angleController);
            _names.Add("Angle controller");

            _pids.Add(_ship._angularVelocityController);
            _names.Add("Angular velocity controller");

        }

        // Update is called once per frame
        void Update()
        {
            DrawDebug();
        }

        Vector3 GetDiretion(eSpriteRotation spriteRotation)
        {
            switch (_controller._spriteOrientation)
            {
                case eSpriteRotation.Rigth:
                    return transform.right;
                case eSpriteRotation.Up:
                    return transform.up;
                case eSpriteRotation.Left:
                    return -transform.right;
                case eSpriteRotation.Down:
                    return -transform.up;
            }
            return Vector3.zero;
        }

        void DrawDebug()
        {
            //Направление поворота
            Vector3 vectorToTarget = transform.position 
                + 5f * new Vector3(-Mathf.Sin(_ship._targetAngle * Mathf.Deg2Rad), 
                    Mathf.Cos(_ship._targetAngle * Mathf.Deg2Rad), 0f);

            // Текущее направление
            Vector3 heading = transform.position + 4f * GetDiretion(_controller._spriteOrientation);

            //Угловое ускорение
            Vector3 torque = heading - transform.right * _ship._Torque;

            Debug.DrawLine(transform.position, vectorToTarget, Color.white);
            Debug.DrawLine(transform.position, heading, Color.green);
            Debug.DrawLine(heading, torque, Color.red);
        }

        void OnGUI()
        {
            float x0 = 10;
            float y0 = 100;

            float dx = 200;
            float dy = 40;

            float SliderKpMax = 1;
            float SliderKpMin = 0;
            float SliderKiMax = .5f;
            float SliderKiMin = -.5f;
            float SliderKdMax = .5f;
            float SliderKdMin = 0;

            int i = 0;
            foreach (SimplePID pid in _pids)
            {
                y0 += 2 * dy;

                GUI.Box(new Rect(25 + x0, 5 + y0, dx, dy), "");

                pid.Kp = GUI.HorizontalSlider(new Rect(25 + x0, 5 + y0, 200, 10), 
                    pid.Kp, 
                    SliderKpMin, 
                    SliderKpMax);
                pid.Ki = GUI.HorizontalSlider(new Rect(25 + x0, 20 + y0, 200, 10), 
                    pid.Ki, 
                    SliderKiMin, 
                    SliderKiMax);
                pid.Kd = GUI.HorizontalSlider(new Rect(25 + x0, 35 + y0, 200, 10), 
                    pid.Kd, 
                    SliderKdMin, 
                    SliderKdMax);

                GUIStyle style1 = new GUIStyle();
                style1.alignment = TextAnchor.MiddleRight;
                style1.fontStyle = FontStyle.Bold;
                style1.normal.textColor = Color.yellow;
                style1.fontSize = 9;

                GUI.Label(new Rect(0 + x0, 5 + y0, 20, 10), "Kp", style1);
                GUI.Label(new Rect(0 + x0, 20 + y0, 20, 10), "Ki", style1);
                GUI.Label(new Rect(0 + x0, 35 + y0, 20, 10), "Kd", style1);

                GUIStyle style2 = new GUIStyle();
                style2.alignment = TextAnchor.MiddleLeft;
                style2.fontStyle = FontStyle.Bold;
                style2.normal.textColor = Color.yellow;
                style2.fontSize = 9;

                GUI.TextField(new Rect(235 + x0, 5 + y0, 60, 10), pid.Kp.ToString(), style2);
                GUI.TextField(new Rect(235 + x0, 20 + y0, 60, 10), pid.Ki.ToString(), style2);
                GUI.TextField(new Rect(235 + x0, 35 + y0, 60, 10), pid.Kd.ToString(), style2);

                GUI.Label(new Rect(0 + x0, -8 + y0, 200, 10), _names[i++], style2);
            }
        }
    }
}

Класс Ship также претерпел необратимые мутации и теперь должен выглядеть вот так:
namespace Assets.Scripts.SpaceShooter.Bodies
{
    public class Ship : BaseBody
    {
        public GameObject _flame;

        public Vector2 _movement = new Vector2();
        public Vector2 _target = new Vector2();

        public float _targetAngle = 0f;
        public float _angle = 0f;

        public float _thrust = 1f;

        [Header("PID")]
        public SimplePID _angleController = new SimplePID(0.1f,0f,0.05f);
        public SimplePID _angularVelocityController = new SimplePID(0f,0f,0f);

        private float _torque = 0f;
        public float _Torque
        {
            get
            {
                return _torque;
            }
        }

        private Vector2 _force = new Vector2();
        public Vector2 _Force
        {
            get
            {
                return _force;
            }
        }

        public void FixedUpdate()
        {
            _torque = ControlRotate(_targetAngle, _thrust);
            _force = ControlForce(_movement, _thrust);

            _rb2d.AddTorque(_torque);
            _rb2d.AddRelativeForce(_force);
        }

        public float ControlRotate(float targetAngle, float thrust)
        {
            float CO = 0f;
            float MV = 0f;
            float dt = Time.fixedDeltaTime;

            _angle = _myTransform.eulerAngles.z;

            //Контроллер угла поворота
            float angleError = Mathf.DeltaAngle(_angle, targetAngle);
            float torqueCorrectionForAngle = 
                _angleController.Update(angleError, _angle, dt);

            //Контроллер стабилизации скорости
            float angularVelocityError = -_rb2d.angularVelocity;
            float torqueCorrectionForAngularVelocity = 
                _angularVelocityController.Update(angularVelocityError, -angularVelocityError, dt);

            //Суммарный выход контроллера
            CO = torqueCorrectionForAngle + torqueCorrectionForAngularVelocity;

            //Дискретизируем с шагом 100            
            CO = Mathf.Round(100f * CO) / 100f;

            //Сатурируем
            MV = CO;
            if (CO > thrust) MV = thrust;
            if (CO < -thrust) MV = -thrust;

            return MV;
        }

        public Vector2 ControlForce(Vector2 movement, float thrust)
        {
            Vector2 MV = new Vector2();

            if (movement != Vector2.zero)
            {
                if (_flame != null)
                {
                    _flame.SetActive(true);
                }
            }
            else
            {
                if (_flame != null)
                {
                    _flame.SetActive(false);
                }
            }

            MV = movement * thrust;

            return MV;
        }

        public void Update()
        {

        }        
    }
}

А вот, собственно заключительное видео того, что должно получиться:



К сожалению получилось охватить не все, что хотелось бы, в частности почти не затронут вопрос настройки ПИД-регулятора и практически не освящена интегральная составляющая — фактически приведен пример только для ПД-регулятора. Собственно изначально планировалось несколько больше примеров (круиз-контроль, вращение турели и компенсация вращательного момента), но статья итак уже разбухла, да и вообще:
image


Немного ссылок


  1. Годная статья на английской вики
  2. PID tutorial
  3. ПИД-регуляторы: вопросы реализации. Часть 1
  4. ПИД-регуляторы: вопросы реализации. Часть 2
  5. PID Without a PhD
  6. PID Without a PhD. Перевод
  7. Derivative Action and PID Control
  8. Control System Lab: PID
  9. ПИД-регулятор своими руками
  10. Корректная реализация разностной схемы ПИД регулятора
  11. Программируем квадрокоптер на Arduino (часть 1)
  12. Виртуальный квадрокоптер на Unity + OpenCV (Часть 1)
  13. Поляков К.Ю. Теория автоматического управления для чайников
  14. PID control system analysis, design, and technology
  15. Aidan O'Dwyer. Handbook of PI and PID Controller Tuning Rules (3rd ed.)
  16. PID process control, a “Cruise Control” example
  17. https://www.mathworks.com/discovery/pid-control.html
  18. http://scilab.ninja/study-modules/scilab-control-engineering-basics/module-4-pid-control/
  19. https://sourceforge.net/p/octave/control/ci/default/tree/inst/optiPID.m

Еще немного ссылок на другие примеры
http://luminaryapps.com/blog/use-a-pid-loop-to-control-unity-game-objects/
http://www.habrador.com/tutorials/pid-controller/3-stabilize-quadcopter/
https://www.gamedev.net/articles/programming/math-and-physics/pid-control-of-physics-bodies-r3885/
https://ksp-kos.github.io/KOS/tutorials/pidloops.html

Комментарии (39)


  1. SmirnoffA
    09.01.2018 09:23

    Те гиперкомплексные числа называются кватерНионами.


    1. CrazyFizik Автор
      09.01.2018 09:31

      Мда. Исправил


  1. Arastas
    09.01.2018 13:57

    Туториалы о ТАУ, тем более в gamedev, это всегда здорово! Спасибо за статью. Я только пять копеек про некоторые неточности о ПИД добавлю, вдруг пригодится.


    Недостаток пропорционального закона управления заключается в том, что регулятор никогда не стабилизируется в заданном значении

    Это не так. Можно говорить, что в механических системах и при наличии трения это похоже на правду, но не в общем случае.


    увеличение коэффициента пропорциональности всегда приводит к автоколебаниям

    То же самое — не всегда, а для некоторых типов систем.


    интегральное звено всегда вносит некоторое смещение и если система подвержена некоторыми постоянным ошибкам, то оно их компенсирует (за счет своего смещения). А вот если же этих ошибок нет или они пренебрежительно малы, то эффект будет обратным — интегральная составляющая сама будет вносить ошибку смещения.

    Вот это вообще на столько не так, что я даже теряюсь, откуда это. Никакой ошибки смещения нормально сделанная интегральная составляющая вносить не будет (разве что в каком-то очень искусственном примере, но я не смог с ходу такого придумать).


    Именно по этой причине её не используют, например, в задачах сверхточного позиционирования.

    Это всё ещё про интегральную составляющую, и это абсолютно не так. Используют, ещё как используют. Скорее от D откажутся, чет от I.


    Ключевым недостатком интегрального закона управления является эффект насыщения интегратора (Integrator windup)

    Справделивости ради, для этого недостатка есть ряд достаточно простых способов обхода, anti-windup.


    На самом деле есть множество других костылей, <...> или же поставить фильтры нижних частот между SP(t) и операцией SP(t)?PV(t) за счет которого будет происходить плавное нарастание ошибки

    Вот это (поставить задатчик траетории) как раз самое правильное решение.


    а можно совсем развернуться и впендюрить самый настоящий фильтр Калмана для сглаживания входных данных <...> В общем костылей много, и добавить наблюдателя конечно хотелось бы

    Фильтр Калмана и наблюдатели в этой задаче ни при чём, они используются для оценивания недоступных измерению состояний объекта.


    Собственно вся ТАУ и крутится вокруг частотных характеристик процессов

    Да уже почти 50 лет как нет. :) Годов с 70-х прошлого века, наверное.


    Но это не критика, а некоторые нюансы. :) Спасибо за статью!


    1. CrazyFizik Автор
      10.01.2018 01:43

      Это не так. Можно говорить, что в механических системах и при наличии трения это похоже на правду, но не в общем случае.

      Вот как раз здесь пример именно, что без трения — можете собрать такой же проект на Unity3D и сидеть ждать, когда положение корабля стабилизируется после поворота — если использовать только один П-регулятор, то это случится… никогда, ну я по крайней мере не дожадлся :-), да и в Octave тоже самое получается.

      А вот при наличии углового трения, уже да, колебания корабля во время поворотов станут затухающими и он сможет стабилизироваться в заданном положении, через какое-то время, но это будет равносильно тому, что для случая без трения мы добавили еще один контур П-регулирования по скорости (как в самом конце статьи).
      А вот если мы выкинем из модели всю динамику вращательного и поступательного движения, т.е., например, будем напрямую и мгновенно менять положение, то тогда да, тогда с П-законом вообще никаких проблем не будет. Но это уже будет другая динамическая система и совсем другая модель. Вот только тут не понятно что общее, а что частное: мое мнение такое, что в принципе все физические процессы инертны, да еще такие неприятные моменты, как задержки в контуре управления и т.д., т.е П-составляющая всегда будет вносить некоторую колебательность, но для некоторых частных случаев все будет хорошо.

      Вот это вообще на столько не так, что я даже теряюсь, откуда это. Никакой ошибки смещения нормально сделанная интегральная составляющая вносить не будет (разве что в каком-то очень искусственном примере, но я не смог с ходу такого придумать).

      Что значит «нормально сделанная»?
      Это же фундаментальная особенность именно операции «интегрирования»: на всем интервале наблюдения (интегрирования) все значения [ошибок в нашем случае] будут просуммированы и накоплены.
      Можно посмотреть на это по другому — звено интегратора найдет площадь криволинейной трапеции, описываемой функции ошибки, и эта площадь на всем интервале наблюдения почти всегда будет отличная от нуля.
      Это разве что ли прикрутить какой-нибудь костыль, например, сбрасывать аккумулятор через определенные интервалы времени или еще чего.

      Просто в реальной жизни астатичные системы встречаются редко и обычно имеют дело со статичными САУ, у которых всегда имеется некоторая установившееся ошибка после переходного процесса. И вот интегральное звено эту ошибку компенсирует и повысит степень астатизма САУ. Осталось только придумать такую динамическую систему, которую можно было бы (и имел бы смысл) использовать в игре.

      Что касается примеров того, где интегральная составляющая вносит смещение, то пожалуйста: собственно сабж — сделайте Ki, отличную от нуля и всегда будете получать некоторую ошибку, вот прям здесь это хорошо видно на примере регулятора угловых скоростей youtu.be/oVL0tvluUe8?t=48 но и с регулятором угла будет такая же ситуация. А уж в совсем запущенных случаях (при большом коэффициенте) интегратор так вообще раскачает корабль и выведет его из равновесия.

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

      Так что тут остается только вносить в программную модельку, блин я даже так с ходу хз, например, какую-то постоянную ошибку при определении скоростей или позиции, шумы там какие-нибудь, то тогда да, тогда интегральная составляющая будет работать. Вопрос только — а надо ли это? Я вот хз, например, в том же KSP я не замечал что они там моделируют какие-то ошибки управления и шумы и т.д.

      В общем все зависит исключительно от того какую динамическую систему мы пытаемся регулировать. Вот можно было бы сейчас повыдумывать разные динамические системы из игр, впихнуть их в Unity3D и расширить статью, вот в тех примерах, которые я приводил — рулят исключительно П- и ПД-регуляторы (что в случае наличия трения, что без оного).

      Может если сюда заглянет какой-нибудь разработчик и расскажет где какие у него проблемы или что он использует. Просто те же повороты в Unity3D можно реализовать без этих всяких регуляторов: заморозили оси вращения и и используем Qunaternion.Lerp и все (пример я тоже тут привел), еще и контроллировать параметры игровых объектов будет проще. А вот, например, если управление осуществляется честно только за счет физики (т.е. прикладывая разные силы к объектам), тут вот у народа возникают вопросы, как реализовать повороты и круиз-контроль (вот хотел его тоже добавить, но сил не хватило, может потом расширю статью), еще и при разных параметрах физики (с трением/без трения, со всякими материалами и т.д.).

      Какие еще примеры можно выдумать?

      Вот в случае… эээ… наведения ракеты на маневрирующую цель с переменным ускорением, вот там как раз можно впихнуть интегратор и будет работать точнее и надежнее, но это точно уже другая статья и отдельная тема по алгоритмам навигации (а их несколько), по которой кстати тоже вопросы «как это сделать в Unity3D» постоянно задаются forum.unity.com/threads/augmented-proportional-navigation.392427 и на них не особенно охотно отвечают, зато продают ассеты :-)

      Фильтр Калмана и наблюдатели в этой задаче ни при чём, они используются для оценивания недоступных измерению состояний объекта.

      Ну, например, я вот не особо в курсе, что за динамическая система типа Ship у меня в итоге получилась: если массу, предположим, я задаю ручками, то вот какой момент инерции получается — я хз, потому что твердое тело сложной формы (и Unity3D это все же учитывает). А знал бы какой момент инерции, то смог бы тогда синтезировать оптимальный регулятор контроля поворота или еще чего. Тем более еще под большим вопросом, что твориться внутри физического движка Unity3D, тут вот на Хабре есть целая статья, где народ выяснял как там работает трение (с RigidBody2D проще, т.к. Unity3D использует Box2D для таких тел и в его исходном коде можно порыться, но опять таки большое хз, чего там своего авторы Unity3D могли наворотить, а их мануал вообще нихрена не раскрывает того, что твориться под капотом).

      В общем, тут бы по-хорошему еще бы пройтись по идентификации динамической системы в Unity3D.

      Да уже почти 50 лет как нет. :) Годов с 70-х прошлого века, наверное.

      Я хз, как крутили-вертели Фурье, Лапласом, Гильбертом и Z-преобразованием, так и крутят-вертят дальше.


      1. Arastas
        10.01.2018 17:02

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


        Сразу же дисклеймер — я очень поверхностно знаком с программированием вообще и совсем не знаком с геймдевом (я долгое время думал, что раз unity это такой gui для Ubuntu, то при чём тут геймдев?). Когда вы мне пишете про кораблик в unity, то я мало что в этом понимаю, я не знаю как это устроено, какую физику и как они моделируют, так что примеры из octave для меня куда понятнее. Но так оказалось, что я несколько разбираюсь в ТАУ.


        Теперь по вопросам.


        Вот как раз здесь пример именно, что без трения — можете собрать такой же проект на Unity3D и сидеть ждать, когда положение корабля стабилизируется после поворота — если использовать только один П-регулятор, то это случится… никогда, ну я по крайней мере не дожадлся :-), да и в Octave тоже самое получается.

        Я предполагаю, что в unity и в octave вы моделируете систему как задание момента, который делится на момент инерции, интегрируется, получается скорость, которая потом интегрируется и получается угол, так? То есть мы говорим про двойной интегратор. Действительно, в этом случае пропорциональный по ошибке угла регулятор приведёт к автоколебаниям. А если бы у вас был какой-то сервопривод, который отрабатывает заданную ему скорость, то П регулятор по ошибке угла обеспечивал бы сходимость этого угла к заданной величине. А если бы было вязкое трение, то тоже была бы сходимость. А если бы вы управляли не углом, а скоростью, то опять П-регулятор давал бы сходимость. А если бы при этом у вас было сухое трение, то была бы (прикидываю в уме) статическая ошибка. Видите сколько вариантов? Потому я и написал, что общие и категоричные утверждения


        <пропорциональный> регулятор никогда не стабилизируется в заданном значении
        т.е П-составляющая всегда будет вносить некоторую колебательность

        неверны, так как всё сильно зависит от конкретной системы и деталей.


        1. CrazyFizik Автор
          10.01.2018 23:20

          Да, так оно и есть. Естественно при добавления трения динамическая модель измениться — регулятор будет себя вести несколько по другому. Вообще с трением все же проще.

          Но есть другой момент — задержки: допустим мы получаем значение pv всегда с какой-то задержкой, да и период опросов тоже повлияет


      1. Arastas
        10.01.2018 17:03

        Второй вопрос.


        Это же фундаментальная особенность именно операции «интегрирования»: на всем интервале наблюдения (интегрирования) все значения [ошибок в нашем случае] будут просуммированы и накоплены. Можно посмотреть на это по другому — звено интегратора найдет площадь криволинейной трапеции, описываемой функции ошибки, и эта площадь на всем интервале наблюдения почти всегда будет отличная от нуля.

        Вы правы, что интегратор интегрирует. Но как ошибка может быть положительной или отрицательной, так и её интеграл может быть равен нулю.


        А ещё мне кажется, что вы смешиваете смещение и колебания. Да, если И составляющая применена не так и не там, где она нужна, то замкнутая система может стать колебательной или неустойчивой. Но ни к какому (постоянному) смещению она приводить не должна. Упрощённо — если есть постоянное смещение (ошибка), то оно накапливается на интеграторе, что приводит к росту управления, что приводит к движению в сторону уменьшения ошибки. Как правило, добавление И составляющей к регулятору в линейных системах приводит к повышению порядка астатизма на единицу. Два интегратора последовательно добавили — повысили порядок астстизма на два. И так далее. Откуда возмётся смещение?


        Что касается примеров того, где интегральная составляющая вносит смещение, то пожалуйста: собственно сабж — сделайте Ki, отличную от нуля и всегда будете получать некоторую ошибку, вот прям здесь это хорошо видно на примере регулятора угловых скоростей

        По указанным выше причинам я не могу воспроизвести этот пример, а видео мне мало о чём говорит. Но если у вас добавление И составляющей приводит к появлению постоянного смещения, а система при этом остается устойчивой, то это очень странно. Попробуйте повторить в octave, было бы интересно разобраться.


        Просто в реальной жизни астатичные системы встречаются редко и обычно имеют дело со статичными САУ, у которых всегда имеется некоторая установившееся ошибка после переходного процесса.

        Я бы более сказал, что встречаются и такие системы, и этакие. А каких больше — специфика предметной области.


        1. CrazyFizik Автор
          11.01.2018 03:05

          Ну тут получается двойно пример на самом деле.
          Сначала в статье описывается регулятор углового положения «кагбэ в космосе» — там отлично работает ПД-регулятор и никакого шума в систему управления тут не вносится (вообще я думал по этому месту тоже пройтись, но меня закосячило, тем более это не совсем оправданное усложнение).

          А И-звено там реально бесполезное. Если его внести, то будет такая ситуация: как только корабль приблизится к нужному углу — у него все равно будет присутствовать некоторая ошибка, которая будет асимптотически стремится к 0, ну т.е. уменьшаться то она будет, но только очень-очень медленно, совсем медленно. Возможно когда-нибудь, где-нибудь в бесконечно, ошибка и установится в 0.
          Можно поиграться с И-составляющей и добиться таки достаточно заметного смещения, главное не поусердствовать — при слишком большой И — корабль просто разбалтывает нафиг.

          Аналогичная ситуация будет с круиз-контролем на этом же пимере (сюда я его не впилил, хотя наверное стоит), но там еще веселее — вполне себе хватает П-составляющей.

          Ну а конце система управления прирастает еще одним ПИД-регулятором, который всегда компенсирует скорость поворота, т.е. его задача выдавать момент сил, противоположенный вращению и вот если там подкрутить И-составляющую, то при определенном угле смещении оба регулятора будут выдавать одинаковые по модулю, но противоположенные по знакам угловые моменты, меняя И-составляющую у второго регулятора можем добиться разных смещения :-)

          В Octave кстати тоже самое получается.

          Вообще все от типа динамической системы зависит — надо что ли это как-то дополнить, я хз. Хорошо было бы придумать пример именно с ПИ-регулятором: зачем добавлять Д к П — хороший пример прям на поверхности, второй пример где только П-звено я не привел (а надо бы наверное, хз только куда его вставлять — перед поворотами или после? Ну и он не совсем честный — работает так, как буд-то бы у корабля движки находятся с 4-х сторон, соосно с центром масс, так шо я хз — стоит ли).
          А вот чего не хватает, так это примера с ПИ — регулятором (был бы он, можно было бы сразу все впендюрить), но я чойт так с ходу и не придумал: если контроль поворота с помощью ПД и вектора скорости с помощью П(Д) — поверхности, то куда воткнуть именно И-регулирование (шоб сразу было очевидно его преимущество) я даже хз.

          И еще раз о системах. Вообще особенность всех наших алгоритмов автоматического управления в том, что они отлично работают… на Земле, когда у нас полно всяких нелинейностей, диссипативных сил и прочих неприятных вещей. Скажем так, аэродинамическое трение таки очень помогает при управлении летальными аппаратами, а попадаешь в среду где диссипативных сил нет, зато есть здоровенные консервативные силы и поведение объектов заметно изменяется. Поэтому, если пройдетесь по космическим аркадкам, то обнаружите, что они в большинстве своем используют физику «а-ля автомобиль» или как буд-то объект движется внутри идеальной жидкости/газе (тоже кстати забавный прием получается), шо собственно и меня подтолкнуло к написанию этой статьи: отличный сферовакуумный пример, на котором можно уже дальше самому поэкспериментировать.


          1. Arastas
            11.01.2018 11:49

            А И-звено там реально бесполезное. Если его внести, то будет такая ситуация: как только корабль приблизится к нужному углу — у него все равно будет присутствовать некоторая ошибка, которая будет асимптотически стремится к 0, ну т.е. уменьшаться то она будет, но только очень-очень медленно, совсем медленно. Возможно когда-нибудь, где-нибудь в бесконечно, ошибка и установится в 0.

            То есть если ошибка асимптотически сходится к нулю, но недостаточно быстро, то вы это называете смещением? :) Ну это как бы не совсем корректно. Когда говорят о постоянном смещении, то, как правило, имеется ввиду, что даже в асимптотике это смещение остаётся.


            А вот чего не хватает, так это примера с ПИ — регулятором (был бы он, можно было бы сразу все впендюрить), но я чойт так с ходу и не придумал

            Пусть корабль поворачивается (следит) за точкой, которая вращается с постоянным ускорением. Если я правильно понял используемую в вашей системе модель, то ПД регулятор даст постоянную ошибку слежения, а добавление И составляющей её уберёт. Если делать не настоящую Д составляющую по ошибке, а только по скорости, то такой же эффект будет уже при слежении за целью, которая движется с постоянной скоростью.


            1. CrazyFizik Автор
              11.01.2018 13:46

              Вот кстати как раз на это у меня заготовлены алгоритмы навигации (например, самонаведении ракет), только это уже будет отдельная статья, хз когда доберусь.


      1. Arastas
        10.01.2018 17:06

        Третья часть


        В общем, тут бы по-хорошему еще бы пройтись по идентификации динамической системы в Unity3D.

        Кончено, (расширенный) фильтр Калмана можно использовать для идентификации параметров модели. Но у вас, как я понимаю, всё это крутится в виртуальной песочнице, никаких шумов измерений. Зачем тут стохастический по наутер аппарат. Опередлить параметры системы будет не сложно гораздо более простыми детерминированными методами.


        Я хз, как крутили-вертели Фурье, Лапласом, Гильбертом и Z-преобразованием, так и крутят-вертят дальше.

        Ну знаете… Крутят, конечно. Вот в програмировании есть алгоритмы сортировки. Их все изучают, постоянно используют, и даже кто-то, вроде, работает над их исследованием. Но мы же не говорим, что всё программирование крутится вокруг сортировки? Так же и в ТАУ. Частотные методы изучают и используют, но они давно уже не составляют центра области.


        1. CrazyFizik Автор
          10.01.2018 23:50

          Ну смотрите. Шум в играх могу добавлять искусственно — например, та же стрельба в играх. Весь вопрос нужно это как элемент геймплея или нет.

          Другой пример уже реальный — мультиплеер. Вот там будут и большие задержки (пинг 100 мс — нормальное явления) и будут потерянные пакеты — некоторая информация будет теряться. Собственно каждый игрок играет в мультиплеере играет в свою игру, а уж потом их игры синхронизируются. Вот это уже реальная проблема, особенно для физики.

          Вот есть такая игра — Space Engeneers, хз как сейчас, но когда я в неё играл (3-4 года назад), при игре по мультиплееру физика в игре начинала сходить с сума: решил пройтись по кораблю во время движения и оказался в стене, например :-) Вот там бы точно не помешал бы прикрученный к каждому игроку наблюдатель (хз, может они его и прикручивали — я хз). Вот в мультиплеере есть де разгуляться — там и задача оценивания при неполной информации, и предсказания.


  1. Gryphon88
    09.01.2018 17:25

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


    1. CrazyFizik Автор
      10.01.2018 03:24

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


    1. CrazyFizik Автор
      10.01.2018 03:33

      Собственно да, самый простой пример использования нейросетей — выбор поведения, действительно легко вставляется и работает. Собственно гейм-дизайнеру остается только подбить таблицу где перечислены варианты желаемого поведения в разных ситуациях (одна проблема, таблица должна быть как можно больше, причем, чем больше нейронов в первом слое, тем больше нужно скормить примеров), а дальше нейросеть сама все выучит и сможет находить решения уже в других ситуациях, причем это достигается без всяких if'ов.

      Вот только это надо бы до логического конца довести, когда будет время — хз.


  1. aslepov78
    09.01.2018 17:27

    Ожидал прямоходячих ботов, аля Boston Dynamics, а тут ракета (


  1. Kinddog
    09.01.2018 22:45

    Статья интересная, но явно не для самых маленьких :)
    Ждем продолжения.
    Интересно было бы услышать про наиболее эффективные алгоритмы автоматической настройки ПИД-регулятора.


    1. CrazyFizik Автор
      10.01.2018 02:57

      На самом деле тема «оптимальный синтез ПИД-регуляторов» еще открыта и я думаю там люди до сих пор диссертации защищают. Собственно ключевое преимущество ПИД-регуляторов, в том что они легко реализуемы (на самом деле ПИД-регулятор в идеальной форме в общем-то нереализуем, но народ итак живет и не жалуется) и их можно быстро настроить ручками, отсюда такое их повальное распространение. Собственно имея более-менее корректно реализованный ПИД-регулятор его можно быстренько на глазок настроить не углубляясь в математику — это большой плюс. Тут правда наверное следуют все же запилить какой-то тулкит для редактора Unity3D, шоб можно было результаты своих настроек посмотреть на графиках.

      А так подходов несколько:
      — характеристические методы по данным полученным в результате испытания в разомкнутом контуре, собственно тот же метод Циглера-Никольса
      — аналитический синтез, формулы там несложные, вот только нужная точная модель объекта управления
      — синтез на основе частотных характеристик, и опять таки, эти частотные характеристики САУ надо откуда-то получить
      — и собственно оптимальный численный синтез, но и там нужно как-то идентифицировать наш объект управления.

      В общем виде придется делать так:
      1. Идентифицировать систему (допустим наш корабль), ну там хз, подавать ступенчатые воздействия или еще какие более хитрые, прям в самой игре. А на их основе построить модель: ПИД-регуляторы могут работать с моделями первого и второго рода, их форма известно, нужно только параметры как-то подобрать.
      2. Ну а потом уже имея модельку, мы можем численно оптимизировать параметры ПИД-регулятора отдельно от игрового цикла Юнити.
      Вот только тут тоже есть проблемка — критериев качества много: максимизировать скорость нарастания переходного процесса, минимизировать ошибку перерегулирования и автоколебания, минимизировать длительность переходного процесса. В общем куча критериев, которые еще друг другу противоречат — увеличиваем реакцию системы (например, скорость поворота) и появляются колебания, уменьшаем колебаний — увеличивается время реакции, ну и т.д.
      Т.е. этих более менее оптимальных ПИД-регуляторов только на один объект управления будет не один, а дохрена и все простые и классические методы оптимизации идут лесом, тут нужно уже что-то посложнее. Хотя в принципе можно обойтись и оптимизации по критерию стабильности САУ.

      Всякие хитры методы автонастройки/оптимизации ПИД-регуляторов — патентуют все кому не лень и хотят за это много денег.

      В общем тут надо еще подумать, как бы получше и попроще запилить тулкит именно для ПИД-регуляторов игровых объектов в Unity3D. Либо это действительно будет метод Циглера-Никольса где как раз и используется стандартная форма ПИД-регулятора, которую я выше привел — правда в примере с космическим кораблем он не заработает — он всегда устойчиво колебается, когда нет трения (а вот когда есть — таки заработает), так что не получится найти критическое значение Kp и соответствующий ему период колебаний Tu. Либо же это будет какой-то сугубо численный метод оптимизации, шоб был простым и эффективным. Это вот предпочтительнее ИМХО, но надо думать.


      1. Kinddog
        10.01.2018 11:50

        Спасибо за развернутый ответ и ссылки.
        Мне, увы, не удалось реализовать более менее стабильно функционирующий (на реальном объекте) алгоритм автонастройки ПИД-регулятора :(


        1. CrazyFizik Автор
          10.01.2018 14:16

          На самом деле я вообще сомневаюсь, что с ПИД-регуляторами можно что-то реально стабильное замутить — там уже по его общей форме передаточной характеристики видно что он нифига не стабилен. Все же оптимальные регуляторы это что-то другое

          Но я когда-то давно использовал NSGA для настройки ПИД-регулятора: на выходе получал кучу вариантов настройки с учетом всех критериев качества. Вроде даже работало, вот только у меня была достаточно точная математическая модель динамической системы, поэтому я могу гонять NSGA в онлайне и уже потом применял полученные результаты на готовом устройстве.

          А вот чтоб прям в онлайне он автотюнинлся… не ну есть такое, но это все под патентами и там куча яйцеголовых, в том же Сименсе над этим трудится.


          1. Kinddog
            10.01.2018 17:10

            Мы использовали тестовый прямоугольный импульс по управляющему каналу, далее аппроксимация получившейся переходной характеристики объекта и определение параметров регулятора. К сожалению, в большинстве случаев качество регулирования только ухудшалось. :(
            В результате был сделан вывод о том, что будущее САУ лежит в области нечеткой логики и нейросетей. :)


            1. Arastas
              10.01.2018 17:46

              Подавляющее большинство специалистов в ТАУ относятся к этому скептически. Смотрите, вы даже три параметра не смогли автоматичеси настроить. Почему же несколько сотен сможете? :)
              Хотя, конечно, место для нечеткой логики и нейронных сетей есть, свою нишу они займут (уже занимают), и ниша эта будет немаленькой.


              1. Kinddog
                11.01.2018 11:45

                К сожалению, имхо, теория автоматического управления на практике зачастую оказывается именно теорией :)

                Один мой хороший товарищ инспектирует системы АСУТП на ТЭЦ и, с его слов, ничего сложнее ПИ-регуляторов там не используют, а зачастую регулирование проходит в ручном режиме а-ля «Васильич, бегом до задвижки #### и поверни ее на пол-оборота».
                Так было по-крайней мере лет пять назад, но не думаю, что с тех пор что-то кардинально поменялось.
                Ключевой момент в том, что полным пониманием того, почему «задвижка #### » и «пол-оборота» обладает сотрудник, который давно там работает и научился понимать, КАК нужно воздействовать на объект регулирования в зависимости от цели регулирования.

                Так вот, имхо, основная задача будущих САУ будет заключаться в том, чтобы тем или иным образом (с использованием нейросетей или еще чего-то) формировать т.н. экспертную систему, которая в процессе наблюдения за процессом управления и объектом регулирования сможет сформировать базу правил, с помощью которой, как минимум, можно будет повторить действия условного «Васильича». Безусловно, такая система должна обладать возможностью дообучения/самообучения.

                В принципе, такой подход позволяет настраивать в том числе и ПИД-регуляторы :)))


                1. Arastas
                  11.01.2018 12:00

                  Я понимаю вашу мысль, но должен отметить, что экспертное сообщество с ней, в своей массе, не согласно.
                  А теория АУ остается теорией когда для её реализации приглашают человека, который либо в ней толком не разбирается, либо не имеет ресурсов для нормальной разработки. Даже не знаю, что чаще, "Вась! У тебя же лет 10 назад был один семестр ТАУ? Забацай тут автоматизацию, ты же всё равно программист!" или "Что значит нужно время на эксперименты и новые датчики? Нам вчера надо, пусть оно пока в полуручном режиме работает!"


                1. CrazyFizik Автор
                  11.01.2018 13:54

                  Ну на самом деле с Васей то все правильно ИМХО, так и должно быть. А вот автопилот вертолета дядя Вася уже сделать не сможет.

                  А нейросеть в принципе итак уже может, например, идентифицировать систему. Вообще если капнуть глубже, то, например, цифровые адаптивные фильтры уже являются однослойным линейным персептроном, а они уже давно работают где только можно.
                  Вот только однослойный персептрон может решать только линейно-разделимы задачи. От многослойного персептрона можно ждать большего. Ну т.е. нейросеть сама по себе может быть регулятором — прям торчать в контуре управления также как и этот ПИД-регулятор: получать на вход ошибку и выдавать на выходе сигнал управления, я правда хз используется ли это где, но в цифровой фильтрации нейросети дают лучшие результаты чем обычные адаптивные КИХ-фильтры… только ресурсов дохрена требуется


                  1. Kinddog
                    12.01.2018 00:47

                    Автопилот дяде Васе не надо делать.

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

                    Ну а если к этому еще добавится обобщенная аналитика действий других пилотов (BigData и пр.), то построенная на этой основе САУ вполне годится для управления самыми сложными системами.

                    При таком подходе в плюсе мы имеем универсальность и существенное снижение затрат.


                    1. Arastas
                      12.01.2018 02:10

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


                      1. Kinddog
                        12.01.2018 23:39

                        Спасибо за ответ.
                        Рассматривал эту задачу с точки зрения экономики ;)


            1. Int_13h
              10.01.2018 18:55

              Вот параметры объекта очень даже легко регулятором определяются, по той же широко известной методике Коэна-Куна хотя бы. А вот как потом из этих параметров объекта получить настройки регулятора — большой секрет. Все «инженерные» методы — Циглера, Николса, Коэна и даже Куна — своими формулами дают ужасное качество регулирования по сравнению с ручной настройкой. Можно классический аналитический способ расчета применить — но математики черезчур много становится для простого регулятора.


              1. Arastas
                10.01.2018 20:43

                Только не параметры объекта, а его аппроксимация. Которая может хорошая, а может нет, и для более-менее сложных систем может не подойти. А типовых настроек, в зависимости от выбранной аппроксимации, с десяток видов. Казалось бы, есть из чего выбирать, нет?
                Из личного опыта — когда ПИ настраивался для более-менее простых систем, типа конутра тока тут, то там был аналитиский расчёт, он простой совсем. А для более-менее сложных систем или задач ПИ/ПИД не тянет, там приходилось другие структуры разрабатывать.


                1. Int_13h
                  12.01.2018 03:55

                  Совершенно верно, аппроксимация. Лучше иметь аппроксимированную модель объекта управления, чем никакой не иметь и настраивать регулятор, как дядя Вася.
                  Помню, настраивал немецкий инженер нам регулятор гидроагрегата на ГЭС. Подал ступенчатое воздействие, снял отклик, а потом уже от определенных параметров объекта и исходил в настройке регулятора.


  1. Babayka_od
    10.01.2018 03:24

    И чего нам в универе подобным образом не объясняли :(


  1. valekdgin
    10.01.2018 13:54

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


    1. Arastas
      10.01.2018 14:02
      +2

      А выпускники ВУЗов по специальности ТАУ сидят и не знаю, куда пойти работать по специальности. Сходите на кафедру ТАУ или систем управления ближнего к вам приличного университета и пригласите студентов на практику или стажировку.


      Если вам нужны разовые консультации по ТАУ, то и это решается.


      1. CrazyFizik Автор
        10.01.2018 14:41

        Ну к слову сказать выпускников ТАУ не так уж и много. У меня в универе всего 2 кафедры выпускали таких (а сейчас вообще одна только осталась), причем там только часть специализировалась именно на ТАУ, там реально капля в море.

        А у остальных ТАУ если проходиться, то как-то совсем мимо. Да еще ТАУ требует хорошую математическую подготовку, а её… эээ… не все вузы могут обеспечить. На моей памяти, по крайней мере раньше, всех тауведов расхватывали еще на стадии студенческой скамьи, как щас я правда хз.


        1. Arastas
          10.01.2018 17:49

          Там в профиле Моква стоит. Я думаю, что уж в Москве-то можно студентов найти. :)
          Не знаю как в ваши времена, но вот лет десять назад спрос на спецов в ТАУ был, но не особо высокий, а предлагаемые зп от больших гос или бывших гос предприятий были не особо конкурентноспособны. Многи на лету переквалифицировались в электронщиков или программистов.


          1. CrazyFizik Автор
            11.01.2018 00:27

            Ну в Москве чувак пришедший с Бауманки и умеющий записывать и решать систему линейных уравнений в матричном виде — таки не редкость, с комплексными числами тоже не все дружат. Или, например, как выяснилось, шо не все мифишники в курсе, что масса Лоренц-инвариантна, хотя казалось бы. Это все конечно не страшно на самом деле, но дисперсия по процессу подготовки даже в пределах одного факультета очень большая.

            Я вот, например, когда учился — на нашем отделении решили сделать финт ушами: увеличили число часов по матанализу и включили в него ТФКП, соответственно саму ТФКП тоже ликвидировали, но суммарное число часов не изменилось… только забыли об этом сказать преподу, студентов тоже никто не предупредили — ну нет ТФКП и нет. Стандартный матан таким макаром очень быстро кончился и препод не зная чем себя занять принялся за углубенное изучение интеграла Лебега, интеграла Интеграл Курцвейля-Хенстока, потом когда и это кончилось — принялся за функан и дифгем. Но ТФКП в итоге не было — об этом узнали только на госниках :-)

            А в Москве так вообще очень странная ситуация: конкуренция очень высокая — только не со стороны выпускников, а наоборот — со стороны работодателей и в тоже время работодатели еще и носом воротят.

            В общем как мне всегда казалось специалистов мало, даже очень мало. А сейчас вон ADAS во вс щели полезла (только вот я уже желающими запилить свой автопилот для автомобиля пообщался — с разными конторами… и по-моему они вообще плохо понимают куда ввязались и кто им нужен)


  1. valekdgin
    10.01.2018 14:51

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


  1. dude_sam
    11.01.2018 10:42

    А уже была шутка про главного врага автоматчика? — Технолога!

    На самом деле...
    Инерционность