Учет забронированных слотов при расчете комплексного плана
This commit is contained in:
+61
-16
@@ -2,6 +2,7 @@ package space.nstart.pcp.slots_service.model.satellite
|
|||||||
|
|
||||||
import ballistics.types.InitialConditions
|
import ballistics.types.InitialConditions
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||||
import space.nstart.pcp.slots_service.entity.BookedSlotEntity
|
import space.nstart.pcp.slots_service.entity.BookedSlotEntity
|
||||||
@@ -40,10 +41,19 @@ class TestSatelliteImpl(
|
|||||||
val selectedSlots = mutableListOf<PreparedCoverageSlot>()
|
val selectedSlots = mutableListOf<PreparedCoverageSlot>()
|
||||||
val cycleBegin = Duration.between(tnCalc, timeStart).toDays() / durationCalc
|
val cycleBegin = Duration.between(tnCalc, timeStart).toDays() / durationCalc
|
||||||
val cycleEnd = Duration.between(tnCalc, timeStop).toDays() / durationCalc
|
val cycleEnd = Duration.between(tnCalc, timeStop).toDays() / durationCalc
|
||||||
val booked = bookedSlots.map { BookedInfo(it.slot.slotId?:0, it.cycle) }
|
val activeBookedSlots = bookedSlots.filter { it.status in ACTIVE_BOOKED_STATUSES }
|
||||||
val bookedData = (
|
val booked = activeBookedSlots.map { BookedInfo(it.slot.slotId?:0, it.cycle) }
|
||||||
bookedSlots.map { BookedSlot(it.slot.tn, it.slot.tk,it.slot.roll, 0, it.cycle) }.toMutableList()
|
val bookedData = activeBookedSlots.map { bookedSlot ->
|
||||||
)
|
val offsetDays = bookedSlot.cycle * durationCalc
|
||||||
|
BookedSlot(
|
||||||
|
bookedSlot.slot.tn.plusDays(offsetDays),
|
||||||
|
bookedSlot.slot.tk.plusDays(offsetDays),
|
||||||
|
bookedSlot.slot.roll,
|
||||||
|
0,
|
||||||
|
bookedSlot.cycle
|
||||||
|
)
|
||||||
|
}.toMutableList()
|
||||||
|
val bookedIntervals = mergeBookedIntervals(bookedData.map { BookedInterval(it.t, it.tEnd) })
|
||||||
calculateChainLengths(bookedData)
|
calculateChainLengths(bookedData)
|
||||||
for (cycleN in cycleBegin..cycleEnd) {
|
for (cycleN in cycleBegin..cycleEnd) {
|
||||||
val currentSlots = slots.map { ent ->
|
val currentSlots = slots.map { ent ->
|
||||||
@@ -68,24 +78,27 @@ class TestSatelliteImpl(
|
|||||||
slot.tn >= timeStart && slot.tk <= timeStop &&
|
slot.tn >= timeStart && slot.tk <= timeStop &&
|
||||||
sign?.let { slot.revolutionSign == it.toString() } ?: true
|
sign?.let { slot.revolutionSign == it.toString() } ?: true
|
||||||
}.map {
|
}.map {
|
||||||
val band = bookedData.filter { booked ->
|
val isBooked = booked.any { booked -> booked.cycle == it.cycle && booked.id == it.slotId }
|
||||||
val dif = intersection(it.tn.minusSeconds(mmi), it.tk.plusSeconds(mmi), booked.t, booked.tEnd)
|
val insideBookedInterval = !isBooked && bookedIntervals.any { booked -> booked.contains(it.tn, it.tk) }
|
||||||
|
val band = if (insideBookedInterval) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
bookedData.any { booked ->
|
||||||
|
val dif = intersection(it.tn.minusSeconds(mmi), it.tk.plusSeconds(mmi), booked.t, booked.tEnd)
|
||||||
|
val gam = abs(it.roll - booked.roll)
|
||||||
|
|
||||||
val gam = abs(it.roll - booked.roll)
|
if (dif) {
|
||||||
|
!(gam < 0.01 && booked.neighbours < maxChainLength)
|
||||||
if (dif) {
|
} else {
|
||||||
if (gam < 0.01 && booked.neighbours < maxChainLength) {
|
|
||||||
// if (gam < 0.01) {
|
|
||||||
// booked.neighbours++
|
|
||||||
false
|
false
|
||||||
} else true
|
}
|
||||||
} else false
|
}
|
||||||
}
|
}
|
||||||
PreparedCoverageSlot(
|
PreparedCoverageSlot(
|
||||||
sourceSlotId = it.slotId ?: 0L,
|
sourceSlotId = it.slotId ?: 0L,
|
||||||
slot = it.toDTO(
|
slot = it.toDTO(
|
||||||
booked.count { booked -> booked.cycle == it.cycle && booked.id == it.slotId } > 0,
|
isBooked,
|
||||||
band.isNotEmpty()
|
band
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -95,6 +108,30 @@ class TestSatelliteImpl(
|
|||||||
return selectedSlots
|
return selectedSlots
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class BookedInterval(val start: LocalDateTime, val end: LocalDateTime) {
|
||||||
|
fun contains(slotStart: LocalDateTime, slotEnd: LocalDateTime): Boolean =
|
||||||
|
!slotStart.isBefore(start) && !slotEnd.isAfter(end)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mergeBookedIntervals(intervals: List<BookedInterval>): List<BookedInterval> {
|
||||||
|
if (intervals.isEmpty()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
val merged = mutableListOf<BookedInterval>()
|
||||||
|
intervals.sortedBy { it.start }.forEach { interval ->
|
||||||
|
val last = merged.lastOrNull()
|
||||||
|
if (last != null && !interval.start.isAfter(last.end)) {
|
||||||
|
merged[merged.lastIndex] = BookedInterval(
|
||||||
|
start = minOf(last.start, interval.start),
|
||||||
|
end = maxOf(last.end, interval.end)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
merged += interval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
private fun calculateChainLengths(slots: MutableList<BookedSlot>) {
|
private fun calculateChainLengths(slots: MutableList<BookedSlot>) {
|
||||||
if (slots.isEmpty())
|
if (slots.isEmpty())
|
||||||
return
|
return
|
||||||
@@ -127,4 +164,12 @@ class TestSatelliteImpl(
|
|||||||
slots[j].neighbours = lastChainLength
|
slots[j].neighbours = lastChainLength
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val ACTIVE_BOOKED_STATUSES = setOf(
|
||||||
|
BookedSlotStatus.BOOKED.name,
|
||||||
|
BookedSlotStatus.PROCESSED.name
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
@@ -2,6 +2,7 @@ package space.nstart.pcp.slots_service.service
|
|||||||
|
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
|
||||||
import space.nstart.pcp.slots_service.model.satellite.AbstractSatellite
|
import space.nstart.pcp.slots_service.model.satellite.AbstractSatellite
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
@@ -71,6 +72,15 @@ internal object OccupiedIntervalPrefilter {
|
|||||||
occupiedIntervals: List<OccupiedIntervalDTO>,
|
occupiedIntervals: List<OccupiedIntervalDTO>,
|
||||||
satellite: AbstractSatellite
|
satellite: AbstractSatellite
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
val insideStrictReservedInterval = candidate.state != SlotStatus.BOOKED &&
|
||||||
|
occupiedIntervals.any { occupied ->
|
||||||
|
isStrictReservedSource(occupied.source) &&
|
||||||
|
isInside(candidate.tn, candidate.tk, occupied.startTime, occupied.endTime)
|
||||||
|
}
|
||||||
|
if (insideStrictReservedInterval) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
val conflictingDifferentRoll = occupiedIntervals.any { occupied ->
|
val conflictingDifferentRoll = occupiedIntervals.any { occupied ->
|
||||||
!sameRoll(candidate.roll, occupied.roll) &&
|
!sameRoll(candidate.roll, occupied.roll) &&
|
||||||
intersectsWithMmi(candidate.tn, candidate.tk, occupied.startTime, occupied.endTime, satellite.mmi)
|
intersectsWithMmi(candidate.tn, candidate.tk, occupied.startTime, occupied.endTime, satellite.mmi)
|
||||||
@@ -101,5 +111,15 @@ internal object OccupiedIntervalPrefilter {
|
|||||||
mmi: Long
|
mmi: Long
|
||||||
): Boolean = start1 <= end2.plusSeconds(mmi) && end1 >= start2.minusSeconds(mmi)
|
): Boolean = start1 <= end2.plusSeconds(mmi) && end1 >= start2.minusSeconds(mmi)
|
||||||
|
|
||||||
|
private fun isInside(
|
||||||
|
start: java.time.LocalDateTime,
|
||||||
|
end: java.time.LocalDateTime,
|
||||||
|
containerStart: java.time.LocalDateTime,
|
||||||
|
containerEnd: java.time.LocalDateTime
|
||||||
|
): Boolean = !start.isBefore(containerStart) && !end.isAfter(containerEnd)
|
||||||
|
|
||||||
|
private fun isStrictReservedSource(source: String?): Boolean =
|
||||||
|
source.equals("SLOTS", ignoreCase = true)
|
||||||
|
|
||||||
private fun sameRoll(left: Double, right: Double): Boolean = abs(left - right) <= ROLL_EPSILON
|
private fun sameRoll(left: Double, right: Double): Boolean = abs(left - right) <= ROLL_EPSILON
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -1686,16 +1686,26 @@ class SlotService {
|
|||||||
val satellite = batchContext.selectedSatellitesById[satelliteId] ?: return@forEach
|
val satellite = batchContext.selectedSatellitesById[satelliteId] ?: return@forEach
|
||||||
val preparedSlotsBySourceSlotId =
|
val preparedSlotsBySourceSlotId =
|
||||||
batchContext.preparedSlotsBySatelliteIdAndSourceSlotId[satelliteId].orEmpty()
|
batchContext.preparedSlotsBySatelliteIdAndSourceSlotId[satelliteId].orEmpty()
|
||||||
val preparedSlots = sourceSlotIdsForSatellite.flatMap { slotId ->
|
val preparedSlotsIncludingBlocked = sourceSlotIdsForSatellite.flatMap { slotId ->
|
||||||
preparedSlotsBySourceSlotId[slotId].orEmpty()
|
preparedSlotsBySourceSlotId[slotId].orEmpty()
|
||||||
}.map { it.slot }
|
}.map { it.slot }
|
||||||
preparedSlotsBeforeOccupiedFilter += preparedSlots.size
|
preparedSlotsBeforeOccupiedFilter += preparedSlotsIncludingBlocked.size
|
||||||
|
|
||||||
|
// BUSY слоты возникают, в частности, когда слот по времени попадает внутрь
|
||||||
|
// активного забронированного интервала. Такие слоты не должны попадать
|
||||||
|
// в дальнейший расчет покрытия: booked-слоты учитываются как BOOKED,
|
||||||
|
// а все остальные слоты внутри их времени считаются заблокированными.
|
||||||
|
val preparedSlots = preparedSlotsIncludingBlocked.filter { slot ->
|
||||||
|
slot.state == SlotStatus.AVAILABLE || slot.state == SlotStatus.BOOKED
|
||||||
|
}
|
||||||
|
val blockedByBookedSlots = preparedSlotsIncludingBlocked.size - preparedSlots.size
|
||||||
|
|
||||||
val filteredSlots = OccupiedIntervalPrefilter.filterCandidates(
|
val filteredSlots = OccupiedIntervalPrefilter.filterCandidates(
|
||||||
candidateSlots = preparedSlots,
|
candidateSlots = preparedSlots,
|
||||||
occupiedIntervals = batchContext.occupiedIntervalsBySatelliteId[satelliteId].orEmpty(),
|
occupiedIntervals = batchContext.occupiedIntervalsBySatelliteId[satelliteId].orEmpty(),
|
||||||
satellite = satellite
|
satellite = satellite
|
||||||
)
|
)
|
||||||
occupiedFilteredSlots += preparedSlots.size - filteredSlots.size
|
occupiedFilteredSlots += blockedByBookedSlots + (preparedSlots.size - filteredSlots.size)
|
||||||
candidateSlotsBeforeSunFilter += filteredSlots.size
|
candidateSlotsBeforeSunFilter += filteredSlots.size
|
||||||
val sunFilterResult = filterBySunAngle(filteredSlots, sun)
|
val sunFilterResult = filterBySunAngle(filteredSlots, sun)
|
||||||
sunFilteredSlots += sunFilterResult.filteredCount
|
sunFilteredSlots += sunFilterResult.filteredCount
|
||||||
|
|||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package space.nstart.pcp.slots_service.model.satellite
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
|
||||||
|
import space.nstart.pcp.slots_service.entity.BookedSlotEntity
|
||||||
|
import space.nstart.pcp.slots_service.entity.SlotEntity
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class TestSatelliteImplBookedBlockingTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `non-booked slot inside active booked interval is marked busy and exact booked slot stays booked`() {
|
||||||
|
val baseTime = LocalDateTime.of(2026, 5, 21, 0, 0)
|
||||||
|
val satellite = TestSatelliteImpl(
|
||||||
|
id = 1L,
|
||||||
|
tnCalc = baseTime,
|
||||||
|
durationCalc = 1,
|
||||||
|
maxChainLength = 3,
|
||||||
|
maxSurveyDuration = 300,
|
||||||
|
mmi = 10
|
||||||
|
)
|
||||||
|
val bookedRawSlot = slot(slotId = 1L, slotNum = 1L, baseTime = baseTime, startSecond = 0, endSecond = 10)
|
||||||
|
val candidateInsideBooked = slot(slotId = 2L, slotNum = 2L, baseTime = baseTime, startSecond = 2, endSecond = 8)
|
||||||
|
val booked = BookedSlotEntity(slot = bookedRawSlot, cycle = 0).apply {
|
||||||
|
status = BookedSlotStatus.PROCESSED.name
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = satellite.prepareCoverageSlots(
|
||||||
|
slots = listOf(bookedRawSlot, candidateInsideBooked),
|
||||||
|
bookedSlots = listOf(booked),
|
||||||
|
timeStart = baseTime,
|
||||||
|
timeStop = baseTime.plusSeconds(20),
|
||||||
|
sign = null,
|
||||||
|
states = null
|
||||||
|
).map { it.slot }.associateBy { it.slotNumber }
|
||||||
|
|
||||||
|
assertEquals(SlotStatus.BOOKED, result.getValue(1L).state)
|
||||||
|
assertEquals(SlotStatus.BUSY, result.getValue(2L).state)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun slot(
|
||||||
|
slotId: Long,
|
||||||
|
slotNum: Long,
|
||||||
|
baseTime: LocalDateTime,
|
||||||
|
startSecond: Long,
|
||||||
|
endSecond: Long
|
||||||
|
) = SlotEntity(
|
||||||
|
slotId = slotId,
|
||||||
|
slotNum = slotNum,
|
||||||
|
cycle = 0,
|
||||||
|
satelliteId = 1L,
|
||||||
|
coveringType = 0,
|
||||||
|
tn = baseTime.plusSeconds(startSecond),
|
||||||
|
tk = baseTime.plusSeconds(endSecond),
|
||||||
|
roll = 10.0,
|
||||||
|
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||||
|
revolution = 1L,
|
||||||
|
revolutionSign = "ASC"
|
||||||
|
)
|
||||||
|
}
|
||||||
+49
-4
@@ -81,6 +81,48 @@ class OccupiedIntervalPrefilterTest {
|
|||||||
assertEquals(listOf(candidate), filtered)
|
assertEquals(listOf(candidate), filtered)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `available candidate inside reserved slots interval is rejected even with same roll`() {
|
||||||
|
val satellite = testSatellite(maxSurveyDuration = 120, mmi = 5)
|
||||||
|
val candidate = candidateSlot(roll = 10.0, startOffsetSeconds = 10, endOffsetSeconds = 20)
|
||||||
|
val occupied = listOf(
|
||||||
|
occupiedInterval(
|
||||||
|
roll = 10.0,
|
||||||
|
startOffsetSeconds = 0,
|
||||||
|
endOffsetSeconds = 30,
|
||||||
|
source = "SLOTS"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val filtered = OccupiedIntervalPrefilter.filterCandidates(listOf(candidate), occupied, satellite)
|
||||||
|
|
||||||
|
assertTrue(filtered.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `booked candidate inside reserved slots interval is kept for coverage accounting`() {
|
||||||
|
val satellite = testSatellite(maxSurveyDuration = 120, mmi = 5)
|
||||||
|
val candidate = candidateSlot(
|
||||||
|
roll = 10.0,
|
||||||
|
startOffsetSeconds = 10,
|
||||||
|
endOffsetSeconds = 20,
|
||||||
|
state = SlotStatus.BOOKED
|
||||||
|
)
|
||||||
|
val occupied = listOf(
|
||||||
|
occupiedInterval(
|
||||||
|
roll = 10.0,
|
||||||
|
startOffsetSeconds = 0,
|
||||||
|
endOffsetSeconds = 30,
|
||||||
|
source = "SLOTS"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val filtered = OccupiedIntervalPrefilter.filterCandidates(listOf(candidate), occupied, satellite)
|
||||||
|
|
||||||
|
assertEquals(listOf(candidate), filtered)
|
||||||
|
}
|
||||||
|
|
||||||
private fun testSatellite(maxSurveyDuration: Int, mmi: Long): AbstractSatellite =
|
private fun testSatellite(maxSurveyDuration: Int, mmi: Long): AbstractSatellite =
|
||||||
object : AbstractSatellite(id = 22, maxSurveyDuration = maxSurveyDuration, mmi = mmi) {
|
object : AbstractSatellite(id = 22, maxSurveyDuration = maxSurveyDuration, mmi = mmi) {
|
||||||
override fun prepareCoverageSlots(
|
override fun prepareCoverageSlots(
|
||||||
@@ -96,26 +138,29 @@ class OccupiedIntervalPrefilterTest {
|
|||||||
private fun occupiedInterval(
|
private fun occupiedInterval(
|
||||||
roll: Double,
|
roll: Double,
|
||||||
startOffsetSeconds: Long,
|
startOffsetSeconds: Long,
|
||||||
endOffsetSeconds: Long
|
endOffsetSeconds: Long,
|
||||||
|
source: String = "TEST"
|
||||||
) = OccupiedIntervalDTO(
|
) = OccupiedIntervalDTO(
|
||||||
satelliteId = 22,
|
satelliteId = 22,
|
||||||
startTime = baseTime().plusSeconds(startOffsetSeconds),
|
startTime = baseTime().plusSeconds(startOffsetSeconds),
|
||||||
endTime = baseTime().plusSeconds(endOffsetSeconds),
|
endTime = baseTime().plusSeconds(endOffsetSeconds),
|
||||||
roll = roll,
|
roll = roll,
|
||||||
source = "TEST"
|
source = source
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun candidateSlot(
|
private fun candidateSlot(
|
||||||
roll: Double,
|
roll: Double,
|
||||||
startOffsetSeconds: Long,
|
startOffsetSeconds: Long,
|
||||||
endOffsetSeconds: Long
|
endOffsetSeconds: Long,
|
||||||
|
state: SlotStatus = SlotStatus.AVAILABLE
|
||||||
) = SlotDTO(
|
) = SlotDTO(
|
||||||
cycle = 0,
|
cycle = 0,
|
||||||
satelliteId = 22,
|
satelliteId = 22,
|
||||||
tn = baseTime().plusSeconds(startOffsetSeconds),
|
tn = baseTime().plusSeconds(startOffsetSeconds),
|
||||||
tk = baseTime().plusSeconds(endOffsetSeconds),
|
tk = baseTime().plusSeconds(endOffsetSeconds),
|
||||||
roll = roll,
|
roll = roll,
|
||||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
|
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||||
|
state = state
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun baseTime(): LocalDateTime = LocalDateTime.of(2026, 3, 26, 10, 0, 0)
|
private fun baseTime(): LocalDateTime = LocalDateTime.of(2026, 3, 26, 10, 0, 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user