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

Три основных типа карьерных самосвалов в России

В российской добывающей отрасли применяются три доминирующих класса машин, каждый из которых имеет собственные особенности измерения веса:

Классические карьерные самосвалы с жёсткой рамой, такие как БелАЗ 7555–7513 и CAT 777–793, оснащаются гидропневматической подвеской с датчиками давления. Эти датчики служат для оценки веса полезной нагрузки через измерение давления в подвеске. Однако их данные сильно зависят от темпа движения и амплитуды раскачки кузова, что создает шумы и неточности при динамическом движении. В качестве инженерного дополнения к измерению веса для таких моделей важно применять алгоритмы фильтрации и компенсации динамических колебаний, а также дополнять данные от датчиков давления весовыми сенсорами, установленными на шасси, для повышения точности оценки нагрузки и снижения влияния факторов движения. Для БелАЗ и CAT, реализованы высокоточные системы взвешивания с погрешностью в диапазоне до ±0,1–1%. Однако, из-за сильных вибраций и динамических пиков в процессе работы, количество данных с аномальными колебаниями составляет примерно 20–35%. Благодаря строгой конструкции и стабильной гидравлической и электронной схемам, такие системы обеспечивают относительно меньшую дисперсию и более предсказуемую точность, особенно при использовании фильтров и компенсационных алгоритмов.

Самосвалы с шарнирно-сочлененной рамой, например Volvo A30–A60 и CAT 730–745, используют гидравлическую подвеску и шарнирный узел. Вес измеряется с помощью датчиков давления в гидроцилиндрах подвески, а в некоторых случаях применяются тензодатчики, установленные на раме. Основной проблемой при определении веса таких машин являются резкие пиковые нагрузки в районе сочленения и сильные выбросы сигналов при разгоне и торможении. Колебания сигнала достигают 30–45%, что заметно выше, чем у жестких рам, и обусловлено интенсивными вертикальными и боковыми колебаниями в сочленениях. В этих условиях уровень зашумленности составляет значительные 30–45%, что связано с сильными пиковыми нагрузками и резкими изменениями давления в гидроцилиндрах. Для повышения точности необходимо использовать расширенные алгоритмы фильтрации, такие как калмановские фильтры или адаптивное сглаживание, а также комбинировать данные с различных датчиков.

Дорожные самосвалы модельного ряда КамАЗ, Scania, MAN отличаются пневматической подвеской или рессорной подвеской. Датчики давления устанавливаются в пневмоподушках, имеются также тензодатчики на мостах. Эти модели демонстрируют наибольшие уровни шумов, до 50–70%. Это связано с высокой зависимостью от дорожных условий, неровностей, вибрации и динамики движения. В этих условиях погрешности увеличиваются из-за поисковых воздействий на датчики давления в пневмоподушках и мостовых тензодатчиков, что приводит к большему количеству ошибок в измерениях. Поэтому, для повышения точности измерения веса рекомендуется использовать дополнительные системы калибровки, адаптивных фильтров и учета профиля дорожного покрытия, что позволяет снизить влияние внешних факторов и обеспечить стабильность данных при разных условиях эксплуатации.

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

Применяемые типы датчиков для трёх групп карьерных и дорожных самосвалов

Группа самосвалов

Тип датчика

Место установки

Плюсы

Минусы

Корректность данных (ориентировочно)

Классические карьерные, жёсткая рама

Датчики подвески (гидравлические/гидропневматические)

В подвеске (амортизаторах, цилиндрах подвески)

Простота установки; достаточная точность в статике

Сильная зависимость от темпа движения и вибраций; шум на 50–200% при 25–45 км/ч; влияние раскачки и неравномерности нагрузки

60–80%

Самосвалы с шарнирно-сочлененной рамой

Датчики давления в гидроцилиндрах подвески; тензодатчики рамы

На гидроцилиндрах подвески, на раме

Высокая точность; меньше влияние подвески

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

70–90%

Дорожные самосвалы (пневмо или рессорная подвеска)

Датчики давления в пневмоподушках; тензодатчики мостов

В пневматических подушках и на мостах

Широкое распространение; позволяет отслеживать нагрузку на оси

Запаздывание реакции подушек; высокие нелинейности; сильный шум на кочках; зависимость от температуры и профиля дороги

40–65%

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

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

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

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

Методологический подход к тарировке и расчёту перевозимого веса самосвала

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

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

  • независимость от типа машины, датчиков и телематики;

  • воспроизводимость результатов;

  • корректная работа в динамических режимах;

  • устойчивость к шуму, выбросам и ошибкам оборудования;

  • универсальность для производственного учёта и аналитики.

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

Для получения достоверного веса нужно работать со всеми звеньями цепочки:

Уровень

Что происходит

Типичные проблемы

Физический уровень

Работа подвески, датчиков, изменение нагрузок

Неровности дороги, крены, динамическая нагрузка

Системный уровень

Аналог → цифра, передача данных, телематика

Сглаживание, задержки, потеря пакетов, квантование

Аналитический уровень

Фильтрация и расчёт веса

Шум, вибрации, ошибки разграничения стадий

Методология должна охватывать все три уровня.

Тарировка: фундамент расчёта веса

Тарировка — это процесс определения зависимости «измеренный параметр → фактический вес». Для карьерных и дорожных самосвалов обычно используются следующие методы тарировки:

Базовая статическая тарировка

Проводится на стационарных весах:

  1. Порожний самосвал:

    • фиксируется фактический вес;

    • проверяется стабильность датчиков.

  2. Гружёный самосвал:

    • выполняется 3–7 прогонов с разным грузом;

    • строится калибровочная кривая.

  3. Определяется математическая модель:

    • линейная (давление подвески → вес),

    • полиномиальная,

    • регрессионная модель.

Вероятность корректной калибровки: 90–95%.

Динамическая тарировка

Необходима, если самосвал работает только на неровных поверхностях.

Метод:

  • выполняются серии замеров при разных скоростях (5–30 км/ч);

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

  • формируются фильтры для подавления колебаний.

Погрешность снижается на 20–30%.

Регулярная ретарировка

Требуется каждые 3–6 месяцев:

  • деградация подвески,

  • падение давления в подушках,

  • изменение характеристик рамы,

  • старение датчиков.

Без ретарировки погрешность растёт на 10–25% ежегодно.

Методология расчёта перевозимого веса: общая схема

Методология включает 6 этапов. Она включает лучшие мировые практики для Caterpillar OWT, Komatsu VHMS, Volvo MATRIS.

Этап 1. Сбор данных

Данные получаются с трёх каналов:

Источник

Что даёт

Надёжность

Датчики подвески / тензодатчики

«Сырой» вес

Средняя

Бортовой компьютер / работомер

Усреднённый вес

Средняя–высокая

Трекер

Периодические значения

Низкая–средняя

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

Этап 2. Классификация стадии движения

Определяются ключевые режимы:

Стадия

Как распознать

Требования к алгоритму

Порожний

вес ≈ вес пустого самосвала ± 5–10%

строгая валидация

Погрузка

рост веса, низкая скорость

исключение из расчётов

Движение с грузом

вес > вес пустого самосвала + пороговый вес

адаптивная фильтрация

Разгрузка

резкое падение веса

исключение из расчётов(*)

Неопределённо

нет стабильности

автоопределение стадии

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

Почему важно исключать измерения при разгрузке

  1. Резкие искажения сигнала
    В момент разгрузки происходит резкое и быстрое падение веса в кузове — груз выгружается, что приводит к большим динамическим колебаниям и шума в сигнале датчиков. Из-за инертности и задержек в реакции измерительного оборудования данные становятся нестабильными и могут содержать сильные погрешности.

  2. Нестабильность распределения нагрузки
    Разгрузка вызывает неравномерное распределение массы в кузове и перемещения груза, что приводит к временным пиковым нагрузкам и скачкам в датчиках давления и тензодатчиках рамы. Это создает некорректные и непредсказуемые показания, которые не отражают реальный вес перевозимого груза.

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

  4. Задача расчёта — именно вес перевозимого груза
    Цель системы взвешивания — определить полезную нагрузку, находящуюся на самосвале в процессе транспортировки. Данные стадии разгрузки не соответствуют этому условию и могут ухудшить качество итоговых результатов анализа.

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

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

Этап 3. Очистка и валидация данных

Удаляются:

  • физически невозможные значения (ниже нуля / выше макс. грузоподъёмности × 1.2);

  • одиночные всплески;

  • шумовые периоды при резких манёврах.

На практике отсеивается 20–40% всех точек.

Этап 4. Выбор метода расчёта веса

Сценарий

Метод

Причина

Порожний вес

Медиана

Устойчивость к выбросам, данные сравнительно стабильны

Мало данных (<10)

Медиана

Минимизация ошибки при малом объёме информации

Высокий шум (Коэффициент вариации >15%)

Усечённое среднее

Удаляет 10–20% экстремальных значений для повышения стабильности данных

Стабильные условия

Среднее арифметическое

Максимальная точность при низком уровне шумов

Методы расчёта веса, используемые на борту самосвалов, выбираются с учётом характера данных, их объёма, уровня шума и наличия выбросов, чтобы обеспечить максимальную точность и надёжность результатов. Ниже объясняется почему используются конкретные методы, приведены соответствующие формулы и обоснования.

1. Медиана

Используется на этапах:

  • Порожний самосвал (почти постоянный вес)

  • Мало данных (<10 точек)

  • Общая минимизация ошибки при выбросах

Почему?
Медиана устойчива к выбросам и экстремальным значениям: она выбирает центральное значение в упорядоченном наборе данных, игнорируя влияние сильных искажений.

Формула медианы:
Пусть данные x1,x2,...,xn упорядочены по возрастанию. Тогда медиана M равна

Медиана особенно эффективна, когда количество данных невелико или когда данные содержат шумы и выбросы.

2. Усечённое среднее (Trimmed Mean)

Применяется при высоком шуме (коэффициент вариации CV > 15%) и большом числе данных.

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

Формула усечённого среднего:
Пусть от общего объема данных nnn отсечено по k на каждом из краёв (обычно 10-20% суммарно), тогда:

где значения xi отсортированы по возрастанию.
где значения xi отсортированы по возрастанию.

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

3. Среднее арифметическое

Используется в условиях стабильных и малошумных данных, когда много наблюдений.

Почему?
Среднее арифметическое даёт максимальную точность при нормальном распределении данных и низком уровне шумов.

Формула среднего арифметического:

Здесь все значения учитываются одинаково, что даёт лучший способ оценки средней тенденции при стабильных данных.

4. Обоснование выбора методов по сценарию

Сценарий

Метод

Причина выбора

Порожний вес

Медиана

Устойчивость к выбросам, данные сравнительно стабильны

Мало данных (<10)

Медиана

Минимизация ошибки при малом объёме информации

Высокий шум (>15%)

Усечённое среднее

Удаление экстремальных шумных значений для повышения стабильности

Стабильные условия

Среднее арифметическое

Максимальная точность при низком уровне шумов

Дополнительный параметр — Коэффициент вариации (CV)

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

где σ — стандартное отклонение, μ — среднее значение.
где σ — стандартное отклонение, μ — среднее значение.

Если CV > 15%, данные считаются шумными, и тогда предпочтительнее использовать усечённое среднее для минимизации влияния выбросов.

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

Это позволяет наилучшим образом использовать измерения веса на борту самосвала для оперативного и аналитического контроля.

Этап 5. Расчёт фактического веса груза

Груз определяется как:

Вес груза = Общий вес самосвала - Вес пустого самосвала

При этом:

  • если значение отрицательное → устанавливается 0;

  • при низком уровне достоверности результат < 0.6 запись расчета помечается как "требует проверки".

Примерные вероятности корректности расчёта:

  • Стабильный рельеф: 90–95%

  • Средний рельеф: 85–90%

  • Сложный профиль трассы: 75–85%

Этап 6. Оценка достоверности результата

В итоговую доверенность входят:

Фактор

Вес

Коэффициент вариации CV

Высокий

Количество точек данных

Средний

Наличие выбросов

Высокий

Отклонение от ожидаемого веса пустого самосвала

Высокий

Стабильность движений

Средний

Итоговый универсальный подход (кратко)

  1. Тарировка (статическая + динамическая).

  2. Определение стадии движения (порожний / загрузка / движение / разгрузка).

  3. Очистка и сертификация данных (фильтрация выбросов, отсечение шумов).

  4. Адаптивный выбор метода расчёта (медиана, среднее, усеченное среднее).

  5. Расчёт веса груза через разницу с весом пустого самосвала.

  6. Оценка достоверности и формирование комментариев расчета.

  7. Регулярная ретарировка каждые 3–6 месяцев.

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

Код C# как пример расчета

Для простоты восприятия "магические числа" оставлены в коде :)

/// <summary>
/// Методы расчёта веса для самосвала в зависимости от качества и состояния данных
/// </summary>
public enum CalculationMethod
{
    /// <summary>Автоматический выбор метода на основе данных</summary>
    Auto,

    /// <summary>Усечённое среднее: исключение 10-20% экстремальных значений</summary>
    TrimmedMean,

    /// <summary>Медиана: устойчива к выбросам, используется при малом объёме данных</summary>
    Median,

    /// <summary>Среднее арифметическое: максимальная точность в стабильных условиях</summary>
    Average
}

/// <summary>
/// Результат вычисления веса для заданной выборки данных с датчиков самосвала
/// </summary>
public class WeightCalculationResult
{
    /// <summary>Масса перевозимого груза (разница между полной массой и массой пустого самосвала)</summary>
    public double CargoWeight { get; set; }

    /// <summary>Общий измеренный вес самосвала (полная масса с грузом)</summary>
    public double TotalWeight { get; set; }

    /// <summary>Коэффициент доверия к результату (от 0 до 1)</summary>
    public double Confidence { get; set; }

    /// <summary>Использованный метод расчёта веса</summary>
    public CalculationMethod UsedMethod { get; set; }

    /// <summary>Количество использованных точек данных</summary>
    public int DataPointsCount { get; set; }

    /// <summary>Стандартное отклонение значений выборки</summary>
    public double StandardDeviation { get; set; }

    /// <summary>Качественная оценка данных (ошибки, предупреждения)</summary>
    public string QualityAssessment { get; set; } = string.Empty;
}

/// <summary>
/// Класс, реализующий методы расчёта веса с учётом стадии движения самосвала
/// </summary>
public static class WeightCalculator
{
    private static readonly WeightCalculatorConfig DefaultConfig = new();

    /// <summary>
    /// Основной метод вычисления веса полезной нагрузки на основе данных с датчиков и стадии движения
    /// </summary>
    /// <param name="sensorRecords">Набор измерений веса (например, давление в подвеске, тензометр)</param>
    /// <param name="stage">Текущая стадия движения самосвала (Порожний, Груженый, Неопределённая)</param>
    /// <param name="emptyTruckWeight">Вес пустого самосвала (ключевой параметр для расчёта груза)</param>
    /// <param name="config">Конфигурация параметров расчёта</param>
    /// <returns>Результат расчёта (масса груза, общий вес, качество, доверие)</returns>
    public static WeightCalculationResult CalculateCargoWeight(
        IEnumerable<SensorRecord> sensorRecords,
        DumpTruckStateEnum stage,
        double emptyTruckWeight = 0,
        WeightCalculatorConfig? config = null)
    {
        config ??= DefaultConfig;

        // Валидация входных данных
        bool shouldCheckEmptyWeight = false; // Можно расширить логику валидации
        var validationResult = ValidateInput(sensorRecords, emptyTruckWeight, stage, shouldCheckEmptyWeight);
        if (!validationResult.IsValid)
        {
            return new WeightCalculationResult
            {
                CargoWeight = 0,
                TotalWeight = 0,
                Confidence = 0,
                UsedMethod = CalculationMethod.Auto,
                DataPointsCount = 0,
                StandardDeviation = 0,
                QualityAssessment = validationResult.ErrorMessage
            };
        }

        // Фильтруем физически невозможные значения и нулевые
        var values = sensorRecords!
            .Where(r => r != null)
            .Select(r => r.Value)
            .Where(v => IsPhysicallyPossible(v, emptyTruckWeight, stage, config))
            .ToList();

        // Выбор метода расчёта на основе стадии движения
        return stage switch
        {
            DumpTruckStateEnum.ReturningEmpty => CalculateForEmptyStage(values, emptyTruckWeight, config),
            DumpTruckStateEnum.HaulingLoaded => CalculateForLoadedStage(values, emptyTruckWeight, config),
            _ => CalculateForUnknownStage(values, emptyTruckWeight, config)
        };
    }

    /// <summary>
    /// Расчёт для этапа с порожним самосвалом (стабильен, вес близок к emptyTruckWeight)
    /// Используется медиана для устойчивости к выбросам
    /// </summary>
    private static WeightCalculationResult CalculateForEmptyStage(List<double> values, double emptyTruckWeight, WeightCalculatorConfig config)
    {
        var result = new WeightCalculationResult
        {
            DataPointsCount = values.Count,
            StandardDeviation = CalculateStandardDeviation(values)
        };

        double median = CalculateMedian(values);

        // Проверка отклонения от ожидаемого веса порожнего самосвала
        double expectedWeight = emptyTruckWeight;
        double weightDifference = Math.Abs(median - expectedWeight);
        double maxAllowedDifference = config.MaxEmptyWeightDeviation * expectedWeight;

        if (weightDifference > maxAllowedDifference)
        {
            result.Confidence = CalculateConfidence(values, 0.3, config); // Низкая достоверность
            result.QualityAssessment = "Вес порожнего самосвала значительно отличается от ожидаемого";
        }
        else
        {
            result.Confidence = CalculateConfidence(values, 0.8, config); // Высокая достоверность
            result.QualityAssessment = "Данные соответствуют ожиданиям для порожнего самосвала";
        }

        result.TotalWeight = median;
        result.CargoWeight = Math.Max(0, median - emptyTruckWeight);
        result.UsedMethod = CalculationMethod.Median;

        if (double.IsNaN(result.CargoWeight) || double.IsInfinity(result.CargoWeight))
        {
            result.CargoWeight = 0;
            result.Confidence = 0;
            result.QualityAssessment += " | Расчет прерван из-за некорректных числовых значений";
        }

        return result;
    }

    /// <summary>
    /// Расчёт для этапа с груженым самосвалом (шумные данные, выбор метода зависит от качества данных)
    /// </summary>
    private static WeightCalculationResult CalculateForLoadedStage(List<double> values, double emptyTruckWeight, WeightCalculatorConfig config)
    {
        var result = new WeightCalculationResult
        {
            DataPointsCount = values.Count,
            StandardDeviation = CalculateStandardDeviation(values)
        };

        double std = result.StandardDeviation;
        double mean = values.Average();
        double cv = mean > config.MinMeanValueForCV ? std / mean : double.MaxValue; // Коэффициент вариации

        if (values.Count < config.MinimumDataPointsForReliableCalculation)
        {
            // Мало данных - применяем медиану для минимизации ошибки
            double median = CalculateMedian(values);
            result.TotalWeight = median;
            result.CargoWeight = Math.Max(0, median - emptyTruckWeight);
            result.UsedMethod = CalculationMethod.Median;
            result.Confidence = CalculateConfidence(values, 0.5, config);
            result.QualityAssessment = "Мало данных для надежного расчета";
        }
        else if (cv > config.HighNoiseThreshold)
        {
            // Высокий шум - применяем усечённое среднее для удаления выбросов
            double trimmedMean = CalculateTrimmedMean(values, config.LoadedTrimPercentage);
            result.TotalWeight = trimmedMean;
            result.CargoWeight = Math.Max(0, trimmedMean - emptyTruckWeight);
            result.UsedMethod = CalculationMethod.TrimmedMean;
            result.Confidence = CalculateConfidence(values, 0.6, config);
            result.QualityAssessment = "Применена усиленная фильтрация из-за высокого шума";
        }
        else
        {
            // Низкий шум и достаточное количество данных - используем среднее арифметическое для максимальной точности
            result.TotalWeight = mean;
            result.CargoWeight = Math.Max(0, mean - emptyTruckWeight);
            result.UsedMethod = CalculationMethod.Average;
            result.Confidence = CalculateConfidence(values, 0.9, config);
            result.QualityAssessment = "Данные хорошего качества";
        }

        if (result.CargoWeight < config.MinExpectedPayload)
        {
            result.QualityAssessment += " | Рассчитанный вес груза подозрительно мал";
            result.Confidence *= 0.7;
        }

        if (double.IsNaN(result.CargoWeight) || double.IsInfinity(result.CargoWeight))
        {
            result.CargoWeight = 0;
            result.Confidence = 0;
            result.QualityAssessment += " | Расчет прерван из-за некорректных числовых значений";
        }

        return result;
    }

    /// <summary>
    /// Расчёт для неопределённого состояния - автоматическое определение стадии и рекурсивный вызов метода
    /// </summary>
    private static WeightCalculationResult CalculateForUnknownStage(List<double> values, double emptyTruckWeight, WeightCalculatorConfig config)
    {
        double median = CalculateMedian(values);
        double expectedEmptyWeight = emptyTruckWeight;
        double tolerance = config.EmptyWeightTolerance * expectedEmptyWeight;

        DumpTruckStateEnum detectedStage = Math.Abs(median - expectedEmptyWeight) <= tolerance
            ? DumpTruckStateEnum.ReturningEmpty
            : DumpTruckStateEnum.HaulingLoaded;

        var result = detectedStage == DumpTruckStateEnum.ReturningEmpty
            ? CalculateForEmptyStage(values, emptyTruckWeight, config)
            : CalculateForLoadedStage(values, emptyTruckWeight, config);

        result.QualityAssessment += $" | Стадия определена автоматически: {detectedStage}";

        return result;
    }

    /// <summary>
    /// Проверка входных данных на корректность (наличие, возвращаемость весов, логичность)
    /// </summary>
    private static ValidationResult ValidateInput(
        IEnumerable<SensorRecord> sensorRecords,
        double emptyTruckWeight,
        DumpTruckStateEnum stage,
        bool checkReturningEmpty)
    {
        if (sensorRecords == null)
            return ValidationResult.Invalid("Отсутствуют данные датчиков");

        var records = sensorRecords.ToList();
        if (records.Count == 0)
            return ValidationResult.Invalid("Нет записей данных");

        if (records.All(r => r == null))
            return ValidationResult.Invalid("Все записи данных являются null");

        if (emptyTruckWeight < 0)
            return ValidationResult.Invalid("Вес пустого самосвала не может быть отрицательным");

        if (checkReturningEmpty)
        {
            if (emptyTruckWeight == 0 && stage == DumpTruckStateEnum.ReturningEmpty)
                return ValidationResult.Invalid("Для стадии 'порожний' должен быть указан вес пустого самосвала");
        }

        return ValidationResult.Valid();
    }

    /// <summary>
    /// Проверка физической корректности измеренного веса
    /// </summary>
    private static bool IsPhysicallyPossible(
        double weight,
        double emptyTruckWeight,
        DumpTruckStateEnum stage,
        WeightCalculatorConfig config)
    {
        if (weight <= 0) return false;

        double maxPossibleWeight = emptyTruckWeight + config.MaxExpectedPayload * 1.2;
        if (weight > maxPossibleWeight) return false;

        if (stage == DumpTruckStateEnum.ReturningEmpty)
        {
            double difference = Math.Abs(weight - emptyTruckWeight);
            return difference <= config.MaxEmptyWeightDeviation * emptyTruckWeight;
        }

        return true;
    }

    /// <summary>
    /// Расчёт коэффициента доверия с учётом шумов и объёма данных (от 0 до 1)
    /// </summary>
    private static double CalculateConfidence(List<double> values, double baseConfidence, WeightCalculatorConfig config)
    {
        if (values.Count == 0) return 0;

        double std = CalculateStandardDeviation(values);
        double mean = values.Average();
        double effectiveMean = Math.Max(mean, config.MinMeanValueForCV);
        double cv = std / effectiveMean;

        double cvFactor = Math.Max(0, 1 - cv);
        double dataPointsFactor = Math.Min(1, values.Count / 20.0);

        double confidence = baseConfidence * cvFactor * dataPointsFactor;
        return Math.Max(0, Math.Min(1, confidence));
    }

    /// <summary>
    /// Стандартное отклонение выборки
    /// </summary>
    private static double CalculateStandardDeviation(List<double> values)
    {
        if (values.Count < 2) return 0;

        double mean = values.Average();
        double sumSq = values.Sum(v => Math.Pow(v - mean, 2));
        return Math.Sqrt(sumSq / (values.Count - 1));
    }

    /// <summary>
    /// Медиана выборки (устойчивая к выбросам характеристика центральной тенденции)
    /// </summary>
    private static double CalculateMedian(List<double> values)
    {
        if (values.Count == 0) return 0;

        var sorted = values.OrderBy(v => v).ToList();
        int count = sorted.Count;

        return count % 2 == 1
            ? sorted[count / 2]
            : (sorted[(count - 1) / 2] + sorted[count / 2]) / 2.0;
    }

    /// <summary>
    /// Усечённое среднее — среднее после удаления экстремальных значений с двух концов (например, по 5-10%)
    /// </summary>
    private static double CalculateTrimmedMean(List<double> values, double trimPercentage)
    {
        if (values == null) throw new ArgumentNullException(nameof(values));
        if (values.Count == 0) return 0;
        if (trimPercentage < 0 || trimPercentage >= 0.5)
            throw new ArgumentOutOfRangeException(nameof(trimPercentage), "trimPercentage должен быть в диапазоне [0, 0.5)");

        if (values.Count == 1) return values[0];
        if (values.Count == 2) return values.Average();
        if (values.Count == 3) return CalculateMedian(values);

        var sorted = values.OrderBy(v => v).ToList();

        int trimCount = CalculateOptimalTrimCount(values.Count, trimPercentage);
        int remainingCount = values.Count - 2 * trimCount;

        var trimmedValues = sorted
            .Skip(trimCount)
            .Take(remainingCount)
            .ToList();

        return CalculateRobustAverage(trimmedValues, values);
    }

    /// <summary>
    /// Расчёт оптимального количества элементов для усечения
    /// </summary>
    private static int CalculateOptimalTrimCount(int totalCount, double trimPercentage)
    {
        int calculated = (int)(totalCount * trimPercentage);
        int minRemaining = Math.Max(1, totalCount / 10);
        int maxTrim = (totalCount - minRemaining) / 2;

        return Math.Min(calculated, maxTrim);
    }

    /// <summary>
    /// Устойчивый к числовым ошибкам расчет среднего
    /// </summary>
    private static double CalculateRobustAverage(List<double> values, List<double> fallbackValues)
    {
        if (values.Count == 0)
            return CalculateMedian(fallbackValues);

        try
        {
            double sum = 0;
            int validCount = 0;

            foreach (double value in values)
            {
                if (double.IsFinite(value))
                {
                    sum += value;
                    validCount++;
                }
            }

            if (validCount == 0) return CalculateMedian(fallbackValues);

            double result = sum / validCount;
            return double.IsFinite(result) ? result : CalculateMedian(fallbackValues);
        }
        catch (OverflowException)
        {
            return CalculateMedian(fallbackValues);
        }
    }
}

/// <summary>
/// Конфигурация параметров расчёта веса
/// </summary>
public class WeightCalculatorConfig
{
    /// <summary>Процент данных для отсечения с каждого конца при расчёте усечённого среднего для порожнего самосвала (например, 10%)</summary>
    public double EmptyTrimPercentage { get; set; } = 0.10;

    /// <summary>Процент данных для отсечения с каждого конца при расчёте усечённого среднего для груженого самосвала (например, 5%)</summary>
    public double LoadedTrimPercentage { get; set; } = 0.05;

    /// <summary>Порог коэффициента вариации для определения высокого шума (например, 15%)</summary>
    public double HighNoiseThreshold { get; set; } = 0.15;

    /// <summary>Минимальное количество точек данных для надежного расчёта</summary>
    public int MinimumDataPointsForReliableCalculation { get; set; } = 10;

    /// <summary>Максимальное допустимое отклонение веса порожнего самосвала от эталонного (например, 10%)</summary>
    public double MaxEmptyWeightDeviation { get; set; } = 0.10;

    /// <summary>Допуск при определении стадии "порожний" по весу (например, 5%)</summary>
    public double EmptyWeightTolerance { get; set; } = 0.05;

    /// <summary>Максимальная ожидаемая грузоподъемность (например, 40 000 кг)</summary>
    public double MaxExpectedPayload { get; set; } = 40000;

    /// <summary>Минимально ожидаемый груз для валидности</summary>
    public double MinExpectedPayload { get; set; } = 1000;

    /// <summary>Минимальное значение среднего веса для расчета коэффициента вариации</summary>
    public double MinMeanValueForCV { get; set; } = 1.0;
}

/// <summary>
/// Результат валидации входных данных
/// </summary>
public class ValidationResult
{
    /// <summary>Признак корректности данных</summary>
    public bool IsValid { get; set; }

    /// <summary>Сообщение об ошибке, если данные некорректны</summary>
    public string ErrorMessage { get; set; } = string.Empty;

    public static ValidationResult Valid() => new() { IsValid = true };

    public static ValidationResult Invalid(string message) => new() { IsValid = false, ErrorMessage = message };
}

/// <summary>
/// Запись данных с датчика веса на борту самосвала
/// </summary>
public class SensorRecord
{
    /// <summary>Уникальный идентификатор записи</summary>
    public Guid Id { get; set; } = Guid.NewGuid();

    /// <summary>Идентификатор самосвала</summary>
    public int TruckId { get; init; }

    /// <summary>Название или метка самосвала</summary>
    public string TruckName { get; init; } = string.Empty;

    /// <summary>Тип датчика (например, датчик давления подвески, тензодатчик)</summary>
    public SensorType Type { get; init; }

    /// <summary>Измеренное значение веса в момент времени</summary>
    public double Value { get; init; }

    /// <summary>Временная метка измерения</summary>
    public DateTime Timestamp { get; init; }
}

/// <summary>
/// Перечисление стадий движения самосвала
/// </summary>
public enum DumpTruckStateEnum
{
    /// <summary>Порожний самосвал (без груза)</summary>
    ReturningEmpty,

    /// <summary>Груженый самосвал (с полезной нагрузкой)</summary>
    HaulingLoaded,

    /// <summary>Неопределённое состояние, требуется автоматическое определение</summary>
    Unknown
}

/// <summary>
/// Тип датчика, для полноты модели
/// </summary>
public enum SensorType
{
    /// <summary>Датчик давления гидропневматической подвески</summary>
    HydraulicPressure,

    /// <summary>Тензодатчик на раме или мостах</summary>
    StrainGauge,

    /// <summary>Датчик давления пневмоподвески</summary>
    PneumaticPressure,

    /// <summary>Прочие</summary>
    Other
}

Пояснения к коду

  • Метод CalculateCargoWeight автоматически выбирает способ обработки данных с учетом этапа движения и качества сигналов.

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

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

  • Используются статистические показатели: стандартное отклонение и коэффициент вариации для оценки шума.

  • В коде есть детальная валидация на физическую возможность и проверка качества данных.

  • Дополнительные параметры (например, пороги, проценты усечения) вынесены в конфигурационный класс.

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

  • Аннотирование /// <summary> делает код пригодным для документации в статье и удобным к пониманию.

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


  1. vfreeeman
    20.11.2025 10:52

    Статья, достойная хабра...


  1. CitizenOfDreams
    20.11.2025 10:52

    Глупый вопрос - а зачем нужно постоянно измерять вес груза в движении, а не один раз сразу после погрузки или разгрузки, когда самосвал стоит на месте? Его же не на ходу загружают/разгружают?


    1. HumanBearPig
      20.11.2025 10:52

      По опыту работы в Ковдорском ГОКе и использования ПО ВИСТ V2:
      В Caterpillar 777-793 вес определялся при погрузке и "фиксировался" при переключении на 3-ю передачу. В ходе рейса вес далее не менялся.
      Система контроля загрузки БелАЗ 7513х - 753хх определяла вес в течение всего рейса. Был вес как при погрузке, так и при разгрузке, водитель видел на экране один вес, а грузила - другой (а диспетчер вообще мог видеть средний из этих двух значений), что приводило к непониманию сторонами друг друга; потом всё-таки пришли к одному значению.

      Опытный водитель на обеих типах машин знал как и скинуть и докинуть себе веса.

      Определение веса зависит от продольного и поперечного уклона самосвала при погрузке и от центра масс груза относительно кузова. Измерение в динамике дополняет измерение в статике, повышает точность.