From 6d1f104beb3bb2a1d2f678024f1f7b20bdeda427 Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Tue, 8 Oct 2024 07:16:29 -0400 Subject: [PATCH] fix: multitool with no headland Row distribution calculation refactored, unit tests added. --- .github/workflows/unit-test.yml | 1 + scripts/courseGenerator/Center.lua | 57 ++++---- scripts/courseGenerator/test/CenterTest.lua | 142 ++++++++++++++++++++ 3 files changed, 172 insertions(+), 28 deletions(-) create mode 100644 scripts/courseGenerator/test/CenterTest.lua diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 2a273747d..367701923 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -37,6 +37,7 @@ jobs: cd scripts/courseGenerator/test lua BlockSequencerTest.lua lua CacheMapTest.lua + lua CenterTest.lua lua FieldTest.lua lua FieldworkCourseTest.lua lua FieldworkCourseMultiVehicleTest.lua diff --git a/scripts/courseGenerator/Center.lua b/scripts/courseGenerator/Center.lua index 3f1a6c0a9..2304cabc4 100644 --- a/scripts/courseGenerator/Center.lua +++ b/scripts/courseGenerator/Center.lua @@ -18,11 +18,10 @@ function Center:init(context, boundary, headland, startLocation, bigIslands) if headland == nil then -- if there are no headlands, we generate a virtual one, from the field boundary -- so using this later is equivalent of having an actual headland - -- using the center row spacing as the width of the headland, since we want to - -- cover the entire field with the center rows and the headland width may be the single - -- working width with multi vehicles + -- using the nominal (without overlap) working the width for the headland, since the rows adjacent to the + -- headland must not extend beyond the field boundary local virtualHeadland = CourseGenerator.FieldworkCourseHelper.createVirtualHeadland(boundary, self.context.headlandClockwise, - self.context:getCenterRowSpacing()) + self.context.workingWidth) if self.context.sharpenCorners then virtualHeadland:sharpenCorners(self.context.turningRadius) end @@ -241,8 +240,7 @@ function Center:_generateStraightUpDownRows(rowAngle, suppressLog) end -- move the baseline to the edge of the area we want to cover baseline = baseline:createNext(dMin) - local rowOffsets = self:_calculateRowDistribution( - self.context:getCenterRowSpacing(), dMax - dMin, self.context.evenRowDistribution, overlapLast) + local rowOffsets = self:_calculateRowDistribution(dMax - dMin, overlapLast) local rows = {} local row = baseline:createNext(rowOffsets[1]) @@ -332,17 +330,22 @@ end --- 3. leave the width of all rows the same working width. Here, part of the first or last row will be --- outside of the field (work width * number of rows > field width). We always do this if there is a headland, --- as the remainder will overlap with the headland. ----@param centerWorkingWidth number working width on the up/down rows in the center ---@param fieldWidth number distance between the headland centerlines we need to fill with rows. If there is no --- headland, this is the distance between the virtual headland centerlines, which is half working width wider than --- the actual field boundary. ----@param sameWidth boolean make all rows of the same width (#1 above) ---@param overlapLast boolean where should the overlapping row be in the sequence we create, true if at the end, --- false at the beginning ----@return number, number, number, number number of rows, offset of first row from the field edge, offset of ---- rows from the previous row for the next rows, offset of last row from the next to last row. -function Center:_calculateRowDistribution(centerWorkingWidth, fieldWidth, sameWidth, overlapLast) - local nRows = math.floor(fieldWidth / centerWorkingWidth) +---@return number[] offset of each row from the previous, the first offset is from the baseline +function Center:_calculateRowDistribution(fieldWidth, overlapLast) + local centerWorkingWidth = self.context:getCenterRowSpacing() + -- only use the overlap-corrected headland width if we have headlands, otherwise, must use the + -- nominal working width to avoid generating rows extending outside of the field + local headlandWorkingWidth = self.mayOverlapHeadland and self.context:getHeadlandWorkingWidth() or + self.context.workingWidth + -- making the field width 1 cm less to avoid generating the last row exactly on the headland if + -- the field width is an exact multiple of the working width + local nRows = math.floor((fieldWidth - headlandWorkingWidth - 0.01) / centerWorkingWidth) + 1 + print('nRows', nRows, self.mayOverlapHeadland) if nRows == 0 then -- only one row fits between the headlands if overlapLast then @@ -351,39 +354,37 @@ function Center:_calculateRowDistribution(centerWorkingWidth, fieldWidth, sameWi return { fieldWidth - centerWorkingWidth / 2 } end else - local width - if sameWidth then + if self.context.evenRowDistribution then -- #1 - width = (fieldWidth - centerWorkingWidth) / (nRows - 1) - else - -- #2 and #3 - width = centerWorkingWidth + centerWorkingWidth = (fieldWidth - centerWorkingWidth) / (nRows - 1) end local firstRowOffset local rowOffsets = {} + -- the first/last row's offset from the surrounding headland centerline + local outermostRowOffset = headlandWorkingWidth / 2 + centerWorkingWidth / 2 if self.mayOverlapHeadland then -- #3 we have headlands if overlapLast then - firstRowOffset = centerWorkingWidth + firstRowOffset = outermostRowOffset else - firstRowOffset = fieldWidth - (centerWorkingWidth + width * (nRows - 1)) + firstRowOffset = fieldWidth - outermostRowOffset - (centerWorkingWidth * (nRows - 1)) end rowOffsets = { firstRowOffset } - for _ = firstRowOffset, fieldWidth, width do - table.insert(rowOffsets, width) + for _ = 2, nRows do + table.insert(rowOffsets, centerWorkingWidth) end else -- #2, no headlands - for _ = centerWorkingWidth, fieldWidth - centerWorkingWidth, width do - table.insert(rowOffsets, width) + rowOffsets = { outermostRowOffset } + for _ = 2, nRows - 1 do + table.insert(rowOffsets, centerWorkingWidth) end if overlapLast then - table.insert(rowOffsets, fieldWidth - (centerWorkingWidth + width * #rowOffsets)) + table.insert(rowOffsets, fieldWidth - 2 * outermostRowOffset - (centerWorkingWidth * (nRows - 2))) else - rowOffsets[2] = fieldWidth - (centerWorkingWidth + width * #rowOffsets) - table.insert(rowOffsets, width) + rowOffsets[2] = fieldWidth - 2 * outermostRowOffset - (centerWorkingWidth * (nRows - 2)) + table.insert(rowOffsets, centerWorkingWidth) end - end return rowOffsets end diff --git a/scripts/courseGenerator/test/CenterTest.lua b/scripts/courseGenerator/test/CenterTest.lua new file mode 100644 index 000000000..3757279b9 --- /dev/null +++ b/scripts/courseGenerator/test/CenterTest.lua @@ -0,0 +1,142 @@ +require('include') + +lu.EPS = 0.01 + +local function printRowOffsets(rowOffsets) + local y = 0 + for i, offset in ipairs(rowOffsets) do + y = y + offset + print(i, offset, y) + end +end + +local function createContext(headlandWidth, centerRowSpacing, evenRowDistribution) + local mockContext = { + evenRowDistribution = evenRowDistribution, + workingWidth = headlandWidth, + getHeadlandWorkingWidth = function() + return headlandWidth + end, + getCenterRowSpacing = function() + return centerRowSpacing + end + } + return mockContext +end + +function testRowDistributionExactMultiple() + local rowOffsets + local center = {context = createContext(5, 5, false), mayOverlapHeadland = true} + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, false) + lu.assertEquals(#rowOffsets, 9) + lu.assertAlmostEquals(rowOffsets[1], 5) + lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5) + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, false) + lu.assertEquals(#rowOffsets, 9) + lu.assertAlmostEquals(rowOffsets[1], 5) + lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5) + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 100, true) + lu.assertEquals(#rowOffsets, 19) + lu.assertAlmostEquals(rowOffsets[1], 5) + lu.assertAlmostEquals(rowOffsets[10], 5) + lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5) + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 100, false) + lu.assertEquals(#rowOffsets, 19) + lu.assertAlmostEquals(rowOffsets[1], 5) + lu.assertAlmostEquals(rowOffsets[10], 5) + lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5) +end + +function testRowDistributionGeneral() + local rowOffsets + local center = {context = createContext(5, 5, false), mayOverlapHeadland = true} + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 49, true) + lu.assertEquals(#rowOffsets, 9) + lu.assertAlmostEquals(rowOffsets[1], 5) + lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5) + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 49, false) + lu.assertEquals(#rowOffsets, 9) + lu.assertAlmostEquals(rowOffsets[1], 4) + lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5) + center.mayOverlapHeadland = false +end + +function testRowDistributionNarrow() + local rowOffsets + local center = {context = createContext(5, 5, false), mayOverlapHeadland = true} + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 10, true) + lu.assertEquals(#rowOffsets, 1) + lu.assertAlmostEquals(rowOffsets[1], 5) + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 7, true) + lu.assertEquals(#rowOffsets, 1) + lu.assertAlmostEquals(rowOffsets[1], 5) + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 7, false) + lu.assertEquals(#rowOffsets, 1) + lu.assertAlmostEquals(rowOffsets[1], 2) + -- calculated nRows will be 0, as we reduce the field width by a centimeter + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 5, false) + lu.assertEquals(#rowOffsets, 1) + lu.assertAlmostEquals(rowOffsets[1], 2.5) + center.mayOverlapHeadland = false + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 5, false) + lu.assertEquals(#rowOffsets, 1) + lu.assertAlmostEquals(rowOffsets[1], 2.5) +end + +function testRowDistributionNoOverlap() + local rowOffsets + local center = {context = createContext(5, 5, false), mayOverlapHeadland = false} + + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 49, true) + lu.assertEquals(#rowOffsets, 9) + lu.assertAlmostEquals(rowOffsets[1], 5) + lu.assertAlmostEquals(rowOffsets[2], 5) + lu.assertAlmostEquals(rowOffsets[#rowOffsets], 4) + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 49, false) + lu.assertEquals(#rowOffsets, 9) + lu.assertAlmostEquals(rowOffsets[1], 5) + lu.assertAlmostEquals(rowOffsets[2], 4) + lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5) + -- same with exact multiple + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, true) + lu.assertEquals(#rowOffsets, 9) + lu.assertAlmostEquals(rowOffsets[1], 5) + lu.assertAlmostEquals(rowOffsets[2], 5) + lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5) + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, false) + lu.assertEquals(#rowOffsets, 9) + lu.assertAlmostEquals(rowOffsets[1], 5) + lu.assertAlmostEquals(rowOffsets[2], 5) + lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5) +end + +function testRowDistributionMultiVehicleWithHeadland() + local rowOffsets + local center = {context = createContext(5, 10, false), mayOverlapHeadland = true} + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, true) + lu.assertEquals(#rowOffsets, 5) + lu.assertAlmostEquals(rowOffsets[1], 7.5) + lu.assertAlmostEquals(rowOffsets[2], 10) + lu.assertAlmostEquals(rowOffsets[#rowOffsets], 10) +end + +function testRowDistributionMultiVehicleNoHeadland() + local center = {context = createContext(5, 10, false), mayOverlapHeadland = false} + local rowOffsets + + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, true) + lu.assertEquals(#rowOffsets, 5) + lu.assertAlmostEquals(rowOffsets[1], 7.5) + lu.assertAlmostEquals(rowOffsets[2], 10) + lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5) + + rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, false) + lu.assertEquals(#rowOffsets, 5) + lu.assertAlmostEquals(rowOffsets[1], 7.5) + lu.assertAlmostEquals(rowOffsets[2], 5) + lu.assertAlmostEquals(rowOffsets[#rowOffsets], 10) + +end + + +os.exit(lu.LuaUnit.run())