Задача

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

Решение

Задача классическая, поэтому и в сети можно найти множество подробных рассуждений на тему, как её правильно решать с помощью различных математических методов, в том числе с применением MS Excel. Также имеется множество интернет-ресурсов с он-лайн калькуляторами, платными и бесплатными. Тем не менее, я решил предложить свой вариант реализации расчета с использованием платформы My Visual Database (MVDB) в качестве инструмента для создания соответствующей программы. В дальнейшем планирую использовать рассматриваемый метод в составе программы “Производство”, совместив его с системой складского учёта.

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

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

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

Реализация

Для хранения исходных данных и результата используется несколько таблиц БД, а для перебора вариантов данные загружаются в оперативную память. К сожалению, MVDB не поддерживает структуры (Record), поэтому для хранения данных использованы динамические массивы – отдельный массив для каждой величины (при реализации данного алгоритма на других языках он будет немного элегантней).

После инициализации массивов и загрузки в них данных из таблиц БД, организуется два цикла repeat..until. Внешний – для перебора заготовок, внутренний – для подбора вариантов раскладки. В качестве оптимизации во внутреннем цикле использованы проверки:

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

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

Скрипт

procedure frmCuttingMap_btnCalc_OnClick (Sender: TObject; var Cancel: boolean); // выполнение расчета var tmpSQL: string; // для хранения текста запроса, удобно для отладки tmpQty: integer; // количество деталей tmpDetLen: array of integer; // список длин деталей tmpDetQty: array of integer; // список количества деталей tmpBQty: integer; // количество заготовок tmpBIndex: integer; // индекс заготовки, с которой сейчас работает алгоритм tmpBlankLen: array of integer; // список длин заготовок tmpBlankQty: array of integer; // список количества заготовок tmpMap: string; // карта нарезки tmpMapQty: integer; // количество использований карты нарезки tmpTestQty: array of integer; // список количества деталей для текущего распила tmpBestQty: array of integer; // лучший распил tmpBestBlankLen: integer; // текущая длина заготовки для записи tmpTestBlankLen: integer; // длина текущей заготовки для расчета tmpTestScrap: integer; // текущий обрезок tmpTestLen: integer; // текущая длина tmpBestScrap: integer; // лучший обрезок tmpLastIncPos: integer; // последний разряд, который был увеличен tmpIncPos: integer;// разряд, который увеличиваем в основном цикле tmpDataSet: TDataSet; i,j: integer; tmpFlag: boolean; s: string; // модификатор матрицы нарезки function NextIndex( APos: integer ):boolean; begin if APos >= tmpQty then begin Result := False; end else begin Result := True; tmpTestQty[APos] := tmpTestQty[APos] + 1; tmpLastIncPos := APos; if tmpTestQty[APos] > tmpDetQty[APos] then begin tmpTestQty[APos] := 0; Result := NextIndex( APos + 1); end; end; end; // begin Progress(0,0,'Поиск решения...',True); try // сохранить настройки для ширины распила CutWidth := Trunc( frmCuttingMap.edtCutWidth.Value ); IniFile_Write_Int(APP_PARAMS,'CutWidth',CutWidth); // инициализация SQLExecute('DELETE FROM cuttingMap'); // очистить таблицу результатов // tmpBQty := SQLExecute('SELECT count(*) FROM blank '); // число заготовок SetLength( tmpBlankLen, tmpBQty); // список длин заготовок SetLength( tmpBlankQty, tmpBQty); // список количества заготовок // данные должны быть упорядочены по возрастанию, чтобы обеспечить стратегию использование коротких обрезков в первую очередь SQLQuery('SELECT * FROM blank ORDER BY length ASC',tmpDataSet); try for i := 0 to tmpBQty-1 do begin tmpBlankLen[i] := tmpDataSet.FieldByName('length').asInteger; if tmpDataSet.FieldByName('basic').asInteger = 1 then tmpBlankQty[i] := MAX_QTY else tmpBlankQty[i] := tmpDataSet.FieldByName('qty').asInteger; tmpDataSet.next; end; finally tmpDataSet.Free; end; tmpQty := SQLExecute('SELECT count(*) FROM piece '); // число позиций SetLength( tmpDetLen, tmpQty); // список длин деталей SetLength( tmpDetQty, tmpQty); // список количества деталей // начинаем с первого размера заготовки tmpBIndex := 0; SetLength( tmpTestQty, tmpQty); // список количества деталей для текущего распила SetLength( tmpBestQty, tmpQty); // данные должны быть упорядочены по убыванию! SQLQuery('SELECT * FROM piece ORDER BY length DESC',tmpDataSet); try for i := 0 to tmpQty-1 do begin tmpDetLen[i] := tmpDataSet.FieldByName('length').asInteger; tmpDetQty[i] := tmpDataSet.FieldByName('qty').asInteger; tmpDataSet.next; end; finally tmpDataSet.Free; end; // repeat tmpTestBlankLen := tmpBlankLen[tmpBIndex]; tmpBestScrap := tmpTestBlankLen; // лучший обрезок равен длине заготовки // сбрасываем матрицу нарезки for i:=0 to tmpQty-1 do tmpTestQty[i] := 0; // tmpIncPos := 0; repeat Application.ProcessMessages; // досрочное завершение if ProgressCancel then exit; if not NextIndex( tmpIncPos ) then // модифицируем матрицу нарезки break // если не удалось, то выход из цикла else begin // смотрим, что у нас получилось по длине tmpTestLen := 0; tmpFlag := False; for i :=0 to tmpQty - 1 do begin tmpTestLen := tmpTestLen + (tmpDetLen[i] + CutWidth)* tmpTestQty[i]; // потом нужно учесть, что последний распил может и не понадобится... if tmpTestLen > tmpTestBlankLen then // если длина превысила длину заготовки, дальше можно не считать - плохой вариант begin tmpFlag := True; break; end; end; // если было переполнение, то перейти к следующему варианту if tmpFlag then begin tmpTestQty[tmpLastIncPos] := 0; // сбрасываем счетчик в разряде, который увеличивали последним tmpIncPos := tmpLastIncPos + 1; // будем увеличивать счетчик следующего разряда, с меньшим весовым коэффициентом continue; end; tmpTestScrap := tmpTestBlankLen - tmpTestLen; // обрезок // if tmpTestScrap < tmpBestScrap then // если найден лучший вариант begin tmpBestScrap := tmpTestScrap; for i:=0 to tmpQty - 1 do // запоминаем вариант раскроя tmpBestQty[i] := tmpTestQty[i]; end; // идеальный вариант - нулевой обрезок, лучше не бывает if tmpBestScrap = 0 then break; // готовимся к инкременту старшего разряда tmpIncPos := 0; end; until 1=0; // цикл подбора вариантов; выход, если нельзя получить след.матрицу // возможен случай когда текущий размер не подходит для расчета if tmpBestScrap = tmpTestBlankLen then begin // перейти к следующему размеру без записи результата inc( tmpBIndex ); // перейти к следующей заготовке if tmpBIndex < tmpBQty then begin continue; // продолжаем... end else begin break; // заготовки кончились... end; end; // рисуем карту раскладки tmpMap := ''; for i := 0 to tmpQty - 1 do for j := 0 to tmpBestQty[i] - 1 do begin if tmpMap <> '' then tmpMap := tmpMap + ': '; tmpMap := tmpMap + IntToStr( tmpDetLen[ i ] ); end; // tmpMapQty := 0; // считаем, сколько раз карту можно использовать tmpBestBlankLen := tmpTestBlankLen; repeat inc(tmpMapQty); // уменьшаем остатки по деталям for i := 0 to tmpQty - 1 do tmpDetQty[i] := tmpDetQty[i] - tmpBestQty[i]; // уменьшаем остатки по заготовкам tmpBlankQty[tmpBIndex] := tmpBlankQty[tmpBIndex] - 1; // теперь надо определить, можно ли использовать такую раскладку ещё раз tmpFlag := false; // проверка по остаткам деталей for i := 0 to tmpQty - 1 do if tmpBestQty[i] > tmpDetQty[i] then begin tmpFlag := True; break; end; // проверка по остаткам заготовок if tmpBlankQty[tmpBIndex] = 0 then begin tmpFlag := True; inc( tmpBIndex ); // перейти к следующей заготовке end; until tmpFlag; // Если что-то удалось подобрать, то if tmpBestScrap <> tmpTestBlankLen then // сохраняем результат SQLExecute('INSERT INTO cuttingMap (blankLength,qty,map,scrap) VALUES ('+IntToStr(tmpBestBlankLen)+','+IntToStr(tmpMapQty)+','+StrToSQL(tmpMap)+','+IntToStr(tmpBestScrap)+')'); // проверяем, осталось ли что-нибудь на изготовление tmpFlag := false; for i := 0 to tmpQty - 1 do if tmpDetQty[i] > 0 then begin tmpFlag := True; // да, что-то нашли break; end; // проверяем, есть ли заготовки if tmpBIndex = tmpBQty then tmpFlag := False; until not tmpFlag; // повторять до тех пор, пока есть что-нибудь на изготовление // обновить отображение данных frmCuttingMap.tgrMap.dbUpdate; finally Progress(); end; ShowMessage('Раcчет завершен'); end;
Code language: Delphi (delphi)

В строке 48 и 213 использована процедура отображения индикатора прогресса выполнения, подробно описанная в статье “Во имя прогресса”.

В 66 строке использована константа MAX_QTY = 1000000; чтобы по заготовкам, которые можно купить, не было завершения внешнего цикла по причине нехватки их количества.

Структура данных

В таблице blank хранятся размеры заготовок, в таблице piece – информация по деталям, которые нужно изготовить, а в таблице cuttingMap – результат: карты нарезки деталей.

Интерфейс

Для редактирования исходных данных используются таблицы с включенным режимом редактирования данных (AllowCreate = True, AllowEdut = True, AllowDelete = True).

Для управления данными предназначены кнопки, расположенные на панели инструментов:

  • Очистить входные данные
  • Выполнить расчет
  • Сформировать документ
  • Сформировать PDF

Также на панели инструментов расположено поле, в котором можно указать ширину разреза. Для фрезы ширина может иметь значение 2-3 мм, а при резке лазером ширина разреза составляет всего 0.1 мм. Это означает, что на практике при расчетах можно указать нулевое значение.

Результат

Для вывода результат понадобился отчет с тремя источниками данных. Для формирования отчета служит скрипт, использующий процедуру Report_Open() , входящую в состав библиотечных процедур программы “ClearApp”. В этом же скрипте рассчитывается эффективность полученной раскладки (процент потерь).

procedure UserApp_LinearCalc_Report( AReportMode: integer ); // отчет линейного раскроя var tmpDataSets : array of TDataSet; tmpDSNames: array of string; tmpStrParam: array of string; tmpStrParamName: array of string; tmpReportMode:integer; tmpReportFileName:string; tmpSQL: string; tmpID: string; i: integer; tmpFileName: string; begin tmpReportMode := AReportMode; tmpReportFileName := 'LinearCalc.fr3'; SetLength( tmpDataSets,3 ); // детали, расчёт и кол-во по размерам SetLength( tmpDSNames,3 ); // SetLength( tmpStrParam, 2 ); // параметры: процент использования SetLength( tmpStrParamName, 2 ); // детали tmpSQL := ' SELECT * FROM piece '; SQLQuery( tmpSQL, tmpDataSets[0] ); tmpDSNames[0] := 'Piece'; // результат tmpSQL := ' SELECT * FROM cuttingMap '; SQLQuery( tmpSQL, tmpDataSets[1] ); tmpDSNames[1] := 'CuttingMap'; // сводный результат tmpSQL := 'SELECT blankLength, sum(qty) as qty FROM cuttingMap GROUP BY blankLength'; SQLQuery( tmpSQL, tmpDataSets[2] ); tmpDSNames[2] := 'blankSummary'; // параметры i := 0; // Коэффициент использования tmpStrParamName[i] := 'Percent'; tmpStrParam[i] := FormatFloat('#0.0', ( SQLExecute('SELECT sum( scrap * qty ) FROM cuttingMap') / SQLExecute('SELECT sum( blankLength * qty ) FROM cuttingMap') )*100 )+'%'; inc(i); // Коэффициент использования tmpStrParamName[i] := 'CutWidth'; tmpStrParam[i] := IntToStr(CutWidth)+' мм.'; inc(i); // вызвать универсальную функцию открытия отчета tmpFileName := Report_Open( tmpDataSets, tmpDSNames, tmpStrParam, tmpStrParamName, tmpReportMode, tmpReportFileName ); // чистим... Report_FreeDataSets( tmpDataSets ); if AReportMode = RM_PDF then OpenFile(tmpFileName); end;
Code language: Delphi (delphi)

Итоги

Несмотря на то, что проект My Visual Database перестал развиваться более года назад, он все ещё пригоден для создания удобных и эффективных средств автоматизации бизнес-процессов.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *