исправлен расчет витка для tle

This commit is contained in:
emelianov
2026-06-16 12:26:24 +03:00
parent 1790192e06
commit 6938700218
4 changed files with 205 additions and 2 deletions
@@ -23,7 +23,7 @@ class SurveyContourCalculator(
fun calculate(result: AngularMotionResult, y: Double,z: Double,focus: Double): String {
val sourcePoints = result.points
val sourcePoints = decimateContourSourcePoints(result.points)
require(sourcePoints.size >= MIN_CONTOUR_POINTS) {
"Для построения контура съемки требуется не менее $MIN_CONTOUR_POINTS точек ПУУД"
}
@@ -62,6 +62,43 @@ class SurveyContourCalculator(
return toWkt(shell)
}
private fun decimateContourSourcePoints(points: List<AngularMotionPoint>): List<AngularMotionPoint> {
if (points.size <= MIN_CONTOUR_POINTS) {
return points
}
val startTime = points.first().t
val stopTime = points.last().t
val duration = stopTime - startTime
if (duration <= 0.0) {
return listOf(points.first(), points.last()).distinct()
}
val step = maxOf(
DEFAULT_CONTOUR_SOURCE_STEP_SEC,
duration / (MAX_CONTOUR_BOUNDARY_POINTS - 1),
)
val selected = ArrayList<AngularMotionPoint>(MAX_CONTOUR_BOUNDARY_POINTS)
selected += points.first()
var nextTime = startTime + step
for (index in 1 until points.lastIndex) {
val point = points[index]
if (point.t + TIME_EPS >= nextTime) {
selected += point
nextTime += step
}
}
if (selected.last() !== points.last()) {
selected += points.last()
}
return selected
}
private fun toWkt(points: List<SurveyContourPoint>): String {
val coordinates = points.joinToString(", ") { point ->
String.format(Locale.US, "%.8f %.8f", point.longitudeDeg, point.latitudeDeg)
@@ -85,5 +122,8 @@ class SurveyContourCalculator(
private companion object {
const val MIN_CONTOUR_POINTS = 2
const val DEFAULT_CONTOUR_SOURCE_STEP_SEC = 2.0
const val MAX_CONTOUR_BOUNDARY_POINTS = 10
const val TIME_EPS = 1.0e-9
}
}
@@ -10,15 +10,21 @@ import org.nstart.dep265.tletools.zeptomoby.orbit.Satellite
import java.util.Calendar
import java.util.GregorianCalendar
import java.util.TimeZone
import kotlin.math.floor
import kotlin.math.min
class TLEStepper(str1: String, str2: String, earthType: EarthType) : AbstractStepper {
val astro = AstronomerJ2000(earthType)
val tleParser: TLE = TLE("", str1, str2)
val satellite: Satellite = Satellite(tleParser)
val baseEpoch: Double
private val nextAscendingNodeAfterEpoch: Double
private val ascendingNodeCache = mutableMapOf<Int, Double>()
init {
baseEpoch = extractUTCMillis(satellite) / 1000.0 + 10800
nextAscendingNodeAfterEpoch = findNextAscendingNodeAfterEpoch()
ascendingNodeCache[0] = nextAscendingNodeAfterEpoch
}
override fun clear() {
@@ -68,7 +74,7 @@ class TLEStepper(str1: String, str2: String, earthType: EarthType) : AbstractSte
val ask =
OrbitalPoint(
t,
satellite.orbit.orbitNum + (dt / satellite.orbit.period).toInt(),
calculateRevolution(t),
Vector3D(
pos.position.x * 1000.0,
pos.position.y * 1000.0,
@@ -87,6 +93,86 @@ class TLEStepper(str1: String, str2: String, earthType: EarthType) : AbstractSte
}
}
private fun calculateRevolution(t: Double): Int {
var nodeIndex = floor((t - nextAscendingNodeAfterEpoch) / satellite.orbit.period).toInt()
while (t < ascendingNode(nodeIndex)) {
--nodeIndex
}
while (t >= ascendingNode(nodeIndex + 1)) {
++nodeIndex
}
return satellite.orbit.orbitNum + nodeIndex + 1
}
private fun ascendingNode(index: Int): Double =
ascendingNodeCache.getOrPut(index) {
val estimatedNode = nextAscendingNodeAfterEpoch + index * satellite.orbit.period
findAscendingNodeNear(estimatedNode)
}
private fun findNextAscendingNodeAfterEpoch(): Double {
val scanStep = min(60.0, satellite.orbit.period / 64.0)
var left = baseEpoch
var leftZ = eciZ(left)
var right = left + scanStep
val scanLimit = baseEpoch + satellite.orbit.period * 2.0
while (right <= scanLimit) {
val rightZ = eciZ(right)
if (leftZ < 0.0 && rightZ >= 0.0) {
return refineAscendingNode(left, right)
}
left = right
leftZ = rightZ
right += scanStep
}
return baseEpoch + satellite.orbit.period
}
private fun findAscendingNodeNear(estimatedNode: Double): Double {
val halfPeriod = satellite.orbit.period / 2.0
val scanStep = min(60.0, satellite.orbit.period / 64.0)
var left = estimatedNode - halfPeriod
var leftZ = eciZ(left)
var right = left + scanStep
val scanLimit = estimatedNode + halfPeriod
while (right <= scanLimit) {
val rightZ = eciZ(right)
if (leftZ < 0.0 && rightZ >= 0.0) {
return refineAscendingNode(left, right)
}
left = right
leftZ = rightZ
right += scanStep
}
return estimatedNode
}
private fun refineAscendingNode(
intervalStart: Double,
intervalStop: Double,
): Double {
var left = intervalStart
var right = intervalStop
repeat(60) {
val middle = (left + right) / 2.0
if (eciZ(middle) < 0.0) {
left = middle
} else {
right = middle
}
}
return (left + right) / 2.0
}
private fun eciZ(t: Double): Double =
satellite.positionEci((t - baseEpoch) / 60.0).position.z
override fun calculate(
t: Double,
p: OrbitalPoint,
@@ -0,0 +1,33 @@
package ballistics.orbitalPoints.timeStepper
import ballistics.types.EarthType
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.LocalDateTime
import java.time.ZoneOffset
internal class TLEStepperTest {
@Test
fun `calculate keeps TLE revolution at epoch`() {
val stepper = TLEStepper(issTleFirst, issTleSecond, EarthType.PZ90d02)
val point = stepper.calculate(stepper.baseEpoch)
assertEquals(40945, point.vit)
}
@Test
fun `calculate increments revolution at ascending node instead of elapsed period from epoch`() {
val stepper = TLEStepper(issTleFirst, issTleSecond, EarthType.PZ90d02)
val firstPass = stepper.calculate(LocalDateTime.of(2026, 6, 16, 14, 19, 50).toEpochSecond(ZoneOffset.UTC).toDouble())
val secondPass = stepper.calculate(LocalDateTime.of(2026, 6, 16, 15, 56, 14).toEpochSecond(ZoneOffset.UTC).toDouble())
assertEquals(firstPass.vit + 1, secondPass.vit)
}
private companion object {
const val issTleFirst = "1 25994U 99068A 26167.13049375 .00000315 00000-0 73126-4 0 9997"
const val issTleSecond = "2 25994 97.9457 216.7186 0001501 193.3628 244.8059 14.61102329409455"
}
}
@@ -10,6 +10,7 @@ import ballistics.types.OrbitalPoint
import ballistics.types.PPI
import ballistics.types.TLE
import ballistics.utils.math.Vector3D
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.time.LocalDateTime
@@ -17,6 +18,49 @@ import java.time.ZoneOffset
import kotlin.math.PI
internal class ZRVStepperCalculatorTest {
@Test
fun `calculate keeps different revolutions for consecutive TLE visibility zones`() {
val ballistics = Ballistics()
val tle = TLE(
"1 25994U 99068A 26167.13049375 .00000315 00000-0 73126-4 0 9997",
"2 25994 97.9457 216.7186 0001501 193.3628 244.8059 14.61102329409455",
)
val timeStart = LocalDateTime.of(2026, 6, 16, 10, 41, 50, 189_000_000).toEpochSecond(ZoneOffset.UTC).toDouble()
val timeStop = LocalDateTime.of(2026, 6, 18, 0, 41, 50, 189_000_000).toEpochSecond(ZoneOffset.UTC).toDouble()
assertEquals(BallisticsError.OK, ballistics.calculateOrbPoints(tle, timeStart, timeStop))
assertEquals(
BallisticsError.OK,
ballistics.calculateZRV(
listOf(
PPI(
1,
0,
69.038863 * PI / 180.0,
32.861553 * PI / 180.0,
124.67,
5 * PI / 180.0,
90 * PI / 180.0,
),
),
timeStart,
timeStop,
),
)
val zones = ballistics.zrv.toList()
val firstZone = zones.first {
it.zoneIn.t >= LocalDateTime.of(2026, 6, 16, 14, 0).toEpochSecond(ZoneOffset.UTC) &&
it.zoneIn.t < LocalDateTime.of(2026, 6, 16, 15, 0).toEpochSecond(ZoneOffset.UTC)
}
val secondZone = zones.first {
it.zoneIn.t >= LocalDateTime.of(2026, 6, 16, 15, 0).toEpochSecond(ZoneOffset.UTC) &&
it.zoneIn.t < LocalDateTime.of(2026, 6, 16, 17, 0).toEpochSecond(ZoneOffset.UTC)
}
assertEquals(firstZone.vit + 1, secondZone.vit)
}
@Test
fun calculate() {
val r = Ballistics()