main #6

Closed
afanasyev.aa wants to merge 2 commits from afanasyev.aa/predictor:main into main
21 changed files with 978 additions and 137 deletions
Showing only changes of commit fe207f3fab - Show all commits

View file

@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(cat:*)",
"Bash(xargs:*)"
],
"deny": [],
"ask": []
}
}

View file

@ -61,6 +61,13 @@ paths:
name: descent_curve
schema:
type: string
- in: query
name: simulate_stages
schema:
type: array
items:
type: string
enum: [ascent, descent, float]
- in: query
name: interpolate
schema:
@ -147,7 +154,7 @@ components:
properties:
stage:
type: string
enum: ["ascent", "descent"]
enum: ["ascent", "descent", "float"]
trajectory:
type: array
items:

23
force_update.bat Normal file
View file

@ -0,0 +1,23 @@
@echo off
REM Batch script to force download fresh GRIB data
REM This deletes old cube files and downloads the latest dataset
echo Forcing fresh GRIB data download...
echo.
REM Set environment
set GSN_PREDICTOR_GRIB_DIR=C:\tmp\grib
set GSN_PREDICTOR_GRIB_TTL=72h
REM Delete old cube files to force fresh download
echo Deleting old cube files...
del /Q C:\tmp\grib\*.cube 2>nul
echo.
echo Starting service to download fresh data...
echo This will download from S3 (may take several minutes)
echo Press Ctrl+C once download completes and you see "initial GRIB update complete"
echo.
REM Run the service - it will download fresh data on startup
go run cmd/api/main.go

27
force_update.ps1 Normal file
View file

@ -0,0 +1,27 @@
# PowerShell script to force download fresh GRIB data
# This deletes old cube files and downloads the latest dataset
Write-Host "Forcing fresh GRIB data download..." -ForegroundColor Yellow
# Set environment
$env:GSN_PREDICTOR_GRIB_DIR = "C:\tmp\grib"
$env:GSN_PREDICTOR_GRIB_TTL = "72h"
# Delete old cube files to force fresh download
$cubeFiles = Get-ChildItem -Path "C:\tmp\grib\*.cube" -ErrorAction SilentlyContinue
if ($cubeFiles) {
Write-Host "Deleting old cube files:" -ForegroundColor Yellow
foreach ($file in $cubeFiles) {
Write-Host " - $($file.Name)" -ForegroundColor Gray
Remove-Item $file.FullName -Force
}
} else {
Write-Host "No old cube files found" -ForegroundColor Gray
}
Write-Host "`nStarting service to download fresh data..." -ForegroundColor Green
Write-Host "This will download from S3 (may take several minutes)" -ForegroundColor Cyan
Write-Host "Press Ctrl+C once download completes and you see 'initial GRIB update complete'`n" -ForegroundColor Cyan
# Run the service - it will download fresh data on startup
go run cmd/api/main.go

11
go.mod
View file

@ -3,6 +3,9 @@ module git.intra.yksa.space/gsn/predictor
go 1.24.4
require (
github.com/aws/aws-sdk-go-v2 v1.39.3
github.com/aws/aws-sdk-go-v2/config v1.31.13
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.5
github.com/caarlos0/env/v11 v11.3.1
github.com/edsrzf/mmap-go v1.2.0
github.com/go-co-op/gocron v1.37.0
@ -11,6 +14,7 @@ require (
github.com/nilsmagnus/grib v1.2.8
github.com/ogen-go/ogen v1.16.0
github.com/rs/cors v1.11.1
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/metric v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
@ -19,9 +23,7 @@ require (
)
require (
github.com/aws/aws-sdk-go-v2 v1.39.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.2 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.13 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.10 // indirect
@ -32,11 +34,11 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.10 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.7 // indirect
github.com/aws/smithy-go v1.23.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
@ -46,9 +48,11 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
@ -57,4 +61,5 @@ require (
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

37
go.sum
View file

@ -57,8 +57,6 @@ github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb
github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@ -77,17 +75,12 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/nilsmagnus/grib v1.2.8 h1:H7ch/1/agaCqM3MC8hW1Ft+EJ+q2XB757uml/IfPvp4=
github.com/nilsmagnus/grib v1.2.8/go.mod h1:XHm+5zuoOk0NSIWaGmA3JaAxI4i50YvD1L1vz+aqPOQ=
github.com/ogen-go/ogen v1.14.0 h1:TU1Nj4z9UBsAfTkf+IhuNNp7igdFQKqkk9+6/y4XuWg=
github.com/ogen-go/ogen v1.14.0/go.mod h1:Iw1vkqkx6SU7I9th5ceP+fVPJ6Wge4e3kAVzAxJEpPE=
github.com/ogen-go/ogen v1.16.0 h1:fKHEYokW/QrMzVNXId74/6RObRIUs9T2oroGKtR25Iw=
github.com/ogen-go/ogen v1.16.0/go.mod h1:s3nWiMzybSf8fhxckyO+wtto92+QHpEL8FmkPnhL3jI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@ -97,13 +90,10 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
@ -111,30 +101,22 @@ github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+D
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
@ -144,26 +126,15 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/exp v0.0.0-20251017212417-90e834f514db h1:by6IehL4BH5k3e3SJmcoNbOobMey2SLpAF79iPOEBvw=
golang.org/x/exp v0.0.0-20251017212417-90e834f514db/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -19,6 +19,7 @@ type PredictionParameters struct {
StopDatetime *time.Time
AscentCurve *string // base64
DescentCurve *string // base64
SimulateStages []string
Interpolate *bool
Format *string
Dataset *time.Time
@ -85,5 +86,11 @@ func ConvertFlatPredictionParams(params api.PerformPredictionParams) *Prediction
if v, ok := params.Dataset.Get(); ok {
out.Dataset = &v
}
if len(params.SimulateStages) > 0 {
out.SimulateStages = make([]string, len(params.SimulateStages))
for i, stage := range params.SimulateStages {
out.SimulateStages[i] = string(stage)
}
}
return out
}

View file

@ -23,6 +23,22 @@ type Stage struct {
EndTime time.Time
}
// shouldSimulateStage checks if a given stage should be simulated based on the SimulateStages filter
func shouldSimulateStage(params ds.PredictionParameters, stage string) bool {
// If no filter is specified, simulate all stages
if len(params.SimulateStages) == 0 {
return true
}
// Check if the stage is in the filter list
for _, s := range params.SimulateStages {
if s == stage {
return true
}
}
return false
}
// CustomCurve represents a custom ascent/descent curve
type CustomCurve struct {
Altitude []float64 `json:"altitude"`
@ -103,16 +119,27 @@ func (s *Service) PerformPrediction(ctx context.Context, params ds.PredictionPar
func (s *Service) standardProfile(ctx context.Context, params ds.PredictionParameters, ascentRate, burstAltitude, descentRate float64, ascentCurve, descentCurve *CustomCurve) []ds.PredicitonResult {
var results []ds.PredicitonResult
var lastResult ds.PredicitonResult
// Stage 1: Ascent
if shouldSimulateStage(params, "ascent") {
ascentResults := s.simulateAscent(ctx, params, ascentRate, burstAltitude, ascentCurve)
results = append(results, ascentResults...)
if len(ascentResults) > 0 {
// Get final position from ascent
lastResult := ascentResults[len(ascentResults)-1]
lastResult = ascentResults[len(ascentResults)-1]
}
} else {
// If ascent is skipped, use initial position as starting point
lastResult = ds.PredicitonResult{
Latitude: params.LaunchLatitude,
Longitude: params.LaunchLongitude,
Altitude: &burstAltitude,
Timestamp: params.LaunchDatetime,
}
}
// Stage 2: Descent
if shouldSimulateStage(params, "descent") && lastResult.Latitude != nil {
descentParams := ds.PredictionParameters{
LaunchLatitude: lastResult.Latitude,
LaunchLongitude: lastResult.Longitude,
@ -129,45 +156,36 @@ func (s *Service) standardProfile(ctx context.Context, params ds.PredictionParam
func (s *Service) floatProfile(ctx context.Context, params ds.PredictionParameters, ascentRate, burstAltitude, floatAltitude, descentRate float64, ascentCurve, descentCurve *CustomCurve) []ds.PredicitonResult {
var results []ds.PredicitonResult
var lastResult ds.PredicitonResult
// Stage 1: Ascent to float altitude
if shouldSimulateStage(params, "ascent") {
ascentResults := s.simulateAscent(ctx, params, ascentRate, floatAltitude, ascentCurve)
results = append(results, ascentResults...)
if len(ascentResults) > 0 {
lastResult = ascentResults[len(ascentResults)-1]
}
} else {
// If ascent is skipped, use initial position at float altitude as starting point
lastResult = ds.PredicitonResult{
Latitude: params.LaunchLatitude,
Longitude: params.LaunchLongitude,
Altitude: &floatAltitude,
Timestamp: params.LaunchDatetime,
}
}
// Stage 2: Float (simulate for some time)
lastResult := ascentResults[len(ascentResults)-1]
if shouldSimulateStage(params, "float") && lastResult.Latitude != nil {
floatResults := s.simulateFloat(ctx, lastResult, 30*time.Minute) // Float for 30 minutes
results = append(results, floatResults...)
if len(floatResults) > 0 {
lastResult = floatResults[len(floatResults)-1]
}
}
// Stage 3: Descent
finalFloat := floatResults[len(floatResults)-1]
descentParams := ds.PredictionParameters{
LaunchLatitude: finalFloat.Latitude,
LaunchLongitude: finalFloat.Longitude,
LaunchAltitude: finalFloat.Altitude,
LaunchDatetime: finalFloat.Timestamp,
}
descentResults := s.simulateDescent(ctx, descentParams, descentRate, 0, descentCurve)
results = append(results, descentResults...)
}
}
return results
}
func (s *Service) reverseProfile(ctx context.Context, params ds.PredictionParameters, ascentRate, burstAltitude, descentRate float64, ascentCurve, descentCurve *CustomCurve) []ds.PredicitonResult {
var results []ds.PredicitonResult
// Stage 1: Ascent
ascentResults := s.simulateAscent(ctx, params, ascentRate, burstAltitude, ascentCurve)
results = append(results, ascentResults...)
if len(ascentResults) > 0 {
// Stage 2: Descent to float altitude
lastResult := ascentResults[len(ascentResults)-1]
if shouldSimulateStage(params, "descent") && lastResult.Latitude != nil {
descentParams := ds.PredictionParameters{
LaunchLatitude: lastResult.Latitude,
LaunchLongitude: lastResult.Longitude,
@ -175,21 +193,67 @@ func (s *Service) reverseProfile(ctx context.Context, params ds.PredictionParame
LaunchDatetime: lastResult.Timestamp,
}
// Descent to float altitude (if specified)
descentResults := s.simulateDescent(ctx, descentParams, descentRate, 0, descentCurve)
results = append(results, descentResults...)
}
return results
}
func (s *Service) reverseProfile(ctx context.Context, params ds.PredictionParameters, ascentRate, burstAltitude, descentRate float64, ascentCurve, descentCurve *CustomCurve) []ds.PredicitonResult {
var results []ds.PredicitonResult
var lastResult ds.PredicitonResult
// Stage 1: Ascent
if shouldSimulateStage(params, "ascent") {
ascentResults := s.simulateAscent(ctx, params, ascentRate, burstAltitude, ascentCurve)
results = append(results, ascentResults...)
if len(ascentResults) > 0 {
lastResult = ascentResults[len(ascentResults)-1]
}
} else {
// If ascent is skipped, use initial position at burst altitude as starting point
lastResult = ds.PredicitonResult{
Latitude: params.LaunchLatitude,
Longitude: params.LaunchLongitude,
Altitude: &burstAltitude,
Timestamp: params.LaunchDatetime,
}
}
// Stage 2: Descent to float altitude
floatAlt := 0.0
if params.FloatAltitude != nil {
floatAlt = *params.FloatAltitude
}
if shouldSimulateStage(params, "descent") && lastResult.Latitude != nil {
descentParams := ds.PredictionParameters{
LaunchLatitude: lastResult.Latitude,
LaunchLongitude: lastResult.Longitude,
LaunchAltitude: lastResult.Altitude,
LaunchDatetime: lastResult.Timestamp,
}
descentResults := s.simulateDescent(ctx, descentParams, descentRate, floatAlt, descentCurve)
results = append(results, descentResults...)
if floatAlt > 0 && len(descentResults) > 0 {
// Stage 3: Float
finalDescent := descentResults[len(descentResults)-1]
floatResults := s.simulateFloat(ctx, finalDescent, 30*time.Minute)
results = append(results, floatResults...)
if len(descentResults) > 0 {
lastResult = descentResults[len(descentResults)-1]
}
} else if floatAlt > 0 {
// If descent is skipped but we need to float, position at float altitude
lastResult = ds.PredicitonResult{
Latitude: lastResult.Latitude,
Longitude: lastResult.Longitude,
Altitude: &floatAlt,
Timestamp: lastResult.Timestamp,
}
}
// Stage 3: Float
if shouldSimulateStage(params, "float") && floatAlt > 0 && lastResult.Latitude != nil {
floatResults := s.simulateFloat(ctx, lastResult, 30*time.Minute)
results = append(results, floatResults...)
}
return results
@ -197,14 +261,27 @@ func (s *Service) reverseProfile(ctx context.Context, params ds.PredictionParame
func (s *Service) customProfile(ctx context.Context, params ds.PredictionParameters, ascentCurve, descentCurve *CustomCurve) []ds.PredicitonResult {
var results []ds.PredicitonResult
var lastResult ds.PredicitonResult
if ascentCurve != nil {
// Custom ascent
if shouldSimulateStage(params, "ascent") && ascentCurve != nil {
ascentResults := s.simulateCustomAscent(ctx, params, ascentCurve)
results = append(results, ascentResults...)
if len(ascentResults) > 0 {
lastResult = ascentResults[len(ascentResults)-1]
}
} else if len(results) == 0 {
// If ascent is skipped, use initial position
lastResult = ds.PredicitonResult{
Latitude: params.LaunchLatitude,
Longitude: params.LaunchLongitude,
Altitude: params.LaunchAltitude,
Timestamp: params.LaunchDatetime,
}
}
if descentCurve != nil && len(results) > 0 {
lastResult := results[len(results)-1]
// Custom descent
if shouldSimulateStage(params, "descent") && descentCurve != nil && lastResult.Latitude != nil {
descentParams := ds.PredictionParameters{
LaunchLatitude: lastResult.Latitude,
LaunchLongitude: lastResult.Longitude,

View file

@ -0,0 +1,492 @@
package service
import (
"context"
"testing"
"time"
"git.intra.yksa.space/gsn/predictor/internal/pkg/ds"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockGrib is a mock implementation of the Grib interface
type MockGrib struct {
mock.Mock
}
func (m *MockGrib) Update(ctx context.Context) error {
args := m.Called(ctx)
return args.Error(0)
}
func (m *MockGrib) Extract(ctx context.Context, lat, lon, alt float64, t time.Time) ([2]float64, error) {
args := m.Called(ctx, lat, lon, alt, t)
return args.Get(0).([2]float64), args.Error(1)
}
func (m *MockGrib) Close() error {
args := m.Called()
return args.Error(0)
}
// Helper function to create a test service with mocked GRIB
func createTestService() (*Service, *MockGrib) {
mockGrib := new(MockGrib)
// Default mock behavior: return constant wind (5 m/s east, 3 m/s north)
mockGrib.On("Extract", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return([2]float64{5.0, 3.0}, nil)
service := &Service{
grib: mockGrib,
}
return service, mockGrib
}
// Helper function to create basic prediction parameters
func createBasicParams() ds.PredictionParameters {
lat := 40.0
lon := -105.0
alt := 1000.0
launchTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
profile := "standard_profile"
ascentRate := 5.0
burstAltitude := 10000.0
descentRate := 5.0
return ds.PredictionParameters{
LaunchLatitude: &lat,
LaunchLongitude: &lon,
LaunchAltitude: &alt,
LaunchDatetime: &launchTime,
Profile: &profile,
AscentRate: &ascentRate,
BurstAltitude: &burstAltitude,
DescentRate: &descentRate,
}
}
func TestRestrictedPrediction_OnlyAscent(t *testing.T) {
service, _ := createTestService()
params := createBasicParams()
// Restrict to ascent only
params.SimulateStages = []string{"ascent"}
results, err := service.PerformPrediction(context.Background(), params)
assert.NoError(t, err)
assert.NotEmpty(t, results)
// Verify all results are during ascent phase (altitude increasing)
for i := 1; i < len(results); i++ {
assert.GreaterOrEqual(t, *results[i].Altitude, *results[i-1].Altitude,
"Altitude should be increasing or equal during ascent")
}
// Last altitude should be near burst altitude
lastAlt := *results[len(results)-1].Altitude
burstAlt := *params.BurstAltitude
assert.InDelta(t, burstAlt, lastAlt, 500.0, "Last altitude should be near burst altitude")
}
func TestRestrictedPrediction_OnlyDescent(t *testing.T) {
service, _ := createTestService()
params := createBasicParams()
// Restrict to descent only
params.SimulateStages = []string{"descent"}
results, err := service.PerformPrediction(context.Background(), params)
assert.NoError(t, err)
assert.NotEmpty(t, results)
// First result should be at burst altitude (since ascent was skipped)
firstAlt := *results[0].Altitude
burstAlt := *params.BurstAltitude
assert.Equal(t, burstAlt, firstAlt, "Should start at burst altitude when ascent is skipped")
// Verify all results are during descent phase (altitude decreasing)
for i := 1; i < len(results); i++ {
assert.LessOrEqual(t, *results[i].Altitude, *results[i-1].Altitude,
"Altitude should be decreasing or equal during descent")
}
// Last altitude should be near ground
lastAlt := *results[len(results)-1].Altitude
assert.Less(t, lastAlt, 1000.0, "Last altitude should be near ground")
}
func TestRestrictedPrediction_AscentAndDescent(t *testing.T) {
service, _ := createTestService()
params := createBasicParams()
// Include both ascent and descent
params.SimulateStages = []string{"ascent", "descent"}
results, err := service.PerformPrediction(context.Background(), params)
assert.NoError(t, err)
assert.NotEmpty(t, results)
// Find the peak altitude (transition point)
maxAlt := 0.0
maxIdx := 0
for i, result := range results {
if *result.Altitude > maxAlt {
maxAlt = *result.Altitude
maxIdx = i
}
}
// Verify ascent phase
for i := 1; i <= maxIdx; i++ {
assert.GreaterOrEqual(t, *results[i].Altitude, *results[i-1].Altitude,
"Altitude should increase during ascent phase")
}
// Verify descent phase
for i := maxIdx + 1; i < len(results); i++ {
assert.LessOrEqual(t, *results[i].Altitude, *results[i-1].Altitude,
"Altitude should decrease during descent phase")
}
}
func TestRestrictedPrediction_FloatProfile_OnlyFloat(t *testing.T) {
service, _ := createTestService()
params := createBasicParams()
profile := "float_profile"
floatAlt := 15000.0
params.Profile = &profile
params.FloatAltitude = &floatAlt
// Restrict to float only
params.SimulateStages = []string{"float"}
results, err := service.PerformPrediction(context.Background(), params)
assert.NoError(t, err)
assert.NotEmpty(t, results)
// All results should be at the float altitude
for _, result := range results {
assert.Equal(t, floatAlt, *result.Altitude,
"Altitude should remain constant at float altitude")
}
// Verify horizontal movement (lat/lon changes due to wind)
firstLat := *results[0].Latitude
lastLat := *results[len(results)-1].Latitude
assert.NotEqual(t, firstLat, lastLat, "Latitude should change during float due to wind")
}
func TestRestrictedPrediction_FloatProfile_AllStages(t *testing.T) {
service, _ := createTestService()
params := createBasicParams()
profile := "float_profile"
floatAlt := 15000.0
params.Profile = &profile
params.FloatAltitude = &floatAlt
// Include all stages
params.SimulateStages = []string{"ascent", "float", "descent"}
results, err := service.PerformPrediction(context.Background(), params)
assert.NoError(t, err)
assert.NotEmpty(t, results)
// Verify we have ascending, constant, and descending altitude patterns
hasAscent := false
hasFloat := false
hasDescent := false
const altTolerance = 50.0 // Tolerance for altitude comparison
for i := 1; i < len(results); i++ {
altDiff := *results[i].Altitude - *results[i-1].Altitude
if altDiff > altTolerance {
hasAscent = true
} else if altDiff < -altTolerance {
hasDescent = true
} else if *results[i].Altitude > 10000 { // Float happens at high altitude
hasFloat = true
}
}
assert.True(t, hasAscent, "Should have ascent phase")
assert.True(t, hasFloat, "Should have float phase")
assert.True(t, hasDescent, "Should have descent phase")
// Verify maximum altitude is near float altitude
maxAlt := 0.0
for _, result := range results {
if *result.Altitude > maxAlt {
maxAlt = *result.Altitude
}
}
assert.InDelta(t, floatAlt, maxAlt, 1000.0, "Max altitude should be near float altitude")
}
func TestRestrictedPrediction_ReverseProfile_OnlyFloat(t *testing.T) {
service, _ := createTestService()
params := createBasicParams()
profile := "reverse_profile"
floatAlt := 5000.0
params.Profile = &profile
params.FloatAltitude = &floatAlt
// Restrict to float only
params.SimulateStages = []string{"float"}
results, err := service.PerformPrediction(context.Background(), params)
assert.NoError(t, err)
assert.NotEmpty(t, results)
// All results should be at the float altitude
for _, result := range results {
assert.InDelta(t, floatAlt, *result.Altitude, 10.0,
"Altitude should remain near float altitude")
}
}
func TestRestrictedPrediction_EmptyStages_SimulatesAll(t *testing.T) {
service, _ := createTestService()
params := createBasicParams()
// Empty SimulateStages should simulate all stages
params.SimulateStages = []string{}
results, err := service.PerformPrediction(context.Background(), params)
assert.NoError(t, err)
assert.NotEmpty(t, results)
// Should have both ascent and descent
// Find the peak
maxAlt := 0.0
hasAscent := false
hasDescent := false
for i := 1; i < len(results); i++ {
if *results[i].Altitude > *results[i-1].Altitude {
hasAscent = true
}
if *results[i].Altitude < *results[i-1].Altitude {
hasDescent = true
}
if *results[i].Altitude > maxAlt {
maxAlt = *results[i].Altitude
}
}
assert.True(t, hasAscent, "Should have ascent phase")
assert.True(t, hasDescent, "Should have descent phase")
}
func TestRestrictedPrediction_NilStages_SimulatesAll(t *testing.T) {
service, _ := createTestService()
params := createBasicParams()
// Nil SimulateStages should simulate all stages
params.SimulateStages = nil
results, err := service.PerformPrediction(context.Background(), params)
assert.NoError(t, err)
assert.NotEmpty(t, results)
// Should have both ascent and descent
maxAlt := 0.0
minAltAfterMax := 1000000.0
for _, result := range results {
if *result.Altitude > maxAlt {
maxAlt = *result.Altitude
}
}
foundMax := false
for _, result := range results {
if *result.Altitude == maxAlt {
foundMax = true
}
if foundMax && *result.Altitude < minAltAfterMax {
minAltAfterMax = *result.Altitude
}
}
// Should reach high altitude and come back down
assert.Greater(t, maxAlt, 5000.0, "Should reach high altitude")
assert.Less(t, minAltAfterMax, maxAlt, "Should descend after reaching max altitude")
}
func TestRestrictedPrediction_InvalidStage_IgnoresInvalid(t *testing.T) {
service, _ := createTestService()
params := createBasicParams()
// Include invalid stage name (should be ignored)
params.SimulateStages = []string{"ascent", "invalid_stage", "descent"}
results, err := service.PerformPrediction(context.Background(), params)
assert.NoError(t, err)
assert.NotEmpty(t, results)
// Should still simulate ascent and descent, ignoring the invalid stage
}
func TestRestrictedPrediction_WindImpact(t *testing.T) {
service, mockGrib := createTestService()
// Override mock to return strong eastward wind
mockGrib.ExpectedCalls = nil
mockGrib.On("Extract", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return([2]float64{20.0, 0.0}, nil) // Strong eastward wind
params := createBasicParams()
params.SimulateStages = []string{"ascent"}
results, err := service.PerformPrediction(context.Background(), params)
assert.NoError(t, err)
assert.NotEmpty(t, results)
// Longitude should increase significantly due to eastward wind
firstLon := *results[0].Longitude
lastLon := *results[len(results)-1].Longitude
assert.Greater(t, lastLon, firstLon, "Longitude should increase with eastward wind")
// Verify wind values are captured in results
for _, result := range results {
if result.WindU != nil {
// Wind values should be present in results
assert.NotNil(t, result.WindV, "WindV should be present if WindU is present")
}
}
}
func TestRestrictedPrediction_MissingRequiredParams(t *testing.T) {
service, _ := createTestService()
testCases := []struct {
name string
params ds.PredictionParameters
}{
{
name: "Missing latitude",
params: ds.PredictionParameters{
LaunchLongitude: floatPtr(-105.0),
LaunchAltitude: floatPtr(1000.0),
LaunchDatetime: timePtr(time.Now()),
},
},
{
name: "Missing longitude",
params: ds.PredictionParameters{
LaunchLatitude: floatPtr(40.0),
LaunchAltitude: floatPtr(1000.0),
LaunchDatetime: timePtr(time.Now()),
},
},
{
name: "Missing altitude",
params: ds.PredictionParameters{
LaunchLatitude: floatPtr(40.0),
LaunchLongitude: floatPtr(-105.0),
LaunchDatetime: timePtr(time.Now()),
},
},
{
name: "Missing datetime",
params: ds.PredictionParameters{
LaunchLatitude: floatPtr(40.0),
LaunchLongitude: floatPtr(-105.0),
LaunchAltitude: floatPtr(1000.0),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.params.SimulateStages = []string{"ascent"}
results, err := service.PerformPrediction(context.Background(), tc.params)
assert.Error(t, err)
assert.Equal(t, ErrInvalidParameters, err)
assert.Nil(t, results)
})
}
}
func TestShouldSimulateStage(t *testing.T) {
testCases := []struct {
name string
stages []string
queryStage string
shouldSimulate bool
}{
{
name: "Empty filter simulates all",
stages: []string{},
queryStage: "ascent",
shouldSimulate: true,
},
{
name: "Nil filter simulates all",
stages: nil,
queryStage: "descent",
shouldSimulate: true,
},
{
name: "Stage in filter",
stages: []string{"ascent", "descent"},
queryStage: "ascent",
shouldSimulate: true,
},
{
name: "Stage not in filter",
stages: []string{"ascent"},
queryStage: "descent",
shouldSimulate: false,
},
{
name: "Float stage in filter",
stages: []string{"float"},
queryStage: "float",
shouldSimulate: true,
},
{
name: "Multiple stages excluding one",
stages: []string{"ascent", "float"},
queryStage: "descent",
shouldSimulate: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
params := ds.PredictionParameters{
SimulateStages: tc.stages,
}
result := shouldSimulateStage(params, tc.queryStage)
assert.Equal(t, tc.shouldSimulate, result)
})
}
}
// Helper functions
func floatPtr(f float64) *float64 {
return &f
}
func timePtr(t time.Time) *time.Time {
return &t
}

View file

@ -5,14 +5,14 @@ package gsn
import (
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
ht "github.com/ogen-go/ogen/http"
"github.com/ogen-go/ogen/middleware"
"github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/otelogen"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
)
var (
@ -32,6 +32,7 @@ type otelConfig struct {
Tracer trace.Tracer
MeterProvider metric.MeterProvider
Meter metric.Meter
Attributes []attribute.KeyValue
}
func (cfg *otelConfig) initOTEL() {
@ -215,6 +216,13 @@ func WithMeterProvider(provider metric.MeterProvider) Option {
})
}
// WithAttributes specifies default otel attributes.
func WithAttributes(attributes ...attribute.KeyValue) Option {
return otelOptionFunc(func(cfg *otelConfig) {
cfg.Attributes = attributes
})
}
// WithClient specifies http client to use.
func WithClient(client ht.Client) ClientOption {
return optionFunc[clientConfig](func(cfg *clientConfig) {

View file

@ -9,16 +9,15 @@ import (
"time"
"github.com/go-faster/errors"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"go.opentelemetry.io/otel/trace"
"github.com/ogen-go/ogen/conv"
ht "github.com/ogen-go/ogen/http"
"github.com/ogen-go/ogen/otelogen"
"github.com/ogen-go/ogen/uri"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
"go.opentelemetry.io/otel/trace"
)
func trimTrailingSlashes(u *url.URL) {
@ -103,8 +102,9 @@ func (c *Client) sendPerformPrediction(ctx context.Context, params PerformPredic
otelAttrs := []attribute.KeyValue{
otelogen.OperationID("performPrediction"),
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/api/v1/prediction"),
semconv.URLTemplateKey.String("/api/v1/prediction"),
}
otelAttrs = append(otelAttrs, c.cfg.Attributes...)
// Run stopwatch.
startTime := time.Now()
@ -345,6 +345,32 @@ func (c *Client) sendPerformPrediction(ctx context.Context, params PerformPredic
return res, errors.Wrap(err, "encode query")
}
}
{
// Encode "simulate_stages" parameter.
cfg := uri.QueryParameterEncodingConfig{
Name: "simulate_stages",
Style: uri.QueryStyleForm,
Explode: true,
}
if err := q.EncodeParam(cfg, func(e uri.Encoder) error {
if params.SimulateStages != nil {
return e.EncodeArray(func(e uri.Encoder) error {
for i, item := range params.SimulateStages {
if err := func() error {
return e.EncodeValue(conv.StringToString(string(item)))
}(); err != nil {
return errors.Wrapf(err, "[%d]", i)
}
}
return nil
})
}
return nil
}); err != nil {
return res, errors.Wrap(err, "encode query")
}
}
{
// Encode "interpolate" parameter.
cfg := uri.QueryParameterEncodingConfig{
@ -434,8 +460,9 @@ func (c *Client) sendReadinessCheck(ctx context.Context) (res *ReadinessResponse
otelAttrs := []attribute.KeyValue{
otelogen.OperationID("readinessCheck"),
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/ready"),
semconv.URLTemplateKey.String("/ready"),
}
otelAttrs = append(otelAttrs, c.cfg.Attributes...)
// Run stopwatch.
startTime := time.Now()

View file

@ -8,16 +8,15 @@ import (
"time"
"github.com/go-faster/errors"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"go.opentelemetry.io/otel/trace"
ht "github.com/ogen-go/ogen/http"
"github.com/ogen-go/ogen/middleware"
"github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/otelogen"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
"go.opentelemetry.io/otel/trace"
)
type codeRecorder struct {
@ -30,6 +29,10 @@ func (c *codeRecorder) WriteHeader(status int) {
c.ResponseWriter.WriteHeader(status)
}
func (c *codeRecorder) Unwrap() http.ResponseWriter {
return c.ResponseWriter
}
// handlePerformPredictionRequest handles performPrediction operation.
//
// Perform prediction.
@ -86,7 +89,7 @@ func (s *Server) handlePerformPredictionRequest(args [0]string, argsEscaped bool
// unless there was another error (e.g., network error receiving the response body; or 3xx codes with
// max redirects exceeded), in which case status MUST be set to Error.
code := statusWriter.status
if code >= 100 && code < 500 {
if code < 100 || code >= 500 {
span.SetStatus(codes.Error, stage)
}
@ -115,6 +118,8 @@ func (s *Server) handlePerformPredictionRequest(args [0]string, argsEscaped bool
return
}
var rawBody []byte
var response *PredictionResult
if m := s.cfg.Middleware; m != nil {
mreq := middleware.Request{
@ -123,6 +128,7 @@ func (s *Server) handlePerformPredictionRequest(args [0]string, argsEscaped bool
OperationSummary: "Perform prediction",
OperationID: "performPrediction",
Body: nil,
RawBody: rawBody,
Params: middleware.Parameters{
{
Name: "launch_latitude",
@ -172,6 +178,10 @@ func (s *Server) handlePerformPredictionRequest(args [0]string, argsEscaped bool
Name: "descent_curve",
In: "query",
}: params.DescentCurve,
{
Name: "simulate_stages",
In: "query",
}: params.SimulateStages,
{
Name: "interpolate",
In: "query",
@ -291,7 +301,7 @@ func (s *Server) handleReadinessCheckRequest(args [0]string, argsEscaped bool, w
// unless there was another error (e.g., network error receiving the response body; or 3xx codes with
// max redirects exceeded), in which case status MUST be set to Error.
code := statusWriter.status
if code >= 100 && code < 500 {
if code < 100 || code >= 500 {
span.SetStatus(codes.Error, stage)
}
@ -306,6 +316,8 @@ func (s *Server) handleReadinessCheckRequest(args [0]string, argsEscaped bool, w
err error
)
var rawBody []byte
var response *ReadinessResponse
if m := s.cfg.Middleware; m != nil {
mreq := middleware.Request{
@ -314,6 +326,7 @@ func (s *Server) handleReadinessCheckRequest(args [0]string, argsEscaped bool, w
OperationSummary: "Readiness check",
OperationID: "readinessCheck",
Body: nil,
RawBody: rawBody,
Params: middleware.Parameters{},
Raw: r,
}

View file

@ -9,7 +9,6 @@ import (
"github.com/go-faster/errors"
"github.com/go-faster/jx"
"github.com/ogen-go/ogen/json"
"github.com/ogen-go/ogen/validate"
)
@ -607,6 +606,8 @@ func (s *PredictionResultPredictionItemStage) Decode(d *jx.Decoder) error {
*s = PredictionResultPredictionItemStageAscent
case PredictionResultPredictionItemStageDescent:
*s = PredictionResultPredictionItemStageDescent
case PredictionResultPredictionItemStageFloat:
*s = PredictionResultPredictionItemStageFloat
default:
*s = PredictionResultPredictionItemStage(v)
}

View file

@ -3,11 +3,11 @@
package gsn
import (
"fmt"
"net/http"
"time"
"github.com/go-faster/errors"
"github.com/ogen-go/ogen/conv"
"github.com/ogen-go/ogen/middleware"
"github.com/ogen-go/ogen/ogenerrors"
@ -17,21 +17,22 @@ import (
// PerformPredictionParams is parameters of performPrediction operation.
type PerformPredictionParams struct {
LaunchLatitude OptFloat64
LaunchLongitude OptFloat64
LaunchDatetime OptDateTime
LaunchAltitude OptFloat64
Profile OptPerformPredictionProfile
AscentRate OptFloat64
BurstAltitude OptFloat64
DescentRate OptFloat64
FloatAltitude OptFloat64
StopDatetime OptDateTime
AscentCurve OptString
DescentCurve OptString
Interpolate OptBool
Format OptPerformPredictionFormat
Dataset OptDateTime
LaunchLatitude OptFloat64 `json:",omitempty,omitzero"`
LaunchLongitude OptFloat64 `json:",omitempty,omitzero"`
LaunchDatetime OptDateTime `json:",omitempty,omitzero"`
LaunchAltitude OptFloat64 `json:",omitempty,omitzero"`
Profile OptPerformPredictionProfile `json:",omitempty,omitzero"`
AscentRate OptFloat64 `json:",omitempty,omitzero"`
BurstAltitude OptFloat64 `json:",omitempty,omitzero"`
DescentRate OptFloat64 `json:",omitempty,omitzero"`
FloatAltitude OptFloat64 `json:",omitempty,omitzero"`
StopDatetime OptDateTime `json:",omitempty,omitzero"`
AscentCurve OptString `json:",omitempty,omitzero"`
DescentCurve OptString `json:",omitempty,omitzero"`
SimulateStages []PerformPredictionSimulateStagesItem `json:",omitempty"`
Interpolate OptBool `json:",omitempty,omitzero"`
Format OptPerformPredictionFormat `json:",omitempty,omitzero"`
Dataset OptDateTime `json:",omitempty,omitzero"`
}
func unpackPerformPredictionParams(packed middleware.Parameters) (params PerformPredictionParams) {
@ -143,6 +144,15 @@ func unpackPerformPredictionParams(packed middleware.Parameters) (params Perform
params.DescentCurve = v.(OptString)
}
}
{
key := middleware.ParameterKey{
Name: "simulate_stages",
In: "query",
}
if v, ok := packed[key]; ok {
params.SimulateStages = v.([]PerformPredictionSimulateStagesItem)
}
}
{
key := middleware.ParameterKey{
Name: "interpolate",
@ -787,6 +797,71 @@ func decodePerformPredictionParams(args [0]string, argsEscaped bool, r *http.Req
Err: err,
}
}
// Decode query: simulate_stages.
if err := func() error {
cfg := uri.QueryParameterDecodingConfig{
Name: "simulate_stages",
Style: uri.QueryStyleForm,
Explode: true,
}
if err := q.HasParam(cfg); err == nil {
if err := q.DecodeParam(cfg, func(d uri.Decoder) error {
return d.DecodeArray(func(d uri.Decoder) error {
var paramsDotSimulateStagesVal PerformPredictionSimulateStagesItem
if err := func() error {
val, err := d.DecodeValue()
if err != nil {
return err
}
c, err := conv.ToString(val)
if err != nil {
return err
}
paramsDotSimulateStagesVal = PerformPredictionSimulateStagesItem(c)
return nil
}(); err != nil {
return err
}
params.SimulateStages = append(params.SimulateStages, paramsDotSimulateStagesVal)
return nil
})
}); err != nil {
return err
}
if err := func() error {
var failures []validate.FieldError
for i, elem := range params.SimulateStages {
if err := func() error {
if err := elem.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: fmt.Sprintf("[%d]", i),
Error: err,
})
}
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}(); err != nil {
return err
}
}
return nil
}(); err != nil {
return params, &ogenerrors.DecodeParamError{
Name: "simulate_stages",
In: "query",
Err: err,
}
}
// Decode query: interpolate.
if err := func() error {
cfg := uri.QueryParameterDecodingConfig{

View file

@ -9,7 +9,6 @@ import (
"github.com/go-faster/errors"
"github.com/go-faster/jx"
"github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/validate"
)

View file

@ -7,10 +7,9 @@ import (
"github.com/go-faster/errors"
"github.com/go-faster/jx"
ht "github.com/ogen-go/ogen/http"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
ht "github.com/ogen-go/ogen/http"
)
func encodePerformPredictionResponse(response *PredictionResult, w http.ResponseWriter, span trace.Span) error {

View file

@ -112,6 +112,7 @@ type Route struct {
name string
summary string
operationID string
operationGroup string
pathPattern string
count int
args [0]string
@ -134,6 +135,11 @@ func (r Route) OperationID() string {
return r.operationID
}
// OperationGroup returns the x-ogen-operation-group value.
func (r Route) OperationGroup() string {
return r.operationGroup
}
// PathPattern returns OpenAPI path.
func (r Route) PathPattern() string {
return r.pathPattern
@ -209,6 +215,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = PerformPredictionOperation
r.summary = "Perform prediction"
r.operationID = "performPrediction"
r.operationGroup = ""
r.pathPattern = "/api/v1/prediction"
r.args = args
r.count = 0
@ -233,6 +240,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.name = ReadinessCheckOperation
r.summary = "Readiness check"
r.operationID = "readinessCheck"
r.operationGroup = ""
r.pathPattern = "/ready"
r.args = args
r.count = 0

View file

@ -430,6 +430,54 @@ func (s *PerformPredictionProfile) UnmarshalText(data []byte) error {
}
}
type PerformPredictionSimulateStagesItem string
const (
PerformPredictionSimulateStagesItemAscent PerformPredictionSimulateStagesItem = "ascent"
PerformPredictionSimulateStagesItemDescent PerformPredictionSimulateStagesItem = "descent"
PerformPredictionSimulateStagesItemFloat PerformPredictionSimulateStagesItem = "float"
)
// AllValues returns all PerformPredictionSimulateStagesItem values.
func (PerformPredictionSimulateStagesItem) AllValues() []PerformPredictionSimulateStagesItem {
return []PerformPredictionSimulateStagesItem{
PerformPredictionSimulateStagesItemAscent,
PerformPredictionSimulateStagesItemDescent,
PerformPredictionSimulateStagesItemFloat,
}
}
// MarshalText implements encoding.TextMarshaler.
func (s PerformPredictionSimulateStagesItem) MarshalText() ([]byte, error) {
switch s {
case PerformPredictionSimulateStagesItemAscent:
return []byte(s), nil
case PerformPredictionSimulateStagesItemDescent:
return []byte(s), nil
case PerformPredictionSimulateStagesItemFloat:
return []byte(s), nil
default:
return nil, errors.Errorf("invalid value: %q", s)
}
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (s *PerformPredictionSimulateStagesItem) UnmarshalText(data []byte) error {
switch PerformPredictionSimulateStagesItem(data) {
case PerformPredictionSimulateStagesItemAscent:
*s = PerformPredictionSimulateStagesItemAscent
return nil
case PerformPredictionSimulateStagesItemDescent:
*s = PerformPredictionSimulateStagesItemDescent
return nil
case PerformPredictionSimulateStagesItemFloat:
*s = PerformPredictionSimulateStagesItemFloat
return nil
default:
return errors.Errorf("invalid value: %q", data)
}
}
// Ref: #/components/schemas/PredictionResult
type PredictionResult struct {
Metadata PredictionResultMetadata `json:"metadata"`
@ -511,6 +559,7 @@ type PredictionResultPredictionItemStage string
const (
PredictionResultPredictionItemStageAscent PredictionResultPredictionItemStage = "ascent"
PredictionResultPredictionItemStageDescent PredictionResultPredictionItemStage = "descent"
PredictionResultPredictionItemStageFloat PredictionResultPredictionItemStage = "float"
)
// AllValues returns all PredictionResultPredictionItemStage values.
@ -518,6 +567,7 @@ func (PredictionResultPredictionItemStage) AllValues() []PredictionResultPredict
return []PredictionResultPredictionItemStage{
PredictionResultPredictionItemStageAscent,
PredictionResultPredictionItemStageDescent,
PredictionResultPredictionItemStageFloat,
}
}
@ -528,6 +578,8 @@ func (s PredictionResultPredictionItemStage) MarshalText() ([]byte, error) {
return []byte(s), nil
case PredictionResultPredictionItemStageDescent:
return []byte(s), nil
case PredictionResultPredictionItemStageFloat:
return []byte(s), nil
default:
return nil, errors.Errorf("invalid value: %q", s)
}
@ -542,6 +594,9 @@ func (s *PredictionResultPredictionItemStage) UnmarshalText(data []byte) error {
case PredictionResultPredictionItemStageDescent:
*s = PredictionResultPredictionItemStageDescent
return nil
case PredictionResultPredictionItemStageFloat:
*s = PredictionResultPredictionItemStageFloat
return nil
default:
return errors.Errorf("invalid value: %q", data)
}

View file

@ -6,7 +6,6 @@ import (
"fmt"
"github.com/go-faster/errors"
"github.com/ogen-go/ogen/validate"
)
@ -34,6 +33,19 @@ func (s PerformPredictionProfile) Validate() error {
}
}
func (s PerformPredictionSimulateStagesItem) Validate() error {
switch s {
case "ascent":
return nil
case "descent":
return nil
case "float":
return nil
default:
return errors.Errorf("invalid value: %v", s)
}
}
func (s *PredictionResult) Validate() error {
if s == nil {
return validate.ErrNilPointer
@ -131,6 +143,8 @@ func (s PredictionResultPredictionItemStage) Validate() error {
return nil
case "descent":
return nil
case "float":
return nil
default:
return errors.Errorf("invalid value: %v", s)
}

12
run.bat Normal file
View file

@ -0,0 +1,12 @@
@echo off
REM Batch startup script for predictor service
REM Sets required environment variables and starts the service
set GSN_PREDICTOR_GRIB_DIR="c://tmp/grib"
set GSN_PREDICTOR_GRIB_TTL=48h
echo Starting predictor service with GRIB directory: %GSN_PREDICTOR_GRIB_DIR%
echo Dataset TTL: %GSN_PREDICTOR_GRIB_TTL%
REM Run the service
go run cmd/api/main.go

11
run.ps1 Normal file
View file

@ -0,0 +1,11 @@
# PowerShell startup script for predictor service
# Sets required environment variables and starts the service
$env:GSN_PREDICTOR_GRIB_DIR = "C:/tmp/grib"
$env:GSN_PREDICTOR_GRIB_TTL = "48h" # Allow datasets up to 48 hours old
Write-Host "Starting predictor service with GRIB directory: $env:GSN_PREDICTOR_GRIB_DIR" -ForegroundColor Green
Write-Host "Dataset TTL: $env:GSN_PREDICTOR_GRIB_TTL" -ForegroundColor Green
# Run the service
go run cmd/api/main.go