package spk import ( "encoding/binary" "errors" "fmt" "math" "os" "sync" "b612.me/astro/internal/ephem" ) const ( dafRecordBytes = 1024 dafFileHeadSize = 1024 dafND = 2 dafNI = 6 dafSummaryWords = dafND + (dafNI+1)/2 dafSummaryBytes = dafSummaryWords * 8 dafHeaderWords = 3 dafWordBytes = 8 spkJ2000 = 2451545.0 spkSecondsDay = 86400.0 spkCacheRecords = 16 ) var ( // ErrInvalidFile 无效 SPK 文件 / invalid SPK file. ErrInvalidFile = errors.New("spk: invalid DAF/SPK file") // ErrUnsupportedFrame 不支持的参考框架 / unsupported reference frame. ErrUnsupportedFrame = errors.New("spk: unsupported reference frame") // ErrUnsupportedType 不支持的段类型 / unsupported segment type. ErrUnsupportedType = errors.New("spk: unsupported SPK data type") // ErrClosed 文件已关闭 / file is closed. ErrClosed = errors.New("spk: file is closed") errInvalidDAF = ErrInvalidFile errClosed = ErrClosed ) // Body NAIF 天体编号 / NAIF body identifier. type Body = ephem.Body const ( // SSB 太阳系质心 / solar-system barycenter. SSB = ephem.SSB // Mercury 水星 / Mercury. Mercury = ephem.Mercury // Venus 金星 / Venus. Venus = ephem.Venus // EMB 地月质心 / Earth-Moon barycenter. EMB = ephem.EMB // Mars 火星 / Mars. Mars = ephem.Mars // Jupiter 木星 / Jupiter. Jupiter = ephem.Jupiter // Saturn 土星 / Saturn. Saturn = ephem.Saturn // Uranus 天王星 / Uranus. Uranus = ephem.Uranus // Neptune 海王星 / Neptune. Neptune = ephem.Neptune // Sun 太阳 / the Sun. Sun = ephem.Sun // Moon 月球 / the Moon. Moon = ephem.Moon // Earth 地球 / Earth. Earth = ephem.Earth ) // Segment 描述一个支持的 SPK 段 / describes a supported SPK segment. type Segment struct { Target Body Center Body Frame int Type int Start float64 End float64 } type summarySegment struct { target Body center Body frame int dataType int startET float64 endET float64 startWord int64 endWord int64 initET float64 interval float64 recordSize int records int maxDim int distanceScale float64 timeScale float64 initJD float64 initFR float64 epochStartWord int64 directoryCount int cacheMu sync.Mutex cache map[int][]float64 cacheOrder []int owner *File } // File 只读 SPK 文件 / read-only SPK file. type File struct { mu sync.RWMutex file *os.File order binary.ByteOrder segments []*summarySegment known map[Body]bool closed bool size int64 } // Open 打开并解析 SPK 文件 / opens and indexes an SPK file. func Open(path string) (*File, error) { f, err := os.Open(path) if err != nil { return nil, err } spk := &File{file: f, known: make(map[Body]bool)} stat, err := f.Stat() if err != nil { _ = f.Close() return nil, err } spk.size = stat.Size() if err := spk.index(); err != nil { _ = f.Close() return nil, err } return spk, nil } // Load Open 的别名 / alias for Open. func Load(path string) (*File, error) { return Open(path) } // Close 关闭文件 / closes the file. func (s *File) Close() error { s.mu.Lock() defer s.mu.Unlock() if s.closed { return nil } s.closed = true return s.file.Close() } // Segments 返回支持段的快照 / returns a snapshot of supported segments. func (s *File) Segments() []Segment { s.mu.RLock() defer s.mu.RUnlock() result := make([]Segment, 0, len(s.segments)) for _, segment := range s.segments { result = append(result, Segment{ Target: segment.target, Center: segment.center, Frame: segment.frame, Type: segment.dataType, Start: spkJ2000 + segment.startET/spkSecondsDay, End: spkJ2000 + segment.endET/spkSecondsDay, }) } return result } // Coverage 返回天体覆盖的 TDB 儒略日范围 / returns the body's TDB Julian-date coverage. func (s *File) Coverage(target Body) (start, end float64, ok bool) { s.mu.RLock() defer s.mu.RUnlock() if s.closed { return 0, 0, false } for _, segment := range s.segments { if segment.target != target || segment.frame != 1 { continue } segmentStart := spkJ2000 + segment.startET/spkSecondsDay segmentEnd := spkJ2000 + segment.endET/spkSecondsDay if !ok || segmentStart < start { start = segmentStart } if !ok || segmentEnd > end { end = segmentEnd } ok = true } return start, end, ok } // Bodies 返回文件中出现的天体编号 / returns body identifiers present in the file. func (s *File) Bodies() []Body { s.mu.RLock() defer s.mu.RUnlock() if s.closed { return nil } result := make([]Body, 0, len(s.known)) for body := range s.known { result = append(result, body) } return result } // State 返回目标相对中心的 ICRF 状态,速度为 km/日 / returns an ICRF state, with velocity in km/day. func (s *File) State(target, center Body, jdTDB float64) (pos, vel [3]float64, err error) { s.mu.RLock() defer s.mu.RUnlock() if s.closed { return pos, vel, errClosed } if math.IsNaN(jdTDB) || math.IsInf(jdTDB, 0) { return pos, vel, ephem.ErrOutOfRange } if target == center { return pos, vel, nil } return s.composeState(target, center, jdTDB) } func (s *File) index() error { header := make([]byte, dafFileHeadSize) if _, err := s.file.ReadAt(header, 0); err != nil { return err } if string(header[:8]) != "DAF/SPK " { return errInvalidDAF } order, err := byteOrderFor(string(header[88:96])) if err != nil { return err } s.order = order if int32Value(s.order, header, 8) != dafND || int32Value(s.order, header, 12) != dafNI { return fmt.Errorf("%w: ND/NI must be 2/6", errInvalidDAF) } first := int(int32Value(s.order, header, 76)) last := int(int32Value(s.order, header, 80)) if first < 2 || last < 2 || int64(first)*dafRecordBytes > s.size || int64(last)*dafRecordBytes > s.size { return fmt.Errorf("%w: invalid summary record range", errInvalidDAF) } seen := make(map[int]bool) previous := 0 supported := 0 for record := first; record != 0; { if seen[record] { return fmt.Errorf("%w: cyclic summary record chain", errInvalidDAF) } seen[record] = true data, err := s.readRecord(record) if err != nil { return err } nextValue := float64Value(s.order, data[0:8]) previousValue := float64Value(s.order, data[8:16]) countValue := float64Value(s.order, data[16:24]) if !validInteger(nextValue, 0, s.size/dafRecordBytes) || previousValue != float64(previous) || !validInteger(countValue, 0, (dafRecordBytes-24)/dafSummaryBytes) { return fmt.Errorf("%w: invalid summary controls", errInvalidDAF) } next, count := int(nextValue), int(countValue) if next == 0 && record != last { return fmt.Errorf("%w: final summary disagrees with backward pointer", errInvalidDAF) } for index := 0; index < count; index++ { base := dafHeaderWords*8 + index*dafSummaryBytes segment, err := s.parseSummary(data[base : base+dafSummaryBytes]) if err != nil { return err } if segment.frame == 1 && (segment.dataType == 2 || segment.dataType == 3 || segment.dataType == 20 || segment.dataType == 21) { if err := segment.loadMeta(s); err != nil { return err } supported++ } else { continue } s.segments = append(s.segments, segment) s.known[segment.target] = true s.known[segment.center] = true } previous, record = record, next } if supported == 0 { return fmt.Errorf("%w: no supported J2000 segments", errInvalidDAF) } return nil } func (s *File) parseSummary(data []byte) (*summarySegment, error) { if len(data) != dafSummaryBytes { return nil, errInvalidDAF } segment := &summarySegment{ startET: float64Value(s.order, data[0:8]), endET: float64Value(s.order, data[8:16]), target: Body(int32Value(s.order, data, 16)), center: Body(int32Value(s.order, data, 20)), frame: int(int32Value(s.order, data, 24)), dataType: int(int32Value(s.order, data, 28)), startWord: int64(int32Value(s.order, data, 32)), endWord: int64(int32Value(s.order, data, 36)), cache: make(map[int][]float64), owner: s, } if !finite(segment.startET) || !finite(segment.endET) || segment.endET < segment.startET || segment.startWord < 1 || segment.endWord < segment.startWord || segment.endWord > s.size/dafWordBytes { return nil, fmt.Errorf("%w: invalid segment summary", errInvalidDAF) } return segment, nil } func (segment *summarySegment) loadMeta(s *File) error { if segment.endWord-segment.startWord+1 < 4 { return fmt.Errorf("%w: segment trailer is missing", errInvalidDAF) } if segment.dataType == 20 { trailer, err := s.readWords(segment.endWord-6, 7) if err != nil { return err } if !finitePositive(trailer[0]) || !finitePositive(trailer[1]) || !finite(trailer[2]) || math.Abs(trailer[3]) >= 1 || !finitePositive(trailer[4]) || !validInteger(trailer[5], 6, segment.endWord-segment.startWord) || !validInteger(trailer[6], 1, segment.endWord-segment.startWord) { return fmt.Errorf("%w: invalid type 20 trailer", errInvalidDAF) } segment.distanceScale = trailer[0] segment.timeScale = trailer[1] segment.initJD = trailer[2] segment.initFR = trailer[3] segment.interval = trailer[4] segment.recordSize = int(trailer[5]) segment.records = int(trailer[6]) wordCount := segment.endWord - segment.startWord - 6 if int64(segment.recordSize)*int64(segment.records) != wordCount || segment.recordSize < 6 || segment.recordSize%3 != 0 { return fmt.Errorf("%w: invalid type 20 record size", errInvalidDAF) } return nil } if segment.dataType == 21 { trailer, err := s.readWords(segment.endWord-1, 2) if err != nil { return err } if !validInteger(trailer[0], 1, 25) || !validInteger(trailer[1], 1, segment.endWord-segment.startWord) { return fmt.Errorf("%w: invalid type 21 trailer", errInvalidDAF) } segment.maxDim = int(trailer[0]) segment.recordSize = 4*segment.maxDim + 11 segment.records = int(trailer[1]) segment.epochStartWord = segment.startWord + int64(segment.recordSize*segment.records) segment.directoryCount = segment.records / 100 wordCount := segment.endWord - segment.startWord + 1 want := int64(segment.recordSize*segment.records + segment.records + segment.directoryCount + 2) if want != wordCount { return fmt.Errorf("%w: invalid type 21 segment size", errInvalidDAF) } return nil } trailerWords := segment.endWord - 3 trailer, err := s.readWords(trailerWords, 4) if err != nil { return err } segment.initET = trailer[0] segment.interval = trailer[1] wordCount := segment.endWord - segment.startWord - 3 if !finite(segment.initET) || !finitePositive(segment.interval) || !validInteger(trailer[2], 5, wordCount) || !validInteger(trailer[3], 1, wordCount) { return fmt.Errorf("%w: invalid type %d trailer", errInvalidDAF, segment.dataType) } segment.recordSize = int(trailer[2]) segment.records = int(trailer[3]) if int64(segment.recordSize)*int64(segment.records) != wordCount { return fmt.Errorf("%w: invalid type %d trailer", errInvalidDAF, segment.dataType) } components := 3 if segment.dataType == 3 { components = 6 } if (segment.recordSize-2)%components != 0 || (segment.recordSize-2)/components < 1 { return fmt.Errorf("%w: invalid type %d record size %d", errInvalidDAF, segment.dataType, segment.recordSize) } return nil } func chosenSegment(segment *summarySegment) (*summarySegment, error) { if segment == nil { return nil, ephem.ErrOutOfRange } if segment.frame != 1 { return nil, ErrUnsupportedFrame } if segment.dataType != 2 && segment.dataType != 3 && segment.dataType != 20 && segment.dataType != 21 { return nil, ErrUnsupportedType } return segment, nil } type stateEdge struct { to Body segment *summarySegment reverse bool } func (s *File) composeState(target, center Body, jdTDB float64) (pos, vel [3]float64, err error) { if !s.known[target] || !s.known[center] { return pos, vel, ephem.ErrUnsupportedBody } // 一次倒序扫描求出每个目标在 jdTDB 处覆盖它的最后一段,后加载的段优先。 chosen := make(map[Body]*summarySegment) for index := len(s.segments) - 1; index >= 0; index-- { segment := s.segments[index] if _, ok := chosen[segment.target]; ok { continue } if jdTDB >= spkJ2000+segment.startET/spkSecondsDay && jdTDB <= spkJ2000+segment.endET/spkSecondsDay { chosen[segment.target] = segment } } graph := make(map[Body][]stateEdge) selected := make(map[Body]bool) for index := len(s.segments) - 1; index >= 0; index-- { segment := s.segments[index] if selected[segment.target] { continue } current, selectErr := chosenSegment(chosen[segment.target]) if selectErr != nil { // 覆盖缺口留到 BFS 结束再报:别的路径仍可能连通。 if selectErr != ephem.ErrOutOfRange && (segment.target == target || segment.target == center) { return pos, vel, selectErr } continue } segment = current selected[segment.target] = true graph[segment.target] = append(graph[segment.target], stateEdge{to: segment.center, segment: segment}) graph[segment.center] = append(graph[segment.center], stateEdge{to: segment.target, segment: segment, reverse: true}) } type node struct { body Body pos [3]float64 vel [3]float64 } queue := []node{{body: target}} visited := map[Body]bool{target: true} for head := 0; head < len(queue); head++ { current := queue[head] if current.body == center { return current.pos, current.vel, nil } for _, edge := range graph[current.body] { if visited[edge.to] { continue } edgePos, edgeVel, edgeErr := edge.segment.state(jdTDB) if edgeErr != nil { return pos, vel, edgeErr } if edge.reverse { negateVector(&edgePos) negateVector(&edgeVel) } addVector(&edgePos, current.pos) addVector(&edgeVel, current.vel) visited[edge.to] = true queue = append(queue, node{body: edge.to, pos: edgePos, vel: edgeVel}) } } return pos, vel, ephem.ErrOutOfRange } func (segment *summarySegment) state(jdTDB float64) (pos, vel [3]float64, err error) { if segment.dataType == 20 { return segment.stateType20(jdTDB) } if segment.dataType == 21 { return segment.stateType21(jdTDB) } et := (jdTDB - spkJ2000) * spkSecondsDay index := int(math.Floor((et - segment.initET) / segment.interval)) if index < 0 { index = 0 } if index >= segment.records { index = segment.records - 1 } if jdTDB < spkJ2000+segment.startET/spkSecondsDay || jdTDB > spkJ2000+segment.endET/spkSecondsDay { return pos, vel, ephem.ErrOutOfRange } record, err := segment.readRecord(index) if err != nil { return pos, vel, err } if !finite(record[0]) || !finitePositive(record[1]) || !finiteAll(record[2:]) { return pos, vel, errInvalidDAF } x := (et - record[0]) / record[1] tolerance := math.Abs(math.Nextafter(jdTDB, math.Inf(1))-jdTDB) * spkSecondsDay / record[1] if x < -1-tolerance || x > 1+tolerance { return pos, vel, ephem.ErrOutOfRange } x = math.Max(-1, math.Min(1, x)) components := 3 if segment.dataType == 3 { components = 6 } coefficientCount := (segment.recordSize - 2) / components for component := 0; component < 3; component++ { value, derivative := chebyshev(record[2+component*coefficientCount:2+(component+1)*coefficientCount], x) pos[component] = value if segment.dataType == 2 { vel[component] = derivative / record[1] * spkSecondsDay } else { velocityStart := 2 + (component+3)*coefficientCount vel[component], _ = chebyshev(record[velocityStart:velocityStart+coefficientCount], x) vel[component] *= spkSecondsDay } } return pos, vel, nil } func (segment *summarySegment) stateType20(jdTDB float64) (pos, vel [3]float64, err error) { if jdTDB < spkJ2000+segment.startET/spkSecondsDay || jdTDB > spkJ2000+segment.endET/spkSecondsDay { return pos, vel, ephem.ErrOutOfRange } et := (jdTDB - spkJ2000) * spkSecondsDay init := (segment.initJD - spkJ2000 + segment.initFR) * spkSecondsDay interval := segment.interval * spkSecondsDay if !finitePositive(interval) { return pos, vel, errInvalidDAF } index := int(math.Floor((et - init) / interval)) if index < 0 { index = 0 } if index >= segment.records { index = segment.records - 1 } record, err := segment.readRecord(index) if err != nil { return pos, vel, err } nterms := segment.recordSize / 3 coefficientCount := nterms - 1 if len(record) != segment.recordSize || coefficientCount < 1 || !finiteAll(record) { return pos, vel, errInvalidDAF } recbeg := (segment.initJD - spkJ2000 + float64(index)*segment.interval) * spkSecondsDay radius := interval / 2 mid := recbeg + segment.initFR*spkSecondsDay + radius x := (et - mid) / radius if !finite(x) || x < -1-1e-12 || x > 1+1e-12 { return pos, vel, ephem.ErrOutOfRange } x = math.Max(-1, math.Min(1, x)) velocityScale := segment.distanceScale / segment.timeScale for component := 0; component < 3; component++ { base := component * nterms coefficients := record[base : base+coefficientCount] value, _ := chebyshev(coefficients, x) integral := chebyshevIntegral(coefficients, x) pos[component] = record[base+coefficientCount]*segment.distanceScale + radius*integral*velocityScale vel[component] = value * velocityScale * spkSecondsDay } return pos, vel, nil } func (segment *summarySegment) stateType21(jdTDB float64) (pos, vel [3]float64, err error) { if jdTDB < spkJ2000+segment.startET/spkSecondsDay || jdTDB > spkJ2000+segment.endET/spkSecondsDay { return pos, vel, ephem.ErrOutOfRange } et := (jdTDB - spkJ2000) * spkSecondsDay epochs, err := segment.owner.readWords(segment.epochStartWord, segment.records) if err != nil { return pos, vel, err } index := 0 for index < len(epochs) && epochs[index] < et { index++ } if index >= len(epochs) { index = len(epochs) - 1 } record, err := segment.readRecord(index) if err != nil { return pos, vel, err } maxdim := segment.maxDim record = append([]float64{float64(maxdim)}, record...) pos, vel, err = evaluateType21(et, record) for component := range vel { vel[component] *= spkSecondsDay } return pos, vel, err } func evaluateType21(et float64, record []float64) (statePos, stateVel [3]float64, err error) { if len(record) < 72 { return statePos, stateVel, errInvalidDAF } maxdim := int(math.Round(record[0])) if maxdim < 15 || maxdim > 25 || len(record) != 4*maxdim+12 || !finiteAll(record) { return statePos, stateVel, errInvalidDAF } tl := record[1] g := record[2 : 2+maxdim] refPos := [3]float64{record[maxdim+2], record[maxdim+4], record[maxdim+6]} refVel := [3]float64{record[maxdim+3], record[maxdim+5], record[maxdim+7]} kqmax1 := int(math.Round(record[4*maxdim+8])) kq := [3]int{int(math.Round(record[4*maxdim+9])), int(math.Round(record[4*maxdim+10])), int(math.Round(record[4*maxdim+11]))} // 差分表与步长表各只有 MAXDIM 项,权重表只有 KQMAX1+1 项。 if kqmax1 < 3 || kqmax1 > maxdim+1 { return statePos, stateVel, errInvalidDAF } for _, value := range g[:kqmax1-2] { if !finite(value) || value == 0 { return statePos, stateVel, errInvalidDAF } } for _, value := range kq { if value < 1 || value > kqmax1-1 { return statePos, stateVel, errInvalidDAF } } delta := et - tl tp := delta mq2 := kqmax1 - 2 ks := kqmax1 - 1 fc := make([]float64, kqmax1+1) wc := make([]float64, kqmax1) weights := make([]float64, kqmax1+2) for j := 1; j <= mq2; j++ { fc[j] = tp / g[j-1] wc[j-1] = delta / g[j-1] tp = delta + g[j-1] } for j := 1; j <= kqmax1; j++ { weights[j-1] = 1 / float64(j) } jx := 0 ks1 := ks - 1 for ks >= 2 { jx++ for j := 1; j <= jx; j++ { weights[j+ks-1] = fc[j]*weights[j+ks1-1] - wc[j-1]*weights[j+ks-1] } ks = ks1 ks1-- } for component := 0; component < 3; component++ { sum := 0.0 for j := kq[component]; j >= 1; j-- { sum += record[8+(component+1)*maxdim+j-1] * weights[j+ks-1] } statePos[component] = refPos[component] + delta*(refVel[component]+delta*sum) } for j := 1; j <= jx; j++ { weights[j+ks-1] = fc[j]*weights[j+ks1-1] - wc[j-1]*weights[j+ks-1] } ks-- for component := 0; component < 3; component++ { sum := 0.0 for j := kq[component]; j >= 1; j-- { sum += record[8+(component+1)*maxdim+j-1] * weights[j+ks-1] } stateVel[component] = refVel[component] + delta*sum } return statePos, stateVel, nil } func chebyshevIntegral(coefficients []float64, x float64) float64 { degree := len(coefficients) - 1 if degree < 0 { return 0 } s2 := 2 * x a2 := coefficients[0] if len(coefficients) >= 3 { a2 -= coefficients[2] * 0.5 } adegree1, adegree2 := 0.0, 0.0 if degree >= 2 { adegree1 = coefficients[degree-1] * 0.5 / float64(degree) } if degree >= 1 { adegree2 = coefficients[degree] * 0.5 / float64(degree+1) } f2, f1 := 0.0, 0.0 f0 := a2 if degree > 0 { f0 = adegree2 } z2, z1, z0 := 0.0, 0.0, f0 w2, w1, w0 := 0.0, 0.0, 0.0 for index := len(coefficients); index > 1; index-- { var coefficient float64 switch { case index == 2: coefficient = a2 case index < len(coefficients): coefficient = (coefficients[index-2] - coefficients[index]) * 0.5 / float64(index-1) default: coefficient = adegree1 } f2, f1 = f1, f0 f0 = coefficient + (s2*f1 - f2) z2, z1 = z1, z0 z0 = coefficient - z2 w2, w1 = w1, w0 w0 = coefficients[index-1] + (s2*w1 - w2) } c0 := z1 return c0 + x*f0 - f1 } func (segment *summarySegment) readRecord(index int) ([]float64, error) { if index < 0 || index >= segment.records { return nil, ephem.ErrOutOfRange } segment.cacheMu.Lock() if record, ok := segment.cache[index]; ok { copyRecord := append([]float64(nil), record...) segment.cacheMu.Unlock() return copyRecord, nil } segment.cacheMu.Unlock() record, err := segment.owner.readWords(segment.startWord+int64(index*segment.recordSize), segment.recordSize) if err != nil { return nil, err } segment.cacheMu.Lock() defer segment.cacheMu.Unlock() if previous, ok := segment.cache[index]; ok { return append([]float64(nil), previous...), nil } if len(segment.cacheOrder) >= spkCacheRecords { delete(segment.cache, segment.cacheOrder[0]) segment.cacheOrder = segment.cacheOrder[1:] } segment.cache[index] = record segment.cacheOrder = append(segment.cacheOrder, index) return append([]float64(nil), record...), nil } func chebyshev(coefficients []float64, x float64) (value, derivative float64) { t0, t1 := 1.0, x d0, d1 := 0.0, 1.0 for index, coefficient := range coefficients { switch index { case 0: value += coefficient case 1: value += coefficient * t1 derivative += coefficient default: t2 := 2*x*t1 - t0 d2 := 2*t1 + 2*x*d1 - d0 value += coefficient * t2 derivative += coefficient * d2 t0, t1 = t1, t2 d0, d1 = d1, d2 } } return value, derivative } func (s *File) readRecord(record int) ([]byte, error) { if record < 1 || int64(record)*dafRecordBytes > s.size { return nil, errInvalidDAF } data := make([]byte, dafRecordBytes) if _, err := s.file.ReadAt(data, int64(record-1)*dafRecordBytes); err != nil { return nil, err } return data, nil } func (s *File) readWords(startWord int64, count int) ([]float64, error) { if startWord < 1 || count < 1 || int64(count) > s.size/dafWordBytes || startWord > s.size/dafWordBytes-int64(count)+1 { return nil, errInvalidDAF } data := make([]byte, count*dafWordBytes) if _, err := s.file.ReadAt(data, (startWord-1)*dafWordBytes); err != nil { return nil, err } words := make([]float64, count) for index := range words { words[index] = float64Value(s.order, data[index*dafWordBytes:(index+1)*dafWordBytes]) } return words, nil } func byteOrderFor(format string) (binary.ByteOrder, error) { switch format { case "LTL-IEEE": return binary.LittleEndian, nil case "BIG-IEEE": return binary.BigEndian, nil default: return nil, fmt.Errorf("%w: binary format %q", errInvalidDAF, format) } } func int32Value(order binary.ByteOrder, data []byte, offset int) int32 { return int32(order.Uint32(data[offset : offset+4])) } func float64Value(order binary.ByteOrder, data []byte) float64 { return math.Float64frombits(order.Uint64(data)) } func finitePositive(value float64) bool { return value > 0 && finite(value) } func finite(value float64) bool { return !math.IsNaN(value) && !math.IsInf(value, 0) } func finiteAll(values []float64) bool { for _, value := range values { if !finite(value) { return false } } return true } func validInteger(value float64, minimum, maximum int64) bool { return finite(value) && value >= float64(minimum) && value <= float64(maximum) && value == math.Trunc(value) } func addVector(dst *[3]float64, src [3]float64) { for index := range dst { dst[index] += src[index] } } func negateVector(value *[3]float64) { for index := range value { value[index] = -value[index] } } var _ ephem.Engine = (*File)(nil)