Skip to content

Support flights as planned#1331

Open
BenjaminPelletier wants to merge 9 commits intointeruss:mainfrom
BenjaminPelletier:flights-as-planned
Open

Support flights as planned#1331
BenjaminPelletier wants to merge 9 commits intointeruss:mainfrom
BenjaminPelletier:flights-as-planned

Conversation

@BenjaminPelletier
Copy link
Member

This PR addresses (but does not fully resolve) #1326 by:

  1. Updating mock_uss to populate the new as_planned field in flight_planning responses after "snapping" requested start times to the current wall time (preventing users from rewriting history by saying a flight started in the past)
  2. Improving the flight_planning client to recognize as_planned in the flight_planning API response and populate a new as_planned field in the business object response
  3. Improving uss_qualifier flight_planning operations to return the FlightInfo as planned, including when it differs from what was requested
  4. Validating that the as_planned response matched what was requested (very strictly, for now) via a new injection fidelity check
  5. Using the as-planned flight info for the remainder of each test rather than assuming the flight info was exactly as requested

To accomplish 4 above, the large injection_evaluation.py tool is added. This attempts to make evaluation of as-planned flights as future-scalable as practical by performing bulk field-by-field comparisons automatically according to comparators defined per JSONPath in addition to a default comparator.

Ideally, each scenario would define what is acceptable for an as-planned flight (though hopefully they would reuse many of the same definitions -- e.g., "anything that overlaps the other flight"). However, making all these changes in one PR would be far too large. Instead, placeholder TODOs referencing the appropriate issue are added in the places where each scenario could (in the future) define what is acceptable for as-planned flights, and one "global" validation is added in flight_planning/test_steps.py:submit_flight which requires all fields in as_planned to match exactly (status quo), except start times are allowed to be adjusted forward to the wall clock.

In addition to the standard CI, I also tested as_planned validation by temporarily setting mock_uss to set start time to wall clock + 5 seconds (instead of just wall clock in this actual PR) and observing that the Injection fidelity check failed. See the diff of mock_uss/flights/planning.py on the second commit for this approach (though the timing did not correspond to exactly commit 2).

A number of small errors/basedpyright findings are resolved as discovered during development of the core feature.

@BenjaminPelletier BenjaminPelletier marked this pull request as ready for review February 11, 2026 05:47
Copy link
Contributor

@mickmis mickmis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM modulo comments

)

return resp, flight_id
if resp.flight_plan_status in {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: improve readability with early return with return resp, flight_id, None if status is not among those
(could also apply to inner if with return resp, flight_id, flight_info)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good -- done



def values_exactly_equal(
as_requested: str | Number | StringBasedDateTime,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How were those types chosen? Is it because that's the exhaustive list of types the JSON values can take? Although I expect not because there is no object-like type.

Also, nit: define a type alias for those? Notably if we expect this list will be extended in the future.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are just the types this CompatibilityEvaluator (values_exactly_equal) is capable of evaluating. They do correspond to leaf JSON values (list and object/dict are inner branches of the JSON value tree). Technically, both parameters could be type-annotated as Any since that matches the general CompatibilityEvaluator pattern, but listing the specific types acts as an additional form of documentation regarding when this CompatibilityEvaluator can be used.

I think in this case, a type alias would just add an unnecessary level of indirection and additional content since it's only used twice on adjacent lines.

return
if dtype == StringBasedDateTime:
assert isinstance(as_planned, StringBasedDateTime)
assert isinstance(as_requested, StringBasedDateTime)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant since we are in if isinstance(as_requested, dtype) and if dtype == StringBasedDateTime blocks.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With sufficiently sophisticated logic this is redundant, but alas, basedpyright's logic is not that sophisticated and it produces an error if this assertion of type is removed. It would probably be ideal for it to be a little smarter, but I don't think I mind a few extra assertions like this in exchange for the validation it performs, and usually it's pretty smart.

)
return
if dtype == StringBasedDateTime:
assert isinstance(as_planned, StringBasedDateTime)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this redundant as well since we previously checked for if not isinstance(as_planned, dtype)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the other line, this is needed for basedpyright



def _resolve_addresses(
paths: Iterable[JSONPath] | JSONPath, content: dict[str, Any]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we check below if that's an str type?

Suggested change
paths: Iterable[JSONPath] | JSONPath, content: dict[str, Any]
paths: Iterable[JSONPath] | JSONPath | str, content: dict[str, Any]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSONPath is just a str with extra semantic meaning -- checking for str below is nearly equivalent to checking for JSONPath, so I've just changed that isinstance check to JSONPath rather than str.

details=f"As requested, {address} has {len(as_requested)} elements, but as planned, {address} has {len(as_planned)} elements (equality expected)",
)
return
for i, v in enumerate(as_requested):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implies the order is important. Is it actually? Wouldn't two lists with the same items but in different order be equivalent? I don't remember some entities in the APIs we use where it is. But I have not checked that exhaustively.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, order is important -- for instance, this might be a list of points describing the outline of an area. I'm sure there are applications where order isn't important, but I think it is important by default.

self._begin_step_fragment()
oi_ref = self._operational_intent_shared_check(flight_info, skip_if_not_found)
if flight_info is None:
raise ScenarioLogicError(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't ScenarioDidNotStopError more appropriate here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason this isn't a ScenarioDidNotStopError is that that error requires a check to be specified (longer name probably something like "Scenario didn't stop due to a failed check that I expected to stop the scenario") and that check is no longer available here. ScenarioLogicError is the more generic error that doesn't required a check.

flight1_planned,
)
# TODO(#1326): Validate that flight as planned still allows this scenario to proceed
assert as_planned is not None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the asserts in this file (and also in others) really required? If there is an actual probability of this happening shouldn't it be handled properly? (i.e. if because of USS -> fail, if because of inconsistency in test suite -> raise exception)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The asserts are almost exclusively (perhaps wholly exclusively) for the benefit of basedpyright. They are effectively providing a human-machine interface layer where the human writes something they think is obviously true (via an assertion), and then the machine validates behavior given that assumption. A human reviewer can review the explicit assumptions the human author is making (making sure the assertions are justified), and then those assumptions are programmatically checked at runtime too. So, while I don't know that they're enormously valuable, I do think they have some value since they enable basedpyright's more extensive validation and also cause things to fail fast as soon as some assumption is violated.

ImplicitDict.parse(query.response.json["as_planned"], FlightPlan)
)
except ValueError:
# Best effort failed so it's ok to ignore additional `as_planned` field
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to log as a warning the error here? That would probably help troubleshoot issues, if any.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense; added warning.

if "as_planned" in inject_resp and inject_resp.as_planned:
resp.as_planned = inject_resp.as_planned.to_flight_plan()
for k, v in inject_resp.items():
if k not in {"planning_result", "flight_plan_status", "has_conflict"}:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be sure that it is not a mistake: has_conflict is no longer required here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that was my intent -- I believe "has_conflict" should actually have not been there previously. The purpose of this list is to avoid overwriting already-populated fields in the response when transferring any additional metadata/extra fields that may have been provided. Since has_conflict wasn't already populated and there's no reason to specifically exclude it, I think it should be removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments