Skip to content

Conversation

JoelCourtney
Copy link
Contributor

@JoelCourtney JoelCourtney commented Jul 24, 2025

  • Tickets addressed: AERIE-000
  • Review: By commit
  • Merge strategy: Merge (no squash)

Description

This PR lets goals and constraints query resources by referencing them through the mission model. Meaning you can write simResults.resource(model.fruit) instead of simResults.resource("/fruit", Real.deserializer()), where mission is a valid instance of the mission model.

  • Vile hack No. 1: The goal/constraint gets the instance of the model through the WithModel<M> interface, which has a mutable static field modelSingleton. The scheduler or constraint action creates an instance of the model with the default sim config and sets the sim config at the beginning of the action. Goals/constraints that inherit from WithModel<M> can then get that instance with this.model().
  • Vile Hack No. 2: The only code this adds to the mission model is a NameableResource interface and NamedResource abstract implementor of that interface for convenience. Concrete resource types can inherit from NamedResource, which just lets the Registrar call NameableResource.setName(string). This happens during the model constructor, so it happens both during simulation (which is useless) and during scheduling/constraint actions. The timeline library SimulationResults then provides a few resource(...) overloads that look for things like NameableResource<RealDynamics>, NameableResource<Boolean>, etc. It calls NameableResource.getName() and associates the payload type with a timeline type like Real, Booleans, etc.

This required some fiddling with ClassLoaders to make sure that the mission model was not loaded twice by different loaders (which isn't just a waste, it also causes a class cast exception).

Verification

I've made two integration tests, one for scheduling and one for constraints. I've also manually verified that the constraint/goal jars don't include the mission model. (unzip -l <jar> | grep banana)

Documentation

I swear I'll update the guide docs this time.

Future work

Because Vile Hack No. 1 creates the model with the default sim config, this could cause some issues when we support other configs during scheduling. I'm assuming that the default config corresponds to the full model with no resources left out.

@JoelCourtney JoelCourtney force-pushed the feat/mission-model-in-procedures branch from 4981ca6 to 92c4f31 Compare July 25, 2025 00:48
Copy link

@JoelCourtney JoelCourtney requested a review from mattdailis July 29, 2025 23:11
@JoelCourtney JoelCourtney marked this pull request as ready for review July 29, 2025 23:11
@JoelCourtney JoelCourtney requested a review from a team as a code owner July 29, 2025 23:11
@JoelCourtney JoelCourtney requested review from jmdelfa and removed request for jmdelfa July 29, 2025 23:11
Copy link
Collaborator

@mattdailis mattdailis left a comment

Choose a reason for hiding this comment

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

No changes requested yet, just some clarifying questions so far. I haven't dug into the classloader business yet, that'll be in a follow-up review

Comment on lines +31 to +34
fun resource(instance: NameableResource<RealDynamics>) = resource(instance.name, Real.deserializer())
fun resource(instance: NameableResource<Boolean>) = resource(instance.name, Booleans.deserializer())
fun resource(instance: NameableResource<String>) = resource(instance.name, Strings.deserializer())
fun resource(instance: NameableResource<Number>) = resource(instance.name, Numbers.deserializer())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Kotlin newbie question: it seems like there are multiple functions defined here with the same name, and whose arguments have the same type erasure—how is this possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wow I didn't even realize how unusual this was when I wrote it. You're right, this would not work in Java. Apparently the JVM is able to differentiate overloads with the same argument type erasure if their return types are different, but Java is not. I don't understand how, but some smart people tried to explain it here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Personally even though I don't really understand it, I'm satisfied because the test cases work in Java. And the type inference works correctly, somehow (the test cases use var, and it is still able to infer the type).

I guess this means that Java's ability to resolve overloads is more flexible/powerful than what it actually allows you to declare. Which is kinda nuts.

*
* @throws ArithmeticException if any of the segment values are not integers
*/
@JvmStatic fun intDeserializer() = { list: List<Segment<SerializedValue>> -> Numbers(list.map { seg ->
Copy link
Collaborator

Choose a reason for hiding this comment

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

The new deserializer methods in this class are unused, so I'm not totally sure how they're related to the rest of this PR

}

companion object {
@JvmStatic var modelSingleton: Any? = null
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just for your awareness/amusement, we do employ a similar pattern on the mission model initialization side:

static final Scoped<Context> context = Scoped.create();

The reason we do it there is to enable static methods like delay and spawn to hook into the context object.

Here, I'm less sure that model() must be a static method. Would it be much less ergonomic for it to be a member of EditablePlan?

Another aside: I recall @Twisol emphasizing that the context linked above was not so much a singleton as a "dynamically scoped variable". This meant that 1) it was scoped to a particular call stack and would definitely not survive beyond it, and 2) it was scoped to a single thread, limiting its "globalness".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think you're right, it probably could be an instance method on EditablePlan. I'll look into it.

ensureRequestIsCurrent(specification, request);
//create scheduler problem seeded with initial plan
final var schedulerMissionModel = loadMissionModel(planMetadata);
WithModel.setModelSingleton(schedulerMissionModel.missionModel.getModel());
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we stick with the singleton approach here, it may be worth making it an AutoCloseable and putting it in a try-with-resources here. That way we can reliably clear it out once we're done with it, so it doesn't stick around hogging memory or possibly being accidentally reused on a future run.

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