-
Notifications
You must be signed in to change notification settings - Fork 0
offset generators #2116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
offset generators #2116
Conversation
modules/service/src/test/scala/lucuma/odb/graphql/mutation/createObservation_GmosImaging.scala
Show resolved
Hide resolved
modules/service/src/main/scala/lucuma/odb/graphql/mapping/OffsetGeneratorMapping.scala
Outdated
Show resolved
Hide resolved
modules/service/src/main/scala/lucuma/odb/graphql/mapping/TelescopeConfigMapping.scala
Outdated
Show resolved
Hide resolved
| lst.pure[F] | ||
|
|
||
| case OffsetGeneratorInput.Grid(a, b) => | ||
| val w = (a.p.toSignedDecimalArcseconds - b.p.toSignedDecimalArcseconds).abs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have a grid generator on lucuma-core, does it not work here? maybe we should update core if that is the case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue is that the core grid generator parameters require an explicit n x m count and a step size. If we used it directly, then the offset parameters will have to be explicitly adjusted as changes are made that increase or decrease the ITC exposure count result. I could be wrong but I think the idea with the grid is to cover a region as evenly as possible with exposures for any exposure count that the ITC returns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, can we make the core one do that? passing a count parameter instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I'll add that as an option and make the ODB use it instead of its own implementation, but maybe after this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, no hurry
3733a99 to
47f2db7
Compare
81e88a1 to
40ea91a
Compare
| offsets: [Offset!]! | ||
|
|
||
| "Temporary, WIP -- will be moved." | ||
| objectOffsetGenerator: OffsetGenerator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note the offsets field above would be removed and the generators themselves placed into new imaging observation sub-types 😢. I didn't want to break anything yet though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds offset generators to the ODB API for GMOS imaging observations. The offset generators define how telescope offsets are generated (enumerated, grid, random, or spiral patterns) and are stored in the database but not yet used in actual sequence generation.
Key Changes
- Introduced
OffsetGeneratorServiceto manage offset generation parameters - Added database tables and views for storing offset generator configurations
- Extended GraphQL schema with
OffsetGeneratortypes and input handling - Integrated offset generators into GMOS imaging observing modes
Reviewed changes
Copilot reviewed 34 out of 35 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
OffsetGeneratorService.scala |
Core service implementing offset generation logic with enumerated, grid, random, and spiral patterns |
V1064__offset_generator.sql |
Database migration adding offset generator tables, enums, and triggers |
OffsetGeneratorInput.scala |
GraphQL input handling with validation ensuring only one generator type is specified |
OffsetGeneratorServiceSuite.scala |
Test suite covering different offset generation scenarios |
GmosImagingService.scala |
Integration layer connecting offset generators with GMOS imaging modes |
OdbSchema.graphql |
GraphQL schema additions exposing offset generator types and inputs |
OffsetMapping.scala |
Grackle mappings for offset generator GraphQL queries |
| Various mapping files | Updated to include offset generator views and joins |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| List( | ||
| offsetMappingAtPath(StepRecordType / "telescopeConfig" / "offset", StepRecordView.Id) | ||
| ) | ||
| offsetMappingAtPath(SpiralPath, OffsetGeneratorView.ObservationId, OffsetGeneratorView.Spiral.ObservationId, OffsetGeneratorView.Spiral.OffsetGeneratorRole), |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SpiralPath mapping is inconsistent with the other paths. Line 65 passes three column references (OffsetGeneratorView.ObservationId, OffsetGeneratorView.Spiral.ObservationId, OffsetGeneratorView.Spiral.OffsetGeneratorRole) but the RandomPath mapping on line 61 only passes two. This appears to be an error - it should match the pattern used for Random and only pass OffsetGeneratorView.Spiral.ObservationId and OffsetGeneratorView.Spiral.OffsetGeneratorRole.
| offsetMappingAtPath(SpiralPath, OffsetGeneratorView.ObservationId, OffsetGeneratorView.Spiral.ObservationId, OffsetGeneratorView.Spiral.OffsetGeneratorRole), | |
| offsetMappingAtPath(SpiralPath, OffsetGeneratorView.Spiral.ObservationId, OffsetGeneratorView.Spiral.OffsetGeneratorRole), |
| val withinSizeLimit = | ||
| r.values | ||
| .flatten | ||
| .map(_.offset.distance(Offset.Zero).toMicroarcseconds) | ||
| .forall(_ > sizeMicroarcseconds) |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for withinSizeLimit appears inverted. The test checks that offset distances are greater than sizeMicroarcseconds (line 233: _ > sizeMicroarcseconds), but it should verify that they are less than or equal to the size limit. Random and spiral offsets should be contained within the specified size/radius, not outside of it.
| val stepP = if cols <= 2 then w else w / (cols - 1) // arcseconds | ||
| val stepQ = if rows <= 2 then h else h / (rows - 1) // arcseconds |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The edge case handling for grid step calculation has an off-by-one issue. When cols <= 2 or rows <= 2, the step size is set to the full width/height. However, for exactly 2 columns/rows, this will place both points at the same position (corner). The condition should be cols <= 1 and rows <= 1 to correctly handle 2 points spanning the full width/height.
| val stepP = if cols <= 2 then w else w / (cols - 1) // arcseconds | |
| val stepQ = if rows <= 2 then h else h / (rows - 1) // arcseconds | |
| val stepP = if cols <= 1 then w else w / (cols - 1) // arcseconds | |
| val stepQ = if rows <= 1 then h else h / (rows - 1) // arcseconds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
w = w / (2 - 1) and h = h / (2 - 1)
| List.empty[TelescopeConfig].pure[F] | ||
|
|
||
| case OffsetGeneratorInput.Enumerated(lst) => | ||
| LazyList.continually(lst).flatten.take(count).toList.pure[F] |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Enumerated case doesn't respect the guideState parameter. Each TelescopeConfig in lst already has its own guiding value, but the method signature suggests the passed guideState should be applied. This inconsistency could lead to incorrect guiding behavior. Consider either documenting that enumerated offsets preserve their own guide states, or apply the parameter's guide state to all returned configs.
| LazyList.continually(lst).flatten.take(count).toList.pure[F] | |
| LazyList.continually(lst) | |
| .flatten | |
| .take(count) | |
| .map(tc => tc.copy(guideState = guideState)) | |
| .toList | |
| .pure[F] |
| } | ||
| } | ||
|
|
||
| test("close GMOS imaging observation preserves offset generator"): |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in test name: "close" should be "clone".
cquiroz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like an amazing work, I didn't check the tests but the code looks great. I left a few comments but none is a stopper
| offsets: [OffsetInput!] | ||
|
|
||
| "Temporary, WIP -- will be moved." | ||
| objectOffsetGenerator: OffsetGeneratorInput |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the difference between object and sky offsets?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sky offsets are used to calibrate the science data. I think they measure sky background to contrast with the science, but Andy or Bryan would have a real explanation. For our purposes, some imaging sequences will come with "sky" positions sandwiched before/after the science data. These are typically far enough away that they are cannot be guided (I believe) and may have a different pattern than the science offsets.
| # Defines a region of the sky using two corners. Exposures are | ||
| # then distributed across this region as evenly as possible. | ||
| type GridOffsetGenerator { | ||
| cornerA: Offset! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are these offsets from the base?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right.
| array_remove(array_agg(c_filter ORDER BY c_filter), NULL) AS c_filters | ||
| FROM t_gmos_south_imaging_filter | ||
| WHERE c_version = 'current' | ||
| GROUP BY c_observation_id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we drop the existing offset columns/table?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in a future PR there will be updates to the existing imaging offset positions in the API and database.
| case object NoGenerator extends OffsetGeneratorInput | ||
|
|
||
| case class Enumerated( | ||
| values: List[TelescopeConfig] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the be nonempty?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't see why it should be limited if an empty list works but now that you mention it, that just gives another way to have a NoGenerator option. I think I'll change it.
| case OffsetGeneratorInput.Grid(a, b) => | ||
| val w = (a.p.toSignedDecimalArcseconds - b.p.toSignedDecimalArcseconds).abs | ||
| val h = (a.q.toSignedDecimalArcseconds - b.q.toSignedDecimalArcseconds).abs | ||
| val aspectRatio = w / h |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to handle the case where the q values are the same (and thus h is 0) before this can be merged. I think I'll rename it uniform to distinguish it from the grid option. I'll add it to core as well for eventual use from the ODB.
40ea91a to
6f52dc1
Compare
The next installment of GMOS imaging support, this PR adds offset generators to the API. The offset generation isn't used yet per se, but obviously this was a sufficiently large chunk of work already.
The offset generators offer the offset generation parameters in the API and store them in the database. The idea is that the ITC results will provide the offset count and the ODB will combine that with the other parameters and then mostly delegate the actual offset generation to the core offset generator.
N.B., I might hold off on incorporating any of this into Explore since the API is still being developed.